Compare commits
53 Commits
lite-v1.0.
...
229373e937
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
229373e937 | ||
|
|
973c390df5 | ||
|
|
d684db446e | ||
|
|
9e1b1397ad | ||
|
|
55a36e0d06 | ||
|
|
7937aad4fb | ||
|
|
077f9a7403 | ||
|
|
9f23b2781c | ||
|
|
79d8f0d809 | ||
|
|
dc4426810f | ||
|
|
915c1b4d23 | ||
|
|
28b9e0dffb | ||
|
|
6be0a58c26 | ||
| fbdba1a001 | |||
| 821c54ba2b | |||
| 3ff62ca248 | |||
| bbf53a130c | |||
| 1f9e43d7b2 | |||
| 5ebccceffc | |||
| 9d2d581474 | |||
| 096f8ee90e | |||
| ca199ef195 | |||
| 97bd2f8168 | |||
|
|
09f287fbc5 | ||
|
|
b3d43ba0ad | ||
| 430290f97a | |||
|
|
30fc5da520 | ||
| f02c965929 | |||
| f0b7b88ef2 | |||
|
|
53d08de639 | ||
|
|
8645a82e4f | ||
|
|
9e94952e0a | ||
|
|
4a841fd032 | ||
|
|
f0c87e4092 | ||
|
|
c5ef4899bb | ||
|
|
36b67e69d0 | ||
|
|
06c80ef51c | ||
|
|
6bd5341507 | ||
|
|
5284c0dbb6 | ||
|
|
cf520fdf40 | ||
|
|
96c27bb949 | ||
|
|
cc617dd5be | ||
|
|
653a90de62 | ||
|
|
4b16a2a2c4 | ||
|
|
c51d3dafff | ||
|
|
68c2a59d09 | ||
|
|
45a2ccd9f3 | ||
|
|
0ca1caf148 | ||
|
|
7fb1f1de9d | ||
|
|
386cc857b0 | ||
|
|
3e6136983a | ||
|
|
2c1862aed3 | ||
|
|
4b815fc9d1 |
173
.github/copilot-instructions.md
vendored
@@ -1,173 +0,0 @@
|
|||||||
# Copilot Instructions — DragonX ImGui Wallet
|
|
||||||
|
|
||||||
## UI Layout: All values in `ui.toml`
|
|
||||||
|
|
||||||
**Every UI layout constant must be defined in `res/themes/ui.toml` and read at runtime via the schema API.** Never hardcode pixel sizes, ratios, rounding values, thicknesses, or spacing constants directly in C++ source files. This is critical for maintainability, theming support, and hot-reload.
|
|
||||||
|
|
||||||
### Schema API reference
|
|
||||||
|
|
||||||
The singleton is accessed via `schema::UI()` (header: `#include "../schema/ui_schema.h"`).
|
|
||||||
|
|
||||||
| Method | Returns | Use for |
|
|
||||||
|---|---|---|
|
|
||||||
| `drawElement(section, name)` | `DrawElementStyle` | Custom DrawList layout values (`.size`, `.height`, `.thickness`, `.radius`, `.opacity`) |
|
|
||||||
| `button(section, name)` | `ButtonStyle` | Button width/height/font |
|
|
||||||
| `input(section, name)` | `InputStyle` | Input field dimensions |
|
|
||||||
| `label(section, name)` | `LabelStyle` | Label styling |
|
|
||||||
| `table(section, name)` | `TableStyle` | Table layout |
|
|
||||||
| `window(section, name)` | `WindowStyle` | Window/dialog dimensions |
|
|
||||||
| `combo(section, name)` | `ComboStyle` | Combo box styling |
|
|
||||||
| `slider(section, name)` | `SliderStyle` | Slider styling |
|
|
||||||
| `checkbox(section, name)` | `CheckboxStyle` | Checkbox styling |
|
|
||||||
| `separator(section, name)` | `SeparatorStyle` | Separator/divider styling |
|
|
||||||
|
|
||||||
### Section naming convention
|
|
||||||
|
|
||||||
Sections use dot-separated paths matching the file/feature:
|
|
||||||
|
|
||||||
- `tabs.send`, `tabs.receive`, `tabs.transactions`, `tabs.mining`, `tabs.peers`, `tabs.market` — tab-specific values
|
|
||||||
- `tabs.balance` — balance/home tab
|
|
||||||
- `components.main-layout`, `components.settings-page` — shared components
|
|
||||||
- `dialogs.about`, `dialogs.backup`, etc. — dialog-specific values
|
|
||||||
- `sidebar` — navigation sidebar
|
|
||||||
|
|
||||||
### How to add a new layout value
|
|
||||||
|
|
||||||
1. **Add the entry to `res/themes/ui.toml`** under the appropriate section:
|
|
||||||
```toml
|
|
||||||
[tabs.example]
|
|
||||||
my-element = {size = 42.0}
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Read it in C++** instead of using a literal:
|
|
||||||
```cpp
|
|
||||||
// WRONG — hardcoded
|
|
||||||
float myValue = 42.0f;
|
|
||||||
|
|
||||||
// CORRECT — schema-driven
|
|
||||||
float myValue = schema::UI().drawElement("tabs.example", "my-element").size;
|
|
||||||
```
|
|
||||||
|
|
||||||
3. For values used as min/max pairs with scaling:
|
|
||||||
```cpp
|
|
||||||
// WRONG
|
|
||||||
float h = std::max(18.0f, 24.0f * vScale);
|
|
||||||
|
|
||||||
// CORRECT
|
|
||||||
float h = std::max(
|
|
||||||
schema::UI().drawElement("tabs.example", "row-min-height").size,
|
|
||||||
schema::UI().drawElement("tabs.example", "row-height").size * vScale
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
### What belongs in `ui.toml`
|
|
||||||
|
|
||||||
- Pixel sizes (card heights, icon sizes, bar widths/heights)
|
|
||||||
- Ratios (column width ratios, max-width ratios)
|
|
||||||
- Rounding values (`FrameRounding`, corner radius)
|
|
||||||
- Thickness values (accent bars, chart lines, borders)
|
|
||||||
- Dot/circle radii
|
|
||||||
- Fade zones, padding constants
|
|
||||||
- Min/max dimension bounds
|
|
||||||
- Font selection (via schema font name strings, resolved with `S.resolveFont()`)
|
|
||||||
- Colors (via `schema::UI().resolveColor()` or color variable references like `"var(--primary)"`)
|
|
||||||
- Animation durations (transition times, fade durations, pulse speeds)
|
|
||||||
- Business logic values (fee amounts, ticker strings, buffer sizes, reward amounts)
|
|
||||||
|
|
||||||
### What does NOT belong in `ui.toml`
|
|
||||||
|
|
||||||
- Spacing that already goes through `Layout::spacing*()` or `spacing::dp()`
|
|
||||||
|
|
||||||
### Legacy system: `UILayout`
|
|
||||||
|
|
||||||
`UILayout::instance()` is the older layout system still used for fonts, typography, panels, and global spacing. New layout values should use `schema::UI().drawElement()` instead. Do not add new keys to `UILayout`.
|
|
||||||
|
|
||||||
### Validation
|
|
||||||
|
|
||||||
After editing `ui.toml`, always validate:
|
|
||||||
```bash
|
|
||||||
python3 -c "import toml; toml.load('res/themes/ui.toml'); print('Valid TOML')"
|
|
||||||
```
|
|
||||||
|
|
||||||
Or with the C++ toml++ parser (which is what the app uses):
|
|
||||||
```bash
|
|
||||||
cd build && make -j$(nproc)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Build
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Linux
|
|
||||||
cd build && make -j$(nproc)
|
|
||||||
|
|
||||||
# Windows cross-compile
|
|
||||||
./build.sh --win-release
|
|
||||||
```
|
|
||||||
|
|
||||||
## Plans
|
|
||||||
|
|
||||||
When asked to "create a plan", always create a new markdown document in the `docs/` directory with the plan contents.
|
|
||||||
|
|
||||||
## Icons: Use Material Design icon font, never Unicode symbols
|
|
||||||
|
|
||||||
**Never use raw Unicode symbols or emoji characters** (e.g. `↓`, `↗`, `⛏`, `🔍`, `📬`, `⚠️`, `ℹ`) for icons in C++ code. Always use the **Material Design Icons font** via the `ICON_MD_*` defines from `#include "../../embedded/IconsMaterialDesign.h"`.
|
|
||||||
|
|
||||||
### Icon font API
|
|
||||||
|
|
||||||
| Method | Size | Fallback |
|
|
||||||
|---|---|---|
|
|
||||||
| `Type().iconSmall()` | 14px | Body2 |
|
|
||||||
| `Type().iconMed()` | 18px | Body1 |
|
|
||||||
| `Type().iconLarge()` | 24px | H5 |
|
|
||||||
| `Type().iconXL()` | 40px | H3 |
|
|
||||||
|
|
||||||
### Correct usage
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
#include "../../embedded/IconsMaterialDesign.h"
|
|
||||||
|
|
||||||
// WRONG — raw Unicode symbol
|
|
||||||
itemSpec.leadingIcon = "↙";
|
|
||||||
|
|
||||||
// CORRECT — Material Design icon codepoint
|
|
||||||
itemSpec.leadingIcon = ICON_MD_CALL_RECEIVED;
|
|
||||||
|
|
||||||
// WRONG — emoji for search
|
|
||||||
searchSpec.leadingIcon = "🔍";
|
|
||||||
|
|
||||||
// CORRECT — Material Design icon
|
|
||||||
searchSpec.leadingIcon = ICON_MD_SEARCH;
|
|
||||||
|
|
||||||
// For rendering with icon font directly:
|
|
||||||
ImGui::PushFont(Type().iconSmall());
|
|
||||||
ImGui::TextUnformatted(ICON_MD_ARROW_DOWNWARD);
|
|
||||||
ImGui::PopFont();
|
|
||||||
```
|
|
||||||
|
|
||||||
### Why
|
|
||||||
|
|
||||||
Raw Unicode symbols and emoji render inconsistently across platforms and may not be present in the loaded text fonts. The Material Design icon font is always loaded and provides consistent, scalable glyphs on both Linux and Windows.
|
|
||||||
|
|
||||||
### Audit for Unicode symbols
|
|
||||||
|
|
||||||
Before completing any task that touches UI code, search for and replace any raw Unicode symbols that may have been introduced. Common symbols to look for:
|
|
||||||
|
|
||||||
| Unicode | Replacement |
|
|
||||||
|---------|-------------|
|
|
||||||
| `▶` `▷` | `ICON_MD_PLAY_ARROW` |
|
|
||||||
| `■` `□` `◼` `◻` | `ICON_MD_STOP` or `ICON_MD_SQUARE` |
|
|
||||||
| `●` `○` `◉` `◎` | `ICON_MD_FIBER_MANUAL_RECORD` or `ICON_MD_CIRCLE` |
|
|
||||||
| `↑` `↓` `←` `→` | `ICON_MD_ARROW_UPWARD`, `_DOWNWARD`, `_BACK`, `_FORWARD` |
|
|
||||||
| `↙` `↗` `↖` `↘` | `ICON_MD_CALL_RECEIVED`, `_MADE`, etc. |
|
|
||||||
| `✓` `✔` | `ICON_MD_CHECK` |
|
|
||||||
| `✗` `✕` `✖` | `ICON_MD_CLOSE` |
|
|
||||||
| `⚠` `⚠️` | `ICON_MD_WARNING` |
|
|
||||||
| `ℹ` `ℹ️` | `ICON_MD_INFO` |
|
|
||||||
| `🔍` | `ICON_MD_SEARCH` |
|
|
||||||
| `📋` | `ICON_MD_CONTENT_COPY` or `ICON_MD_DESCRIPTION` |
|
|
||||||
| `🛡` `🛡️` | `ICON_MD_SHIELD` |
|
|
||||||
| `⏳` | `ICON_MD_HOURGLASS_EMPTY` |
|
|
||||||
| `🔄` `↻` `⟳` | `ICON_MD_SYNC` or `ICON_MD_REFRESH` |
|
|
||||||
| `⚙` `⚙️` | `ICON_MD_SETTINGS` |
|
|
||||||
| `🔒` | `ICON_MD_LOCK` |
|
|
||||||
| `★` `☆` | `ICON_MD_STAR` or `ICON_MD_STAR_BORDER` |
|
|
||||||
10
.gitignore
vendored
@@ -32,4 +32,12 @@ imgui.ini
|
|||||||
*.bak
|
*.bak
|
||||||
*.bak*
|
*.bak*
|
||||||
*.params
|
*.params
|
||||||
asmap.dat
|
asmap.dat
|
||||||
|
/external/xmrig-hac
|
||||||
|
/memory
|
||||||
|
/todo.md
|
||||||
|
/.github/
|
||||||
|
/ObsidianDragon-agent/
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
257
CMakeLists.txt
@@ -3,12 +3,26 @@
|
|||||||
# Released under the GPLv3
|
# Released under the GPLv3
|
||||||
|
|
||||||
cmake_minimum_required(VERSION 3.20)
|
cmake_minimum_required(VERSION 3.20)
|
||||||
|
|
||||||
|
# macOS: set deployment target and universal architectures BEFORE project()
|
||||||
|
# so they propagate to all targets, including FetchContent dependencies (SDL3, etc.)
|
||||||
|
if(APPLE)
|
||||||
|
set(CMAKE_OSX_DEPLOYMENT_TARGET "11.0" CACHE STRING "Minimum macOS version" FORCE)
|
||||||
|
# Build universal binary (Apple Silicon + Intel) unless the user explicitly set architectures
|
||||||
|
if(NOT DEFINED CMAKE_OSX_ARCHITECTURES OR CMAKE_OSX_ARCHITECTURES STREQUAL "")
|
||||||
|
set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64" CACHE STRING "macOS architectures" FORCE)
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
project(ObsidianDragon
|
project(ObsidianDragon
|
||||||
VERSION 1.0.0
|
VERSION 1.2.0
|
||||||
LANGUAGES C CXX
|
LANGUAGES C CXX
|
||||||
DESCRIPTION "DragonX Cryptocurrency Wallet"
|
DESCRIPTION "DragonX Cryptocurrency Wallet"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Pre-release suffix (e.g. "-rc1", "-beta2"). Leave empty for stable releases.
|
||||||
|
set(DRAGONX_VERSION_SUFFIX "-rc1")
|
||||||
|
|
||||||
# C++17 standard
|
# C++17 standard
|
||||||
set(CMAKE_CXX_STANDARD 17)
|
set(CMAKE_CXX_STANDARD 17)
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
@@ -23,6 +37,8 @@ endif()
|
|||||||
option(DRAGONX_USE_SYSTEM_SDL3 "Use system SDL3 instead of fetching" ON)
|
option(DRAGONX_USE_SYSTEM_SDL3 "Use system SDL3 instead of fetching" ON)
|
||||||
option(DRAGONX_ENABLE_EMBEDDED_DAEMON "Enable embedded dragonxd support" ON)
|
option(DRAGONX_ENABLE_EMBEDDED_DAEMON "Enable embedded dragonxd support" ON)
|
||||||
|
|
||||||
|
include(CTest)
|
||||||
|
|
||||||
# Output directories
|
# Output directories
|
||||||
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
|
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
|
||||||
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
|
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
|
||||||
@@ -94,6 +110,32 @@ FetchContent_Declare(
|
|||||||
)
|
)
|
||||||
FetchContent_MakeAvailable(tomlplusplus)
|
FetchContent_MakeAvailable(tomlplusplus)
|
||||||
|
|
||||||
|
# SQLite amalgamation - local Explorer block-summary cache
|
||||||
|
FetchContent_Declare(
|
||||||
|
sqlite3
|
||||||
|
URL https://www.sqlite.org/2024/sqlite-amalgamation-3450300.zip
|
||||||
|
URL_HASH SHA256=ea170e73e447703e8359308ca2e4366a3ae0c4304a8665896f068c736781c651
|
||||||
|
)
|
||||||
|
FetchContent_GetProperties(sqlite3)
|
||||||
|
if(NOT sqlite3_POPULATED)
|
||||||
|
FetchContent_Populate(sqlite3)
|
||||||
|
endif()
|
||||||
|
file(GLOB SQLITE3_AMALGAMATION_C CONFIGURE_DEPENDS
|
||||||
|
${sqlite3_SOURCE_DIR}/sqlite3.c
|
||||||
|
${sqlite3_SOURCE_DIR}/*/sqlite3.c
|
||||||
|
)
|
||||||
|
if(NOT SQLITE3_AMALGAMATION_C)
|
||||||
|
message(FATAL_ERROR "SQLite amalgamation source not found")
|
||||||
|
endif()
|
||||||
|
list(GET SQLITE3_AMALGAMATION_C 0 SQLITE3_SOURCE_FILE)
|
||||||
|
get_filename_component(SQLITE3_INCLUDE_DIR ${SQLITE3_SOURCE_FILE} DIRECTORY)
|
||||||
|
add_library(sqlite3_amalgamation STATIC ${SQLITE3_SOURCE_FILE})
|
||||||
|
target_include_directories(sqlite3_amalgamation PUBLIC ${SQLITE3_INCLUDE_DIR})
|
||||||
|
target_compile_definitions(sqlite3_amalgamation PRIVATE
|
||||||
|
SQLITE_THREADSAFE=1
|
||||||
|
SQLITE_OMIT_LOAD_EXTENSION
|
||||||
|
)
|
||||||
|
|
||||||
# libcurl for HTTPS RPC connections (more reliable than cpp-httplib with OpenSSL 3.x)
|
# libcurl for HTTPS RPC connections (more reliable than cpp-httplib with OpenSSL 3.x)
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
# For Windows cross-compilation, fetch and build libcurl statically
|
# For Windows cross-compilation, fetch and build libcurl statically
|
||||||
@@ -147,6 +189,9 @@ elseif(APPLE)
|
|||||||
if(EXISTS ${CMAKE_SOURCE_DIR}/libs/libsodium-mac/lib/libsodium.a)
|
if(EXISTS ${CMAKE_SOURCE_DIR}/libs/libsodium-mac/lib/libsodium.a)
|
||||||
set(SODIUM_LIBRARY ${CMAKE_SOURCE_DIR}/libs/libsodium-mac/lib/libsodium.a)
|
set(SODIUM_LIBRARY ${CMAKE_SOURCE_DIR}/libs/libsodium-mac/lib/libsodium.a)
|
||||||
set(SODIUM_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/libs/libsodium-mac/include)
|
set(SODIUM_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/libs/libsodium-mac/include)
|
||||||
|
elseif(EXISTS ${CMAKE_SOURCE_DIR}/libs/libsodium/lib/libsodium.a)
|
||||||
|
set(SODIUM_LIBRARY ${CMAKE_SOURCE_DIR}/libs/libsodium/lib/libsodium.a)
|
||||||
|
set(SODIUM_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/libs/libsodium/include)
|
||||||
endif()
|
endif()
|
||||||
else()
|
else()
|
||||||
# Linux: prefer system libsodium, fall back to local build
|
# Linux: prefer system libsodium, fall back to local build
|
||||||
@@ -230,21 +275,39 @@ set(APP_SOURCES
|
|||||||
src/app_network.cpp
|
src/app_network.cpp
|
||||||
src/app_security.cpp
|
src/app_security.cpp
|
||||||
src/app_wizard.cpp
|
src/app_wizard.cpp
|
||||||
|
src/services/network_refresh_service.cpp
|
||||||
|
src/services/refresh_scheduler.cpp
|
||||||
|
src/services/wallet_security_controller.cpp
|
||||||
|
src/services/wallet_security_workflow.cpp
|
||||||
|
src/services/wallet_security_workflow_executor.cpp
|
||||||
src/data/wallet_state.cpp
|
src/data/wallet_state.cpp
|
||||||
|
src/data/transaction_history_cache.cpp
|
||||||
src/ui/theme.cpp
|
src/ui/theme.cpp
|
||||||
src/ui/theme_loader.cpp
|
src/ui/theme_loader.cpp
|
||||||
|
src/ui/explorer/explorer_block_cache.cpp
|
||||||
src/ui/material/color_theme.cpp
|
src/ui/material/color_theme.cpp
|
||||||
src/ui/material/typography.cpp
|
src/ui/material/typography.cpp
|
||||||
src/ui/notifications.cpp
|
src/ui/notifications.cpp
|
||||||
src/ui/windows/main_window.cpp
|
src/ui/windows/main_window.cpp
|
||||||
src/ui/windows/balance_tab.cpp
|
src/ui/windows/balance_tab.cpp
|
||||||
|
src/ui/windows/balance_address_list.cpp
|
||||||
|
src/ui/windows/balance_recent_tx.cpp
|
||||||
|
src/ui/windows/balance_tab_helpers.cpp
|
||||||
src/ui/windows/send_tab.cpp
|
src/ui/windows/send_tab.cpp
|
||||||
src/ui/windows/receive_tab.cpp
|
src/ui/windows/receive_tab.cpp
|
||||||
src/ui/windows/transactions_tab.cpp
|
src/ui/windows/transactions_tab.cpp
|
||||||
src/ui/windows/mining_tab.cpp
|
src/ui/windows/mining_tab.cpp
|
||||||
|
src/ui/windows/mining_benchmark.cpp
|
||||||
|
src/ui/windows/mining_pool_panel.cpp
|
||||||
|
src/ui/windows/mining_tab_helpers.cpp
|
||||||
src/ui/windows/peers_tab.cpp
|
src/ui/windows/peers_tab.cpp
|
||||||
|
src/ui/windows/explorer_tab.cpp
|
||||||
src/ui/windows/market_tab.cpp
|
src/ui/windows/market_tab.cpp
|
||||||
src/ui/windows/console_tab.cpp
|
src/ui/windows/console_tab.cpp
|
||||||
|
src/ui/windows/console_command_reference.cpp
|
||||||
|
src/ui/windows/console_input_model.cpp
|
||||||
|
src/ui/windows/console_output_model.cpp
|
||||||
|
src/ui/windows/console_tab_helpers.cpp
|
||||||
src/ui/windows/settings_window.cpp
|
src/ui/windows/settings_window.cpp
|
||||||
src/ui/pages/settings_page.cpp
|
src/ui/pages/settings_page.cpp
|
||||||
src/ui/windows/about_dialog.cpp
|
src/ui/windows/about_dialog.cpp
|
||||||
@@ -268,6 +331,8 @@ set(APP_SOURCES
|
|||||||
src/data/address_book.cpp
|
src/data/address_book.cpp
|
||||||
src/data/exchange_info.cpp
|
src/data/exchange_info.cpp
|
||||||
src/util/logger.cpp
|
src/util/logger.cpp
|
||||||
|
src/util/async_task_manager.cpp
|
||||||
|
src/util/amount_format.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
|
||||||
@@ -276,6 +341,8 @@ set(APP_SOURCES
|
|||||||
src/util/texture_loader.cpp
|
src/util/texture_loader.cpp
|
||||||
src/util/noise_texture.cpp
|
src/util/noise_texture.cpp
|
||||||
src/daemon/embedded_daemon.cpp
|
src/daemon/embedded_daemon.cpp
|
||||||
|
src/daemon/daemon_controller.cpp
|
||||||
|
src/daemon/lifecycle_adapters.cpp
|
||||||
src/daemon/xmrig_manager.cpp
|
src/daemon/xmrig_manager.cpp
|
||||||
src/util/bootstrap.cpp
|
src/util/bootstrap.cpp
|
||||||
src/util/secure_vault.cpp
|
src/util/secure_vault.cpp
|
||||||
@@ -308,19 +375,38 @@ endif()
|
|||||||
|
|
||||||
set(APP_HEADERS
|
set(APP_HEADERS
|
||||||
src/app.h
|
src/app.h
|
||||||
|
src/services/network_refresh_service.h
|
||||||
|
src/services/refresh_scheduler.h
|
||||||
|
src/services/wallet_security_controller.h
|
||||||
|
src/services/wallet_security_workflow.h
|
||||||
|
src/services/wallet_security_workflow_executor.h
|
||||||
src/config/version.h
|
src/config/version.h
|
||||||
src/data/wallet_state.h
|
src/data/wallet_state.h
|
||||||
|
src/data/transaction_history_cache.h
|
||||||
src/ui/theme.h
|
src/ui/theme.h
|
||||||
src/ui/theme_loader.h
|
src/ui/theme_loader.h
|
||||||
|
src/ui/explorer/explorer_block_cache.h
|
||||||
src/ui/notifications.h
|
src/ui/notifications.h
|
||||||
src/ui/windows/main_window.h
|
src/ui/windows/main_window.h
|
||||||
src/ui/windows/balance_tab.h
|
src/ui/windows/balance_tab.h
|
||||||
|
src/ui/windows/balance_address_list.h
|
||||||
|
src/ui/windows/balance_recent_tx.h
|
||||||
|
src/ui/windows/balance_tab_helpers.h
|
||||||
src/ui/windows/send_tab.h
|
src/ui/windows/send_tab.h
|
||||||
src/ui/windows/receive_tab.h
|
src/ui/windows/receive_tab.h
|
||||||
src/ui/windows/transactions_tab.h
|
src/ui/windows/transactions_tab.h
|
||||||
src/ui/windows/mining_tab.h
|
src/ui/windows/mining_tab.h
|
||||||
|
src/ui/windows/mining_benchmark.h
|
||||||
|
src/ui/windows/mining_pool_panel.h
|
||||||
|
src/ui/windows/mining_tab_helpers.h
|
||||||
src/ui/windows/peers_tab.h
|
src/ui/windows/peers_tab.h
|
||||||
|
src/ui/windows/explorer_tab.h
|
||||||
src/ui/windows/market_tab.h
|
src/ui/windows/market_tab.h
|
||||||
|
src/ui/windows/console_command_reference.h
|
||||||
|
src/ui/windows/console_input_model.h
|
||||||
|
src/ui/windows/console_output_model.h
|
||||||
|
src/ui/windows/console_tab.h
|
||||||
|
src/ui/windows/console_tab_helpers.h
|
||||||
src/ui/windows/settings_window.h
|
src/ui/windows/settings_window.h
|
||||||
src/ui/windows/about_dialog.h
|
src/ui/windows/about_dialog.h
|
||||||
src/ui/windows/key_export_dialog.h
|
src/ui/windows/key_export_dialog.h
|
||||||
@@ -344,6 +430,8 @@ set(APP_HEADERS
|
|||||||
src/data/address_book.h
|
src/data/address_book.h
|
||||||
src/data/exchange_info.h
|
src/data/exchange_info.h
|
||||||
src/util/logger.h
|
src/util/logger.h
|
||||||
|
src/util/async_task_manager.h
|
||||||
|
src/util/amount_format.h
|
||||||
src/util/base64.h
|
src/util/base64.h
|
||||||
src/util/single_instance.h
|
src/util/single_instance.h
|
||||||
src/util/i18n.h
|
src/util/i18n.h
|
||||||
@@ -351,6 +439,8 @@ set(APP_HEADERS
|
|||||||
src/util/payment_uri.h
|
src/util/payment_uri.h
|
||||||
src/util/secure_vault.h
|
src/util/secure_vault.h
|
||||||
src/daemon/embedded_daemon.h
|
src/daemon/embedded_daemon.h
|
||||||
|
src/daemon/daemon_controller.h
|
||||||
|
src/daemon/lifecycle_adapters.h
|
||||||
src/daemon/xmrig_manager.h
|
src/daemon/xmrig_manager.h
|
||||||
src/ui/effects/framebuffer.h
|
src/ui/effects/framebuffer.h
|
||||||
src/ui/effects/blur_shader.h
|
src/ui/effects/blur_shader.h
|
||||||
@@ -373,11 +463,14 @@ endif()
|
|||||||
# Windows application icon + VERSIONINFO (.rc -> .res -> linked into .exe)
|
# Windows application icon + VERSIONINFO (.rc -> .res -> linked into .exe)
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
set(OBSIDIAN_ICO_PATH "${CMAKE_SOURCE_DIR}/res/img/ObsidianDragon.ico")
|
set(OBSIDIAN_ICO_PATH "${CMAKE_SOURCE_DIR}/res/img/ObsidianDragon.ico")
|
||||||
# Version numbers for the VERSIONINFO resource block
|
# Generate manifest with version from project()
|
||||||
set(DRAGONX_VER_MAJOR 1)
|
configure_file(
|
||||||
set(DRAGONX_VER_MINOR 0)
|
${CMAKE_SOURCE_DIR}/res/ObsidianDragon.manifest.in
|
||||||
set(DRAGONX_VER_PATCH 0)
|
${CMAKE_SOURCE_DIR}/res/ObsidianDragon.manifest
|
||||||
set(DRAGONX_VERSION "1.0.0")
|
@ONLY
|
||||||
|
)
|
||||||
|
set(OBSIDIAN_MANIFEST_PATH "${CMAKE_SOURCE_DIR}/res/ObsidianDragon.manifest")
|
||||||
|
# Generate .rc with version from project()
|
||||||
configure_file(
|
configure_file(
|
||||||
${CMAKE_SOURCE_DIR}/res/ObsidianDragon.rc
|
${CMAKE_SOURCE_DIR}/res/ObsidianDragon.rc
|
||||||
${CMAKE_BINARY_DIR}/generated/ObsidianDragon.rc
|
${CMAKE_BINARY_DIR}/generated/ObsidianDragon.rc
|
||||||
@@ -386,6 +479,13 @@ if(WIN32)
|
|||||||
set(WIN_RC_FILE ${CMAKE_BINARY_DIR}/generated/ObsidianDragon.rc)
|
set(WIN_RC_FILE ${CMAKE_BINARY_DIR}/generated/ObsidianDragon.rc)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# Generate version.h from the single project(VERSION ...) declaration
|
||||||
|
configure_file(
|
||||||
|
${CMAKE_SOURCE_DIR}/src/config/version.h.in
|
||||||
|
${CMAKE_SOURCE_DIR}/src/config/version.h
|
||||||
|
@ONLY
|
||||||
|
)
|
||||||
|
|
||||||
# Generate INCBIN font embedding source with absolute paths to .ttf files
|
# Generate INCBIN font embedding source with absolute paths to .ttf files
|
||||||
configure_file(
|
configure_file(
|
||||||
${CMAKE_SOURCE_DIR}/src/embedded/embedded_fonts.cpp.in
|
${CMAKE_SOURCE_DIR}/src/embedded/embedded_fonts.cpp.in
|
||||||
@@ -393,6 +493,21 @@ configure_file(
|
|||||||
@ONLY
|
@ONLY
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# INCBIN uses .incbin assembler directives that reference font files at
|
||||||
|
# assembly time — CMake doesn't track these implicit dependencies.
|
||||||
|
# Tell CMake that the generated source depends on the actual font binaries
|
||||||
|
# so a font file change triggers recompilation.
|
||||||
|
set_source_files_properties(
|
||||||
|
${CMAKE_BINARY_DIR}/generated/embedded_fonts.cpp
|
||||||
|
PROPERTIES OBJECT_DEPENDS
|
||||||
|
"${CMAKE_SOURCE_DIR}/res/fonts/Ubuntu-R.ttf;\
|
||||||
|
${CMAKE_SOURCE_DIR}/res/fonts/Ubuntu-Light.ttf;\
|
||||||
|
${CMAKE_SOURCE_DIR}/res/fonts/Ubuntu-Medium.ttf;\
|
||||||
|
${CMAKE_SOURCE_DIR}/res/fonts/MaterialIcons-Regular.ttf;\
|
||||||
|
${CMAKE_SOURCE_DIR}/res/fonts/MaterialDesignIcons-Pickaxe-Subset.ttf;\
|
||||||
|
${CMAKE_SOURCE_DIR}/res/fonts/NotoSansCJK-Subset.ttf"
|
||||||
|
)
|
||||||
|
|
||||||
add_executable(ObsidianDragon
|
add_executable(ObsidianDragon
|
||||||
${APP_SOURCES}
|
${APP_SOURCES}
|
||||||
${CMAKE_BINARY_DIR}/generated/embedded_fonts.cpp
|
${CMAKE_BINARY_DIR}/generated/embedded_fonts.cpp
|
||||||
@@ -422,13 +537,14 @@ target_link_libraries(ObsidianDragon PRIVATE
|
|||||||
SDL3::SDL3
|
SDL3::SDL3
|
||||||
nlohmann_json::nlohmann_json
|
nlohmann_json::nlohmann_json
|
||||||
tomlplusplus::tomlplusplus
|
tomlplusplus::tomlplusplus
|
||||||
|
sqlite3_amalgamation
|
||||||
${CURL_LIBRARIES}
|
${CURL_LIBRARIES}
|
||||||
${SODIUM_LIBRARY}
|
${SODIUM_LIBRARY}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Platform-specific settings
|
# Platform-specific settings
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
target_link_libraries(ObsidianDragon PRIVATE ws2_32 winmm imm32 version setupapi dwmapi crypt32 wldap32 psapi d3d11 dxgi d3dcompiler dcomp)
|
target_link_libraries(ObsidianDragon PRIVATE ws2_32 winmm imm32 version setupapi dwmapi crypt32 wldap32 psapi iphlpapi d3d11 dxgi d3dcompiler dcomp)
|
||||||
# Hide console window in release builds
|
# Hide console window in release builds
|
||||||
if(CMAKE_BUILD_TYPE STREQUAL "Release")
|
if(CMAKE_BUILD_TYPE STREQUAL "Release")
|
||||||
set_target_properties(ObsidianDragon PROPERTIES WIN32_EXECUTABLE TRUE)
|
set_target_properties(ObsidianDragon PROPERTIES WIN32_EXECUTABLE TRUE)
|
||||||
@@ -453,7 +569,7 @@ endif()
|
|||||||
|
|
||||||
# Compile definitions
|
# Compile definitions
|
||||||
target_compile_definitions(ObsidianDragon PRIVATE
|
target_compile_definitions(ObsidianDragon PRIVATE
|
||||||
$<$<CONFIG:Debug>:DRAGONX_DEBUG>
|
DRAGONX_DEBUG
|
||||||
)
|
)
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
target_compile_definitions(ObsidianDragon PRIVATE DRAGONX_USE_DX11)
|
target_compile_definitions(ObsidianDragon PRIVATE DRAGONX_USE_DX11)
|
||||||
@@ -480,17 +596,44 @@ elseif(EXISTS ${CMAKE_SOURCE_DIR}/../SilentDragonX/res/Ubuntu-R.ttf)
|
|||||||
)
|
)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# Copy language files
|
# Copy language files at BUILD time (not just cmake configure time)
|
||||||
|
# so edits to res/lang/*.json are picked up by 'make' without re-running cmake.
|
||||||
file(GLOB LANG_FILES ${CMAKE_SOURCE_DIR}/res/lang/*.json)
|
file(GLOB LANG_FILES ${CMAKE_SOURCE_DIR}/res/lang/*.json)
|
||||||
if(LANG_FILES)
|
if(LANG_FILES)
|
||||||
|
find_program(XXD_EXECUTABLE NAMES xxd)
|
||||||
|
if(NOT XXD_EXECUTABLE)
|
||||||
|
message(WARNING "xxd not found; runtime language JSON files will be copied, but embedded build/generated/embedded/lang_*.h files will not be regenerated")
|
||||||
|
endif()
|
||||||
|
file(MAKE_DIRECTORY ${CMAKE_BINARY_DIR}/generated/embedded)
|
||||||
|
|
||||||
foreach(LANG_FILE ${LANG_FILES})
|
foreach(LANG_FILE ${LANG_FILES})
|
||||||
get_filename_component(LANG_FILENAME ${LANG_FILE} NAME)
|
get_filename_component(LANG_FILENAME ${LANG_FILE} NAME)
|
||||||
configure_file(
|
add_custom_command(
|
||||||
${LANG_FILE}
|
OUTPUT ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/res/lang/${LANG_FILENAME}
|
||||||
${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/res/lang/${LANG_FILENAME}
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||||
COPYONLY
|
${LANG_FILE}
|
||||||
|
${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/res/lang/${LANG_FILENAME}
|
||||||
|
DEPENDS ${LANG_FILE}
|
||||||
|
COMMENT "Copying ${LANG_FILENAME}"
|
||||||
)
|
)
|
||||||
|
list(APPEND LANG_OUTPUTS ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/res/lang/${LANG_FILENAME})
|
||||||
|
|
||||||
|
# Also regenerate the embedded header so the binary always has fresh translations
|
||||||
|
if(XXD_EXECUTABLE)
|
||||||
|
get_filename_component(LANG_CODE ${LANG_FILENAME} NAME_WE)
|
||||||
|
set(LANG_HEADER ${CMAKE_BINARY_DIR}/generated/embedded/lang_${LANG_CODE}.h)
|
||||||
|
add_custom_command(
|
||||||
|
OUTPUT ${LANG_HEADER}
|
||||||
|
COMMAND ${XXD_EXECUTABLE} -i "res/lang/${LANG_FILENAME}" > "${LANG_HEADER}"
|
||||||
|
DEPENDS ${LANG_FILE}
|
||||||
|
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||||
|
COMMENT "Embedding lang_${LANG_CODE}.h"
|
||||||
|
)
|
||||||
|
list(APPEND LANG_OUTPUTS ${LANG_HEADER})
|
||||||
|
endif()
|
||||||
endforeach()
|
endforeach()
|
||||||
|
add_custom_target(copy_langs ALL DEPENDS ${LANG_OUTPUTS})
|
||||||
|
add_dependencies(ObsidianDragon copy_langs)
|
||||||
message(STATUS " Language files: ${LANG_FILES}")
|
message(STATUS " Language files: ${LANG_FILES}")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
@@ -502,14 +645,39 @@ embed_resource(
|
|||||||
${CMAKE_BINARY_DIR}/generated/ui_toml_embedded.h
|
${CMAKE_BINARY_DIR}/generated/ui_toml_embedded.h
|
||||||
ui_toml
|
ui_toml
|
||||||
)
|
)
|
||||||
|
embed_resource(
|
||||||
|
${CMAKE_SOURCE_DIR}/res/default_banlist.txt
|
||||||
|
${CMAKE_BINARY_DIR}/generated/default_banlist_embedded.h
|
||||||
|
default_banlist
|
||||||
|
)
|
||||||
|
|
||||||
# Note: xmrig is embedded via build.sh (embedded_data.h) for Windows builds,
|
# Note: xmrig is embedded via build.sh (embedded_data.h) for Windows builds,
|
||||||
# following the same pattern as daemon embedding.
|
# following the same pattern as daemon embedding.
|
||||||
|
|
||||||
# Copy theme files at BUILD time (not just cmake configure time)
|
# Expand and copy theme files at BUILD time — skin files get layout sections
|
||||||
# so edits to res/themes/*.toml are picked up by 'make' without re-running cmake.
|
# from ui.toml appended automatically so users can see/edit all properties.
|
||||||
|
# Source skin files stay minimal; the merged output goes to build/bin/res/themes/.
|
||||||
|
find_package(Python3 QUIET COMPONENTS Interpreter)
|
||||||
|
if(NOT Python3_FOUND)
|
||||||
|
find_program(Python3_EXECUTABLE NAMES python3 python)
|
||||||
|
endif()
|
||||||
file(GLOB THEME_FILES ${CMAKE_SOURCE_DIR}/res/themes/*.toml)
|
file(GLOB THEME_FILES ${CMAKE_SOURCE_DIR}/res/themes/*.toml)
|
||||||
if(THEME_FILES)
|
if(THEME_FILES AND Python3_EXECUTABLE)
|
||||||
|
add_custom_command(
|
||||||
|
OUTPUT ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/res/themes/.expanded
|
||||||
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_SOURCE_DIR}/scripts/expand_themes.py
|
||||||
|
${CMAKE_SOURCE_DIR}/res/themes
|
||||||
|
${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/res/themes
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E touch ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/res/themes/.expanded
|
||||||
|
DEPENDS ${THEME_FILES} ${CMAKE_SOURCE_DIR}/scripts/expand_themes.py
|
||||||
|
COMMENT "Expanding theme files (merging layout from ui.toml)"
|
||||||
|
)
|
||||||
|
add_custom_target(copy_themes ALL DEPENDS ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/res/themes/.expanded)
|
||||||
|
add_dependencies(ObsidianDragon copy_themes)
|
||||||
|
message(STATUS " Theme files: ${THEME_FILES} (build-time expansion via Python)")
|
||||||
|
elseif(THEME_FILES)
|
||||||
|
# Fallback: plain copy if Python is not available
|
||||||
|
message(WARNING "Python3 not found; copying theme files without expand_themes.py layout merge")
|
||||||
foreach(THEME_FILE ${THEME_FILES})
|
foreach(THEME_FILE ${THEME_FILES})
|
||||||
get_filename_component(THEME_FILENAME ${THEME_FILE} NAME)
|
get_filename_component(THEME_FILENAME ${THEME_FILE} NAME)
|
||||||
add_custom_command(
|
add_custom_command(
|
||||||
@@ -524,7 +692,7 @@ if(THEME_FILES)
|
|||||||
endforeach()
|
endforeach()
|
||||||
add_custom_target(copy_themes ALL DEPENDS ${THEME_OUTPUTS})
|
add_custom_target(copy_themes ALL DEPENDS ${THEME_OUTPUTS})
|
||||||
add_dependencies(ObsidianDragon copy_themes)
|
add_dependencies(ObsidianDragon copy_themes)
|
||||||
message(STATUS " Theme files: ${THEME_FILES}")
|
message(STATUS " Theme files: ${THEME_FILES} (plain copy, Python not found)")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# Copy image files (including backgrounds/ subdirectories and logos/)
|
# Copy image files (including backgrounds/ subdirectories and logos/)
|
||||||
@@ -563,6 +731,61 @@ install(DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/res
|
|||||||
OPTIONAL
|
OPTIONAL
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Tests
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
if(BUILD_TESTING)
|
||||||
|
add_executable(ObsidianDragonTests
|
||||||
|
tests/test_phase4.cpp
|
||||||
|
src/services/network_refresh_service.cpp
|
||||||
|
src/services/refresh_scheduler.cpp
|
||||||
|
src/services/wallet_security_controller.cpp
|
||||||
|
src/services/wallet_security_workflow.cpp
|
||||||
|
src/services/wallet_security_workflow_executor.cpp
|
||||||
|
src/ui/explorer/explorer_block_cache.cpp
|
||||||
|
src/ui/windows/balance_address_list.cpp
|
||||||
|
src/ui/windows/balance_recent_tx.cpp
|
||||||
|
src/ui/windows/console_input_model.cpp
|
||||||
|
src/ui/windows/console_output_model.cpp
|
||||||
|
src/ui/windows/console_tab_helpers.cpp
|
||||||
|
src/ui/windows/mining_benchmark.cpp
|
||||||
|
src/ui/windows/mining_pool_panel.cpp
|
||||||
|
src/ui/windows/mining_tab_helpers.cpp
|
||||||
|
src/util/payment_uri.cpp
|
||||||
|
src/util/amount_format.cpp
|
||||||
|
src/data/wallet_state.cpp
|
||||||
|
src/data/transaction_history_cache.cpp
|
||||||
|
src/daemon/lifecycle_adapters.cpp
|
||||||
|
src/rpc/connection.cpp
|
||||||
|
src/resources/embedded_resources.cpp
|
||||||
|
src/util/secure_vault.cpp
|
||||||
|
src/util/platform.cpp
|
||||||
|
src/util/logger.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(ObsidianDragonTests PRIVATE
|
||||||
|
${CMAKE_SOURCE_DIR}/src
|
||||||
|
${CMAKE_SOURCE_DIR}/src/resources
|
||||||
|
${CMAKE_SOURCE_DIR}/libs
|
||||||
|
${CMAKE_BINARY_DIR}/generated
|
||||||
|
${IMGUI_DIR}
|
||||||
|
${SODIUM_INCLUDE_DIR}
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(ObsidianDragonTests PRIVATE
|
||||||
|
nlohmann_json::nlohmann_json
|
||||||
|
sqlite3_amalgamation
|
||||||
|
${SODIUM_LIBRARY}
|
||||||
|
)
|
||||||
|
|
||||||
|
if(UNIX)
|
||||||
|
target_link_libraries(ObsidianDragonTests PRIVATE ${CMAKE_DL_LIBS})
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_test(NAME ObsidianDragonPhase4Tests COMMAND ObsidianDragonTests)
|
||||||
|
endif()
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Summary
|
# Summary
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ Thank you for your interest in contributing! This guide will help you get starte
|
|||||||
1. Fork the repository
|
1. Fork the repository
|
||||||
2. Clone your fork and create a branch:
|
2. Clone your fork and create a branch:
|
||||||
```bash
|
```bash
|
||||||
git clone https://git.hush.is/<your-username>/ObsidianDragon.git
|
git clone https://git.dragonx.is/<your-username>/ObsidianDragon.git
|
||||||
cd ObsidianDragon
|
cd ObsidianDragon
|
||||||
git checkout -b my-feature
|
git checkout -b my-feature
|
||||||
```
|
```
|
||||||
@@ -67,7 +67,7 @@ Run the wallet and verify:
|
|||||||
|
|
||||||
## Reporting Issues
|
## Reporting Issues
|
||||||
|
|
||||||
- Use the [issue tracker](https://git.hush.is/dragonx/ObsidianDragon/issues)
|
- Use the [issue tracker](https://git.dragonx.is/dragonx/ObsidianDragon/issues)
|
||||||
- Include: OS, wallet version, steps to reproduce, expected vs actual behaviour
|
- Include: OS, wallet version, steps to reproduce, expected vs actual behaviour
|
||||||
- For crashes: include any terminal output
|
- For crashes: include any terminal output
|
||||||
|
|
||||||
|
|||||||
BIN
ObsidianDragon.iconset/icon_128x128.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
ObsidianDragon.iconset/icon_128x128@2x.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
ObsidianDragon.iconset/icon_16x16.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
ObsidianDragon.iconset/icon_16x16@2x.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
ObsidianDragon.iconset/icon_256x256.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
ObsidianDragon.iconset/icon_256x256@2x.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
ObsidianDragon.iconset/icon_32x32.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
ObsidianDragon.iconset/icon_32x32@2x.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
ObsidianDragon.iconset/icon_512x512.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
ObsidianDragon.iconset/icon_512x512@2x.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
67
README.md
@@ -1,6 +1,8 @@
|
|||||||
# DragonX Wallet - ImGui Edition
|
# ObsidianDragon - DragonX Wallet
|
||||||
|
|
||||||
A lightweight, portable cryptocurrency wallet for DragonX (DRGX), built with Dear ImGui.
|
A lightweight, portable full-node cryptocurrency wallet for DragonX (DRGX), built with Dear ImGui.
|
||||||
|
|
||||||
|
Current pre-release: **1.2.0-rc1**.
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||
@@ -9,10 +11,13 @@ A lightweight, portable cryptocurrency wallet for DragonX (DRGX), built with Dea
|
|||||||
|
|
||||||
- **Full Node Support**: Connects to dragonxd for complete blockchain verification
|
- **Full Node Support**: Connects to dragonxd for complete blockchain verification
|
||||||
- **Shielded Transactions**: Full z-address support with encrypted memos
|
- **Shielded Transactions**: Full z-address support with encrypted memos
|
||||||
- **Integrated Mining**: CPU mining controls with hashrate monitoring
|
- **Address Management**: Labels, icons, favorites, hidden addresses, and address-to-address transfers
|
||||||
|
- **Integrated Mining**: Solo CPU mining plus pool mining through xmrig, with idle-mining controls
|
||||||
|
- **Explorer Tools**: Block/transaction lookup and bootstrap snapshot download
|
||||||
- **Market Data**: Real-time price charts from CoinGecko
|
- **Market Data**: Real-time price charts from CoinGecko
|
||||||
- **QR Codes**: Generate and display QR codes for receiving addresses
|
- **QR Codes**: Generate and display QR codes for receiving addresses
|
||||||
- **Multi-language**: i18n support (English, Spanish, more coming)
|
- **Multi-language**: i18n support for English, German, Spanish, French, Japanese, Korean, Portuguese, Russian, and Chinese
|
||||||
|
- **CJK Fonts**: Bundled CJK subset font for translated interfaces
|
||||||
- **Lightweight**: ~5-10MB binary vs ~50MB+ for Qt version
|
- **Lightweight**: ~5-10MB binary vs ~50MB+ for Qt version
|
||||||
- **Fast Builds**: Compiles in seconds, not minutes
|
- **Fast Builds**: Compiles in seconds, not minutes
|
||||||
|
|
||||||
@@ -33,10 +38,10 @@ A lightweight, portable cryptocurrency wallet for DragonX (DRGX), built with Dea
|
|||||||
The setup script detects your OS, installs all build dependencies, and validates your environment:
|
The setup script detects your OS, installs all build dependencies, and validates your environment:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./scripts/setup.sh # Install core build deps (interactive)
|
./setup.sh # Install core build deps (interactive)
|
||||||
./scripts/setup.sh --check # Just report what's missing
|
./setup.sh --check # Just report what's missing
|
||||||
./scripts/setup.sh --all # Core + Windows/macOS cross-compile + Sapling params
|
./setup.sh --all # Core + Windows/macOS cross-compile + Sapling params
|
||||||
./scripts/setup.sh --win # Also install mingw-w64 + libsodium-win
|
./setup.sh --win # Also install mingw-w64 + libsodium-win
|
||||||
```
|
```
|
||||||
|
|
||||||
### Manual Prerequisites
|
### Manual Prerequisites
|
||||||
@@ -71,12 +76,12 @@ brew install cmake
|
|||||||
### Binaries
|
### Binaries
|
||||||
Download linux and windows binaries of latest releases and place in binary directories:
|
Download linux and windows binaries of latest releases and place in binary directories:
|
||||||
|
|
||||||
**DragonX daemon** (https://git.hush.is/dragonx/hush3):
|
**DragonX daemon** (https://git.dragonx.is/DragonX/dragonx):
|
||||||
- prebuilt-binaries/dragonxd-linux/
|
- prebuilt-binaries/dragonxd-linux/
|
||||||
- prebuilt-binaries/dragonxd-win/
|
- prebuilt-binaries/dragonxd-win/
|
||||||
- prebuilt-binaries/dragonxd-mac/
|
- prebuilt-binaries/dragonxd-mac/
|
||||||
|
|
||||||
**xmrig HAC fork** (https://git.hush.is/dragonx/xmrig-hac):
|
**xmrig HAC fork** (https://git.dragonx.is/dragonx/xmrig-hac):
|
||||||
- prebuilt-binaries/xmrig-hac/
|
- prebuilt-binaries/xmrig-hac/
|
||||||
|
|
||||||
|
|
||||||
@@ -84,7 +89,7 @@ Download linux and windows binaries of latest releases and place in binary direc
|
|||||||
|
|
||||||
```
|
```
|
||||||
### Clone repository (if not already)
|
### Clone repository (if not already)
|
||||||
git clone https://git.hush.is/dragonx/ObsidianDragon.git
|
git clone https://git.dragonx.is/dragonx/ObsidianDragon.git
|
||||||
cd ObsidianDragon/
|
cd ObsidianDragon/
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -116,7 +121,8 @@ cd ObsidianDragon/
|
|||||||
./ObsidianDragon
|
./ObsidianDragon
|
||||||
```
|
```
|
||||||
|
|
||||||
The wallet will automatically connect to the daemon using credentials from \`~/.hush/DRAGONX/DRAGONX.conf\`.
|
The wallet will automatically connect to the daemon using credentials from `~/.hush/DRAGONX/DRAGONX.conf`.
|
||||||
|
|
||||||
### Using Custom Node Binaries
|
### Using Custom Node Binaries
|
||||||
|
|
||||||
The wallet checks its **own directory first** when looking for DragonX node binaries. This means you can test new or different branch builds of `hush-arrakis-chain`/`hushd` without waiting for a new wallet release:
|
The wallet checks its **own directory first** when looking for DragonX node binaries. This means you can test new or different branch builds of `hush-arrakis-chain`/`hushd` without waiting for a new wallet release:
|
||||||
@@ -128,12 +134,13 @@ The wallet checks its **own directory first** when looking for DragonX node bina
|
|||||||
**Search order:**
|
**Search order:**
|
||||||
1. Wallet executable directory (highest priority)
|
1. Wallet executable directory (highest priority)
|
||||||
2. Embedded/extracted daemon (app data directory)
|
2. Embedded/extracted daemon (app data directory)
|
||||||
3. System-wide locations (`/usr/local/bin`, `~/hush3/src`, etc.)
|
3. System-wide locations (`/usr/local/bin`, `~/dragonx/src`, etc.)
|
||||||
|
|
||||||
This is useful for testing new branches or hotfixes to the node software before they are bundled into a wallet release.
|
This is useful for testing new branches or hotfixes to the node software before they are bundled into a wallet release.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Configuration is stored in \`~/.hush/DRAGONX/DRAGONX.conf\`:
|
Configuration is stored in `~/.hush/DRAGONX/DRAGONX.conf`:
|
||||||
|
|
||||||
```
|
```
|
||||||
rpcuser=your_rpc_user
|
rpcuser=your_rpc_user
|
||||||
@@ -148,44 +155,46 @@ ObsidianDragon/
|
|||||||
├── src/
|
├── src/
|
||||||
│ ├── main.cpp # Entry point, SDL/ImGui setup
|
│ ├── main.cpp # Entry point, SDL/ImGui setup
|
||||||
│ ├── app.cpp/h # Main application class
|
│ ├── app.cpp/h # Main application class
|
||||||
│ ├── wallet_state.h # Wallet data structures
|
│ ├── data/ # WalletState, address book, exchange info
|
||||||
│ ├── version.h # Version definitions
|
│ ├── config/ # Settings persistence and committed/generated version.h
|
||||||
│ ├── ui/
|
│ ├── ui/
|
||||||
│ │ ├── theme.cpp/h # DragonX theme
|
│ │ ├── schema/ # TOML UI schema and skin manager
|
||||||
│ │ └── windows/ # UI tabs and dialogs
|
│ │ ├── material/ # Material components, typography, layout
|
||||||
|
│ │ ├── windows/ # Tabs and dialogs
|
||||||
|
│ │ └── pages/ # Multi-page screens such as Settings
|
||||||
│ ├── rpc/
|
│ ├── rpc/
|
||||||
│ │ ├── rpc_client.cpp # JSON-RPC client
|
│ │ ├── rpc_client.cpp # JSON-RPC client
|
||||||
│ │ └── connection.cpp # Daemon connection
|
│ │ └── connection.cpp # Daemon connection
|
||||||
│ ├── config/
|
│ ├── resources/ # Embedded resource extraction
|
||||||
│ │ └── settings.cpp # Settings persistence
|
│ ├── platform/ # Windows DX11/backdrop helpers
|
||||||
│ ├── util/
|
│ ├── util/
|
||||||
│ │ ├── i18n.cpp # Internationalization
|
│ │ ├── i18n.cpp # Internationalization
|
||||||
│ │ └── ...
|
│ │ └── ...
|
||||||
│ └── daemon/
|
│ └── daemon/
|
||||||
│ └── embedded_daemon.cpp
|
│ └── embedded_daemon.cpp
|
||||||
├── res/
|
├── res/
|
||||||
│ ├── fonts/ # Ubuntu font
|
│ ├── fonts/ # Ubuntu, icon, and CJK fonts
|
||||||
│ └── lang/ # Translation files
|
│ └── lang/ # Translation files
|
||||||
├── libs/
|
├── libs/
|
||||||
│ └── qrcode/ # QR code generation
|
│ └── qrcode/ # QR code generation
|
||||||
├── CMakeLists.txt
|
├── CMakeLists.txt
|
||||||
├── build-release.sh # Build script
|
├── build.sh # Release/cross-platform build script
|
||||||
└── create-appimage.sh # AppImage packaging
|
└── scripts/create-appimage.sh # AppImage packaging
|
||||||
```
|
```
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
Fetched automatically by CMake (no manual install needed):
|
Fetched or discovered by CMake:
|
||||||
|
|
||||||
- **[SDL3](https://github.com/libsdl-org/SDL)** — Cross-platform windowing/input
|
- **[SDL3](https://github.com/libsdl-org/SDL)** — Cross-platform windowing/input
|
||||||
- **[nlohmann/json](https://github.com/nlohmann/json)** — JSON parsing
|
- **[nlohmann/json](https://github.com/nlohmann/json)** — JSON parsing
|
||||||
- **[toml++](https://github.com/marzer/tomlplusplus)** — TOML parsing (UI schema/themes)
|
- **[toml++](https://github.com/marzer/tomlplusplus)** — TOML parsing (UI schema/themes)
|
||||||
- **[libcurl](https://curl.se/libcurl/)** — HTTPS RPC transport (system on Linux, fetched on Windows)
|
- **[libcurl](https://curl.se/libcurl/)** — HTTP/HTTPS transport for daemon RPC and network calls (system on Linux/macOS, fetched on Windows)
|
||||||
|
|
||||||
Bundled in `libs/`:
|
Bundled in `libs/`:
|
||||||
|
|
||||||
- **[Dear ImGui](https://github.com/ocornut/imgui)** — Immediate mode GUI
|
- **[Dear ImGui](https://github.com/ocornut/imgui)** — Immediate mode GUI
|
||||||
- **[libsodium](https://libsodium.org)** — Cryptographic operations (fetched by `scripts/fetch-libsodium.sh`)
|
- **[libsodium](https://libsodium.org)** — Cryptographic operations (system on Linux or fetched by `scripts/fetch-libsodium.sh`)
|
||||||
- **[QR-Code-generator](https://github.com/nayuki/QR-Code-generator)** — QR code rendering
|
- **[QR-Code-generator](https://github.com/nayuki/QR-Code-generator)** — QR code rendering
|
||||||
- **[miniz](https://github.com/richgel999/miniz)** — ZIP compression
|
- **[miniz](https://github.com/richgel999/miniz)** — ZIP compression
|
||||||
- **[GLAD](https://glad.dav1d.de/)** — OpenGL loader (Linux/macOS)
|
- **[GLAD](https://glad.dav1d.de/)** — OpenGL loader (Linux/macOS)
|
||||||
@@ -202,9 +211,11 @@ Bundled in `libs/`:
|
|||||||
|
|
||||||
## Translation
|
## Translation
|
||||||
|
|
||||||
|
Current language files live in `res/lang/` as `de`, `es`, `fr`, `ja`, `ko`, `pt`, `ru`, and `zh` JSON files, with built-in English fallbacks.
|
||||||
|
|
||||||
To add a new language:
|
To add a new language:
|
||||||
|
|
||||||
1. Copy \`res/lang/es.json\` to \`res/lang/<code>.json\`
|
1. Copy `res/lang/es.json` to `res/lang/<code>.json`
|
||||||
2. Translate all strings
|
2. Translate all strings
|
||||||
3. The language will appear in Settings automatically
|
3. The language will appear in Settings automatically
|
||||||
|
|
||||||
@@ -223,4 +234,4 @@ This project is licensed under the GNU General Public License v3 (GPLv3).
|
|||||||
|
|
||||||
- Website: https://dragonx.is
|
- Website: https://dragonx.is
|
||||||
- Explorer: https://explorer.dragonx.is
|
- Explorer: https://explorer.dragonx.is
|
||||||
- Source: https://git.hush.is/dragonx/ObsidianDragon
|
- Source: https://git.dragonx.is/dragonx/ObsidianDragon
|
||||||
|
|||||||
@@ -361,7 +361,22 @@ https://www.apache.org/licenses/LICENSE-2.0
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 13. IconFontCppHeaders
|
## 13. Material Design Icons Pickaxe Subset Font
|
||||||
|
|
||||||
|
- **Location:** `res/fonts/MaterialDesignIcons-Pickaxe-Subset.ttf`
|
||||||
|
- **Source:** https://github.com/Templarian/MaterialDesign-Webfont
|
||||||
|
- **Derived from:** Pictogrammers Material Design Icons webfont (`materialdesignicons-webfont.ttf`)
|
||||||
|
- **Copyright:** Pictogrammers contributors
|
||||||
|
- **License:** Apache License 2.0
|
||||||
|
|
||||||
|
This bundled font is a local one-glyph subset containing only the MDI pickaxe
|
||||||
|
icon, remapped onto a BMP private-use codepoint for Dear ImGui compatibility.
|
||||||
|
The full text of the Apache License 2.0 is available at:
|
||||||
|
https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14. IconFontCppHeaders
|
||||||
|
|
||||||
- **Location:** `src/embedded/IconsMaterialDesign.h`
|
- **Location:** `src/embedded/IconsMaterialDesign.h`
|
||||||
- **Source:** https://github.com/juliettef/IconFontCppHeaders
|
- **Source:** https://github.com/juliettef/IconFontCppHeaders
|
||||||
@@ -390,7 +405,7 @@ freely, subject to the following restrictions:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 14. Ubuntu Font Family
|
## 15. Ubuntu Font Family
|
||||||
|
|
||||||
- **Location:** `res/fonts/Ubuntu-Light.ttf`, `Ubuntu-Medium.ttf`, `Ubuntu-R.ttf`
|
- **Location:** `res/fonts/Ubuntu-Light.ttf`, `Ubuntu-Medium.ttf`, `Ubuntu-R.ttf`
|
||||||
- **Source:** https://design.ubuntu.com/font
|
- **Source:** https://design.ubuntu.com/font
|
||||||
|
|||||||
276
build.sh
@@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# ./build.sh # Dev build (Linux, debug-friendly)
|
# ./build.sh # Dev build (Linux, debug-friendly)
|
||||||
# ./build.sh --linux-release # Linux release + AppImage
|
# ./build.sh --linux-release # Linux release (zip + AppImage)
|
||||||
# ./build.sh --win-release # Windows cross-compile (mingw-w64)
|
# ./build.sh --win-release # Windows cross-compile (mingw-w64)
|
||||||
# ./build.sh --mac-release # macOS .app bundle + DMG
|
# ./build.sh --mac-release # macOS .app bundle + DMG
|
||||||
# ./build.sh --linux-release --win-release # Multiple targets
|
# ./build.sh --linux-release --win-release # Multiple targets
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
set -e
|
set -e
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
VERSION="1.0.0"
|
VERSION="1.2.0-rc1"
|
||||||
|
|
||||||
# ── Colours ──────────────────────────────────────────────────────────────────
|
# ── Colours ──────────────────────────────────────────────────────────────────
|
||||||
RED='\033[0;31m'
|
RED='\033[0;31m'
|
||||||
@@ -51,7 +51,7 @@ DragonX Wallet — Unified Build Script
|
|||||||
Usage: $0 [options]
|
Usage: $0 [options]
|
||||||
|
|
||||||
Targets (at least one required, or none for dev build):
|
Targets (at least one required, or none for dev build):
|
||||||
--linux-release Linux release build + AppImage -> release/linux/
|
--linux-release Linux release (zip + AppImage) -> release/linux/
|
||||||
--win-release Windows cross-compile (mingw-w64) -> release/windows/
|
--win-release Windows cross-compile (mingw-w64) -> release/windows/
|
||||||
--mac-release macOS .app bundle + DMG -> release/mac/
|
--mac-release macOS .app bundle + DMG -> release/mac/
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ Cross-compiling from Linux:
|
|||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
$0 # Quick dev build (Linux)
|
$0 # Quick dev build (Linux)
|
||||||
$0 --linux-release # Linux release + AppImage
|
$0 --linux-release # Linux release (zip + AppImage)
|
||||||
$0 --win-release # Windows cross-compile
|
$0 --win-release # Windows cross-compile
|
||||||
$0 --mac-release # macOS bundle + DMG (native or osxcross)
|
$0 --mac-release # macOS bundle + DMG (native or osxcross)
|
||||||
$0 --clean --linux-release --win-release # Clean + both
|
$0 --clean --linux-release --win-release # Clean + both
|
||||||
@@ -122,10 +122,10 @@ find_sapling_params() {
|
|||||||
|
|
||||||
find_asmap() {
|
find_asmap() {
|
||||||
local paths=(
|
local paths=(
|
||||||
"$SCRIPT_DIR/external/hush3/asmap.dat"
|
"$SCRIPT_DIR/external/dragonx/asmap.dat"
|
||||||
"$SCRIPT_DIR/external/hush3/contrib/asmap/asmap.dat"
|
"$SCRIPT_DIR/external/dragonx/contrib/asmap/asmap.dat"
|
||||||
"$HOME/hush3/asmap.dat"
|
"$HOME/dragonx/asmap.dat"
|
||||||
"$HOME/hush3/contrib/asmap/asmap.dat"
|
"$HOME/dragonx/contrib/asmap/asmap.dat"
|
||||||
"$SCRIPT_DIR/../asmap.dat"
|
"$SCRIPT_DIR/../asmap.dat"
|
||||||
"$SCRIPT_DIR/asmap.dat"
|
"$SCRIPT_DIR/asmap.dat"
|
||||||
"$SCRIPT_DIR/../SilentDragonX/asmap.dat"
|
"$SCRIPT_DIR/../SilentDragonX/asmap.dat"
|
||||||
@@ -147,37 +147,37 @@ bundle_linux_daemon() {
|
|||||||
local dest="$1"
|
local dest="$1"
|
||||||
local found=0
|
local found=0
|
||||||
|
|
||||||
local launcher_paths=(
|
local daemon_paths=(
|
||||||
"$SCRIPT_DIR/prebuilt-binaries/dragonxd-linux/hush-arrakis-chain"
|
"$SCRIPT_DIR/prebuilt-binaries/dragonxd-linux/dragonxd"
|
||||||
"$SCRIPT_DIR/../hush-arrakis-chain"
|
"$SCRIPT_DIR/../dragonxd"
|
||||||
"$SCRIPT_DIR/external/hush3/src/hush-arrakis-chain"
|
"$SCRIPT_DIR/external/dragonx/src/dragonxd"
|
||||||
"$HOME/hush3/src/hush-arrakis-chain"
|
"$HOME/dragonx/src/dragonxd"
|
||||||
)
|
)
|
||||||
for p in "${launcher_paths[@]}"; do
|
for p in "${daemon_paths[@]}"; do
|
||||||
if [[ -f "$p" ]]; then
|
if [[ -f "$p" ]]; then
|
||||||
cp "$p" "$dest/hush-arrakis-chain"; chmod +x "$dest/hush-arrakis-chain"
|
cp "$p" "$dest/dragonxd"; chmod +x "$dest/dragonxd"
|
||||||
info " Bundled hush-arrakis-chain"; found=1; break
|
info " Bundled dragonxd"; found=1; break
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
local hushd_paths=(
|
local cli_paths=(
|
||||||
"$SCRIPT_DIR/prebuilt-binaries/dragonxd-linux/hushd"
|
"$SCRIPT_DIR/prebuilt-binaries/dragonxd-linux/dragonx-cli"
|
||||||
"$SCRIPT_DIR/../hushd"
|
"$SCRIPT_DIR/../dragonx-cli"
|
||||||
"$SCRIPT_DIR/external/hush3/src/hushd"
|
"$SCRIPT_DIR/external/dragonx/src/dragonx-cli"
|
||||||
"$HOME/hush3/src/hushd"
|
"$HOME/dragonx/src/dragonx-cli"
|
||||||
)
|
)
|
||||||
for p in "${hushd_paths[@]}"; do
|
for p in "${cli_paths[@]}"; do
|
||||||
if [[ -f "$p" ]]; then
|
if [[ -f "$p" ]]; then
|
||||||
cp "$p" "$dest/hushd"; chmod +x "$dest/hushd"
|
cp "$p" "$dest/dragonx-cli"; chmod +x "$dest/dragonx-cli"
|
||||||
info " Bundled hushd"; break
|
info " Bundled dragonx-cli"; break
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
local dragonxd_paths=(
|
local dragonxd_paths=(
|
||||||
"$SCRIPT_DIR/prebuilt-binaries/dragonxd-linux/dragonxd"
|
"$SCRIPT_DIR/prebuilt-binaries/dragonxd-linux/dragonxd"
|
||||||
"$SCRIPT_DIR/../dragonxd"
|
"$SCRIPT_DIR/../dragonxd"
|
||||||
"$SCRIPT_DIR/external/hush3/src/dragonxd"
|
"$SCRIPT_DIR/external/dragonx/src/dragonxd"
|
||||||
"$HOME/hush3/src/dragonxd"
|
"$HOME/dragonx/src/dragonxd"
|
||||||
)
|
)
|
||||||
for p in "${dragonxd_paths[@]}"; do
|
for p in "${dragonxd_paths[@]}"; do
|
||||||
if [[ -f "$p" ]]; then
|
if [[ -f "$p" ]]; then
|
||||||
@@ -197,7 +197,14 @@ bundle_linux_daemon() {
|
|||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
build_dev() {
|
build_dev() {
|
||||||
header "Dev Build ($(uname -s) / $BUILD_TYPE)"
|
header "Dev Build ($(uname -s) / $BUILD_TYPE)"
|
||||||
local bd="$SCRIPT_DIR/build/linux"
|
|
||||||
|
# Use platform-appropriate build directory
|
||||||
|
if [[ "$(uname -s)" == "Darwin" ]]; then
|
||||||
|
local bd="$SCRIPT_DIR/build/mac"
|
||||||
|
export MACOSX_DEPLOYMENT_TARGET="11.0"
|
||||||
|
else
|
||||||
|
local bd="$SCRIPT_DIR/build/linux"
|
||||||
|
fi
|
||||||
|
|
||||||
if $CLEAN; then
|
if $CLEAN; then
|
||||||
info "Cleaning $bd ..."; rm -rf "$bd"
|
info "Cleaning $bd ..."; rm -rf "$bd"
|
||||||
@@ -218,7 +225,7 @@ build_dev() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
# RELEASE: LINUX — build + strip + bundle daemon + AppImage
|
# RELEASE: LINUX — build + strip + bundle daemon + zip + AppImage
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
build_release_linux() {
|
build_release_linux() {
|
||||||
header "Release: Linux x86_64"
|
header "Release: Linux x86_64"
|
||||||
@@ -249,18 +256,41 @@ build_release_linux() {
|
|||||||
# ── Bundle daemon ────────────────────────────────────────────────────────
|
# ── Bundle daemon ────────────────────────────────────────────────────────
|
||||||
bundle_linux_daemon "bin" || warn "Daemon not bundled — wallet-only build"
|
bundle_linux_daemon "bin" || warn "Daemon not bundled — wallet-only build"
|
||||||
|
|
||||||
|
# ── Bundle Sapling params ────────────────────────────────────────────────
|
||||||
|
SAPLING_SPEND="" SAPLING_OUTPUT=""
|
||||||
|
find_sapling_params && {
|
||||||
|
cp -f "$SAPLING_SPEND" "bin/sapling-spend.params"
|
||||||
|
cp -f "$SAPLING_OUTPUT" "bin/sapling-output.params"
|
||||||
|
info "Bundled Sapling params"
|
||||||
|
} || warn "Sapling params not found — not bundled"
|
||||||
|
|
||||||
# ── Package: release/linux/ ──────────────────────────────────────────────
|
# ── Package: release/linux/ ──────────────────────────────────────────────
|
||||||
rm -rf "$out"
|
rm -rf "$out"
|
||||||
mkdir -p "$out"
|
mkdir -p "$out"
|
||||||
|
|
||||||
cp bin/ObsidianDragon "$out/"
|
local DIST="ObsidianDragon-${VERSION}-Linux-x64"
|
||||||
[[ -f bin/hush-arrakis-chain ]] && cp bin/hush-arrakis-chain "$out/"
|
local dist_dir="$out/$DIST"
|
||||||
[[ -f bin/hushd ]] && cp bin/hushd "$out/"
|
mkdir -p "$dist_dir"
|
||||||
[[ -f bin/dragonxd ]] && cp bin/dragonxd "$out/"
|
|
||||||
[[ -f bin/asmap.dat ]] && cp bin/asmap.dat "$out/"
|
|
||||||
cp -r bin/res "$out/" 2>/dev/null || true
|
|
||||||
|
|
||||||
# ── AppImage ─────────────────────────────────────────────────────────────
|
cp bin/ObsidianDragon "$dist_dir/"
|
||||||
|
[[ -f bin/dragonxd ]] && cp bin/dragonxd "$dist_dir/"
|
||||||
|
[[ -f bin/dragonx-cli ]] && cp bin/dragonx-cli "$dist_dir/"
|
||||||
|
[[ -f bin/asmap.dat ]] && cp bin/asmap.dat "$dist_dir/"
|
||||||
|
[[ -f bin/sapling-spend.params ]] && cp bin/sapling-spend.params "$dist_dir/"
|
||||||
|
[[ -f bin/sapling-output.params ]] && cp bin/sapling-output.params "$dist_dir/"
|
||||||
|
# Bundle xmrig for mining support
|
||||||
|
local XMRIG_LINUX="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac/xmrig"
|
||||||
|
[[ -f "$XMRIG_LINUX" ]] && { cp "$XMRIG_LINUX" "$dist_dir/"; chmod +x "$dist_dir/xmrig"; info "Bundled xmrig"; } || warn "xmrig not found — mining unavailable in zip"
|
||||||
|
cp -r bin/res "$dist_dir/" 2>/dev/null || true
|
||||||
|
|
||||||
|
# ── Zip ──────────────────────────────────────────────────────────────────
|
||||||
|
if command -v zip &>/dev/null; then
|
||||||
|
(cd "$out" && zip -r "$DIST.zip" "$DIST")
|
||||||
|
info "Zip: $out/$DIST.zip ($(du -h "$out/$DIST.zip" | cut -f1))"
|
||||||
|
fi
|
||||||
|
rm -rf "$dist_dir"
|
||||||
|
|
||||||
|
# ── AppImage (single-file) ───────────────────────────────────────────────
|
||||||
info "Creating AppImage ..."
|
info "Creating AppImage ..."
|
||||||
local APPDIR="$bd/AppDir"
|
local APPDIR="$bd/AppDir"
|
||||||
rm -rf "$APPDIR"
|
rm -rf "$APPDIR"
|
||||||
@@ -272,11 +302,16 @@ build_release_linux() {
|
|||||||
cp bin/ObsidianDragon "$APPDIR/usr/bin/"
|
cp bin/ObsidianDragon "$APPDIR/usr/bin/"
|
||||||
cp -r bin/res/* "$APPDIR/usr/share/ObsidianDragon/res/" 2>/dev/null || true
|
cp -r bin/res/* "$APPDIR/usr/share/ObsidianDragon/res/" 2>/dev/null || true
|
||||||
|
|
||||||
# Daemon inside AppImage
|
[[ -f bin/dragonxd ]] && cp bin/dragonxd "$APPDIR/usr/bin/"
|
||||||
[[ -f bin/hush-arrakis-chain ]] && cp bin/hush-arrakis-chain "$APPDIR/usr/bin/"
|
[[ -f bin/dragonx-cli ]] && cp bin/dragonx-cli "$APPDIR/usr/bin/"
|
||||||
[[ -f bin/hushd ]] && cp bin/hushd "$APPDIR/usr/bin/"
|
# Daemon data files must be alongside the daemon binary (usr/bin/)
|
||||||
[[ -f bin/dragonxd ]] && cp bin/dragonxd "$APPDIR/usr/bin/"
|
# because dragonxd searches relative to its own directory.
|
||||||
[[ -f bin/asmap.dat ]] && cp bin/asmap.dat "$APPDIR/usr/share/ObsidianDragon/"
|
[[ -f bin/asmap.dat ]] && cp bin/asmap.dat "$APPDIR/usr/bin/"
|
||||||
|
[[ -f bin/sapling-spend.params ]] && cp bin/sapling-spend.params "$APPDIR/usr/bin/"
|
||||||
|
[[ -f bin/sapling-output.params ]] && cp bin/sapling-output.params "$APPDIR/usr/bin/"
|
||||||
|
# Bundle xmrig for mining support
|
||||||
|
local XMRIG_LINUX_AI="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac/xmrig"
|
||||||
|
[[ -f "$XMRIG_LINUX_AI" ]] && { cp "$XMRIG_LINUX_AI" "$APPDIR/usr/bin/"; chmod +x "$APPDIR/usr/bin/xmrig"; }
|
||||||
|
|
||||||
# Desktop entry
|
# Desktop entry
|
||||||
cat > "$APPDIR/usr/share/applications/ObsidianDragon.desktop" <<'DESK'
|
cat > "$APPDIR/usr/share/applications/ObsidianDragon.desktop" <<'DESK'
|
||||||
@@ -350,19 +385,11 @@ APPRUN
|
|||||||
|
|
||||||
local ARCH
|
local ARCH
|
||||||
ARCH=$(uname -m)
|
ARCH=$(uname -m)
|
||||||
local IMG_NAME="DragonX_Wallet-${VERSION}-${ARCH}.AppImage"
|
|
||||||
cd "$bd"
|
cd "$bd"
|
||||||
ARCH="$ARCH" "$APPIMAGETOOL" "$APPDIR" "$IMG_NAME" 2>/dev/null && {
|
ARCH="$ARCH" "$APPIMAGETOOL" "$APPDIR" "ObsidianDragon-${VERSION}-${ARCH}.AppImage" 2>/dev/null && {
|
||||||
cp "$IMG_NAME" "$out/"
|
cp "ObsidianDragon-${VERSION}-${ARCH}.AppImage" "$out/ObsidianDragon-${VERSION}.AppImage"
|
||||||
info "AppImage: $out/$IMG_NAME ($(du -h "$IMG_NAME" | cut -f1))"
|
info "AppImage: $out/ObsidianDragon-${VERSION}.AppImage ($(du -h "$out/ObsidianDragon-${VERSION}.AppImage" | cut -f1))"
|
||||||
} || warn "AppImage creation failed (appimagetool issue) — raw binary still in release/linux/"
|
} || warn "AppImage creation failed — binaries zip still in release/linux/"
|
||||||
|
|
||||||
# Clean up: keep only AppImage + raw binary in release/linux/
|
|
||||||
if ls "$out"/*.AppImage 1>/dev/null 2>&1; then
|
|
||||||
# AppImage succeeded — remove everything except AppImage and the binary
|
|
||||||
find "$out" -maxdepth 1 -type f ! -name '*.AppImage' ! -name 'ObsidianDragon' -delete
|
|
||||||
rm -rf "$out/res" 2>/dev/null
|
|
||||||
fi
|
|
||||||
|
|
||||||
info "Linux release artifacts: $out/"
|
info "Linux release artifacts: $out/"
|
||||||
ls -lh "$out/"
|
ls -lh "$out/"
|
||||||
@@ -401,6 +428,22 @@ build_release_win() {
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# ── Patch libwinpthread + libpthread to remove VERSIONINFO resources ────
|
||||||
|
# mingw-w64's libwinpthread.a and libpthread.a each ship a version.o
|
||||||
|
# with their own VERSIONINFO ("POSIX WinThreads for Windows") that
|
||||||
|
# collides with ours during .rsrc merge, causing Task Manager to show
|
||||||
|
# the wrong process description.
|
||||||
|
local PATCHED_LIB_DIR="$bd/patched-lib"
|
||||||
|
mkdir -p "$PATCHED_LIB_DIR"
|
||||||
|
for plib in libwinpthread.a libpthread.a; do
|
||||||
|
local SYS_LIB="/usr/x86_64-w64-mingw32/lib/$plib"
|
||||||
|
if [[ -f "$SYS_LIB" ]]; then
|
||||||
|
cp -f "$SYS_LIB" "$PATCHED_LIB_DIR/$plib"
|
||||||
|
x86_64-w64-mingw32-ar d "$PATCHED_LIB_DIR/$plib" version.o 2>/dev/null || true
|
||||||
|
info "Patched $plib (removed version.o VERSIONINFO resource)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
# ── Toolchain file ───────────────────────────────────────────────────────
|
# ── Toolchain file ───────────────────────────────────────────────────────
|
||||||
cat > "$bd/mingw-toolchain.cmake" <<TOOLCHAIN
|
cat > "$bd/mingw-toolchain.cmake" <<TOOLCHAIN
|
||||||
set(CMAKE_SYSTEM_NAME Windows)
|
set(CMAKE_SYSTEM_NAME Windows)
|
||||||
@@ -412,7 +455,7 @@ set(CMAKE_FIND_ROOT_PATH /usr/x86_64-w64-mingw32)
|
|||||||
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
||||||
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
|
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
|
||||||
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
|
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
|
||||||
set(CMAKE_EXE_LINKER_FLAGS "-static -static-libgcc -static-libstdc++ -Wl,-Bstatic,--whole-archive -lwinpthread -Wl,--no-whole-archive")
|
set(CMAKE_EXE_LINKER_FLAGS "-static -static-libgcc -static-libstdc++ -Wl,-Bstatic,--whole-archive -L$PATCHED_LIB_DIR -lwinpthread -Wl,--no-whole-archive")
|
||||||
set(CMAKE_CXX_FLAGS "\${CMAKE_CXX_FLAGS} -static")
|
set(CMAKE_CXX_FLAGS "\${CMAKE_CXX_FLAGS} -static")
|
||||||
set(CMAKE_C_FLAGS "\${CMAKE_C_FLAGS} -static")
|
set(CMAKE_C_FLAGS "\${CMAKE_C_FLAGS} -static")
|
||||||
set(CMAKE_FIND_LIBRARY_SUFFIXES ".a")
|
set(CMAKE_FIND_LIBRARY_SUFFIXES ".a")
|
||||||
@@ -454,10 +497,10 @@ HDR
|
|||||||
|
|
||||||
# ── Daemon binaries ──────────────────────────────────────────────
|
# ── Daemon binaries ──────────────────────────────────────────────
|
||||||
local DD="$SCRIPT_DIR/prebuilt-binaries/dragonxd-win"
|
local DD="$SCRIPT_DIR/prebuilt-binaries/dragonxd-win"
|
||||||
if [[ -d "$DD" && -f "$DD/hushd.exe" ]]; then
|
if [[ -d "$DD" && -f "$DD/dragonxd.exe" ]]; then
|
||||||
info "Embedding daemon binaries ..."
|
info "Embedding daemon binaries ..."
|
||||||
echo -e "\n#define HAS_EMBEDDED_DAEMON 1\n" >> "$GEN/embedded_data.h"
|
echo -e "\n#define HAS_EMBEDDED_DAEMON 1\n" >> "$GEN/embedded_data.h"
|
||||||
for f in hushd.exe hush-cli.exe hush-tx.exe dragonxd.bat dragonx-cli.bat; do
|
for f in dragonxd.exe dragonx-cli.exe dragonx-tx.exe; do
|
||||||
local sym=$(echo "$f" | sed 's/[^a-zA-Z0-9]/_/g')
|
local sym=$(echo "$f" | sed 's/[^a-zA-Z0-9]/_/g')
|
||||||
if [[ -f "$DD/$f" ]]; then
|
if [[ -f "$DD/$f" ]]; then
|
||||||
cp -f "$DD/$f" "$RES/$f"
|
cp -f "$DD/$f" "$RES/$f"
|
||||||
@@ -517,9 +560,13 @@ HDR
|
|||||||
echo "};" >> "$GEN/embedded_data.h"
|
echo "};" >> "$GEN/embedded_data.h"
|
||||||
|
|
||||||
# ── Overlay themes ───────────────────────────────────────────────
|
# ── Overlay themes ───────────────────────────────────────────────
|
||||||
|
# Expand skin files with layout sections from ui.toml before embedding
|
||||||
echo -e "\n// ---- Bundled overlay themes ----" >> "$GEN/embedded_data.h"
|
echo -e "\n// ---- Bundled overlay themes ----" >> "$GEN/embedded_data.h"
|
||||||
|
local THEME_STAGE_DIR="$bd/_expanded_themes"
|
||||||
|
mkdir -p "$THEME_STAGE_DIR"
|
||||||
|
python3 "$SCRIPT_DIR/scripts/expand_themes.py" "$SCRIPT_DIR/res/themes" "$THEME_STAGE_DIR"
|
||||||
local THEME_TABLE="" THEME_COUNT=0
|
local THEME_TABLE="" THEME_COUNT=0
|
||||||
for tf in "$SCRIPT_DIR/res/themes"/*.toml; do
|
for tf in "$THEME_STAGE_DIR"/*.toml; do
|
||||||
local tbn=$(basename "$tf")
|
local tbn=$(basename "$tf")
|
||||||
[[ "$tbn" == "ui.toml" ]] && continue
|
[[ "$tbn" == "ui.toml" ]] && continue
|
||||||
local tsym=$(echo "$tbn" | sed 's/[^a-zA-Z0-9]/_/g')
|
local tsym=$(echo "$tbn" | sed 's/[^a-zA-Z0-9]/_/g')
|
||||||
@@ -564,41 +611,39 @@ HDR
|
|||||||
rm -rf "$out"
|
rm -rf "$out"
|
||||||
mkdir -p "$out"
|
mkdir -p "$out"
|
||||||
|
|
||||||
local DIST="DragonX-Wallet-Windows-x64"
|
local DIST="ObsidianDragon-${VERSION}-Windows-x64"
|
||||||
local dist_dir="$out/$DIST"
|
local dist_dir="$out/$DIST"
|
||||||
mkdir -p "$dist_dir"
|
mkdir -p "$dist_dir"
|
||||||
cp bin/ObsidianDragon.exe "$dist_dir/"
|
cp bin/ObsidianDragon.exe "$dist_dir/"
|
||||||
|
|
||||||
local DD="$SCRIPT_DIR/prebuilt-binaries/dragonxd-win"
|
local DD="$SCRIPT_DIR/prebuilt-binaries/dragonxd-win"
|
||||||
for f in dragonxd.bat dragonx-cli.bat hushd.exe hush-cli.exe hush-tx.exe; do
|
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
|
||||||
|
|
||||||
cat > "$dist_dir/README.txt" <<'README'
|
# Bundle Sapling params + asmap for the zip distribution
|
||||||
DragonX Wallet - Windows Edition
|
# (The single-file exe has these embedded via INCBIN, but the zip
|
||||||
================================
|
# needs them on disk so the daemon can find them in its work dir.)
|
||||||
|
for f in sapling-spend.params sapling-output.params asmap.dat; do
|
||||||
|
[[ -f "$DD/$f" ]] && cp "$DD/$f" "$dist_dir/"
|
||||||
|
done
|
||||||
|
|
||||||
SINGLE-FILE DISTRIBUTION
|
# Bundle xmrig for mining support
|
||||||
========================
|
local XMRIG_WIN="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac/xmrig.exe"
|
||||||
This wallet is a true single-file executable with all resources embedded.
|
[[ -f "$XMRIG_WIN" ]] && { cp "$XMRIG_WIN" "$dist_dir/"; info "Bundled xmrig.exe"; } || warn "xmrig.exe not found — mining unavailable in zip"
|
||||||
Just run ObsidianDragon.exe — no additional files needed!
|
|
||||||
|
|
||||||
On first run, the wallet will automatically extract:
|
cp -r bin/res "$dist_dir/" 2>/dev/null || true
|
||||||
- Sapling parameters to %APPDATA%\ZcashParams\
|
|
||||||
- asmap.dat to %APPDATA%\Hush\DRAGONX\
|
|
||||||
|
|
||||||
For support: https://git.hush.is/hush/ObsidianDragon
|
# ── Single-file exe (all resources embedded) ────────────────────────────
|
||||||
README
|
cp bin/ObsidianDragon.exe "$out/ObsidianDragon-${VERSION}.exe"
|
||||||
|
info "Single-file exe: $out/ObsidianDragon-${VERSION}.exe ($(du -h "$out/ObsidianDragon-${VERSION}.exe" | cut -f1))"
|
||||||
# Copy single-file exe to release dir
|
|
||||||
cp bin/ObsidianDragon.exe "$out/"
|
|
||||||
|
|
||||||
|
# ── Zip ──────────────────────────────────────────────────────────────────
|
||||||
if command -v zip &>/dev/null; then
|
if command -v zip &>/dev/null; then
|
||||||
(cd "$out" && zip -r "$DIST.zip" "$DIST")
|
(cd "$out" && zip -r "$DIST.zip" "$DIST")
|
||||||
info "Zip: $out/$DIST.zip ($(du -h "$out/$DIST.zip" | cut -f1))"
|
info "Zip: $out/$DIST.zip ($(du -h "$out/$DIST.zip" | cut -f1))"
|
||||||
# Clean up: keep .zip + single-file exe, remove loose directory
|
|
||||||
rm -rf "$dist_dir"
|
|
||||||
fi
|
fi
|
||||||
|
rm -rf "$dist_dir"
|
||||||
|
|
||||||
info "Windows release artifacts: $out/"
|
info "Windows release artifacts: $out/"
|
||||||
ls -lh "$out/"
|
ls -lh "$out/"
|
||||||
@@ -694,7 +739,9 @@ 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
|
||||||
MAC_ARCH=$(uname -m)
|
# Native macOS: build universal binary (arm64 + x86_64)
|
||||||
|
MAC_ARCH="universal"
|
||||||
|
export MACOSX_DEPLOYMENT_TARGET="11.0"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
header "Release: macOS ($MAC_ARCH$(${IS_CROSS} && echo ' — cross-compile'))"
|
header "Release: macOS ($MAC_ARCH$(${IS_CROSS} && echo ' — cross-compile'))"
|
||||||
@@ -773,12 +820,31 @@ TOOLCHAIN
|
|||||||
-DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 \
|
-DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 \
|
||||||
${COMPILER_RT:+-DOSXCROSS_COMPILER_RT="$COMPILER_RT"}
|
${COMPILER_RT:+-DOSXCROSS_COMPILER_RT="$COMPILER_RT"}
|
||||||
else
|
else
|
||||||
info "Configuring (native) ..."
|
# Build libsodium as universal if needed
|
||||||
|
local need_sodium=false
|
||||||
|
if [[ ! -f "$SCRIPT_DIR/libs/libsodium/lib/libsodium.a" ]] && \
|
||||||
|
[[ ! -f "$SCRIPT_DIR/libs/libsodium-mac/lib/libsodium.a" ]]; then
|
||||||
|
need_sodium=true
|
||||||
|
elif [[ -f "$SCRIPT_DIR/libs/libsodium/lib/libsodium.a" ]]; then
|
||||||
|
# Rebuild if existing lib is not universal (single-arch won't link)
|
||||||
|
if ! lipo -info "$SCRIPT_DIR/libs/libsodium/lib/libsodium.a" 2>/dev/null | grep -q "arm64.*x86_64\|x86_64.*arm64"; then
|
||||||
|
info "Existing libsodium is not universal — rebuilding ..."
|
||||||
|
rm -rf "$SCRIPT_DIR/libs/libsodium"
|
||||||
|
need_sodium=true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if $need_sodium; then
|
||||||
|
info "Building libsodium (universal) ..."
|
||||||
|
"$SCRIPT_DIR/scripts/fetch-libsodium.sh"
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "Configuring (native universal arm64+x86_64) ..."
|
||||||
cmake "$SCRIPT_DIR" \
|
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"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
info "Building with $JOBS jobs ..."
|
info "Building with $JOBS jobs ..."
|
||||||
@@ -798,6 +864,11 @@ TOOLCHAIN
|
|||||||
else
|
else
|
||||||
info "Stripping ..."
|
info "Stripping ..."
|
||||||
strip bin/ObsidianDragon
|
strip bin/ObsidianDragon
|
||||||
|
# Verify universal binary
|
||||||
|
if command -v lipo &>/dev/null; then
|
||||||
|
info "Architecture info:"
|
||||||
|
lipo -info bin/ObsidianDragon
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
info "Binary: $(du -h bin/ObsidianDragon | cut -f1)"
|
info "Binary: $(du -h bin/ObsidianDragon | cut -f1)"
|
||||||
|
|
||||||
@@ -823,31 +894,47 @@ TOOLCHAIN
|
|||||||
# Daemon binaries (macOS native, from dragonxd-mac/)
|
# Daemon binaries (macOS native, from dragonxd-mac/)
|
||||||
local daemon_dir="$SCRIPT_DIR/prebuilt-binaries/dragonxd-mac"
|
local daemon_dir="$SCRIPT_DIR/prebuilt-binaries/dragonxd-mac"
|
||||||
if [[ -d "$daemon_dir" ]]; then
|
if [[ -d "$daemon_dir" ]]; then
|
||||||
for f in hush-arrakis-chain hushd hush-cli hush-tx dragonxd; do
|
for f in dragonxd dragonx-cli dragonx-tx; do
|
||||||
[[ -f "$daemon_dir/$f" ]] && { cp "$daemon_dir/$f" "$MACOS/"; chmod +x "$MACOS/$f"; info " Bundled $f"; }
|
[[ -f "$daemon_dir/$f" ]] && { cp "$daemon_dir/$f" "$MACOS/"; chmod +x "$MACOS/$f"; info " Bundled $f"; }
|
||||||
done
|
done
|
||||||
|
for f in sapling-spend.params sapling-output.params; do
|
||||||
|
[[ -f "$daemon_dir/$f" ]] && { cp "$daemon_dir/$f" "$MACOS/"; info " Bundled $f"; }
|
||||||
|
done
|
||||||
elif ! $IS_CROSS; then
|
elif ! $IS_CROSS; then
|
||||||
# Native macOS: try standard paths
|
# Native macOS: try standard paths
|
||||||
local launcher_paths=(
|
local daemon_paths=(
|
||||||
"$SCRIPT_DIR/../hush-arrakis-chain"
|
"$SCRIPT_DIR/../dragonxd"
|
||||||
"$HOME/hush3/src/hush-arrakis-chain"
|
"$HOME/dragonx/src/dragonxd"
|
||||||
)
|
)
|
||||||
for p in "${launcher_paths[@]}"; do
|
for p in "${daemon_paths[@]}"; do
|
||||||
[[ -f "$p" ]] && { cp "$p" "$MACOS/hush-arrakis-chain"; chmod +x "$MACOS/hush-arrakis-chain"; info " Bundled hush-arrakis-chain"; break; }
|
[[ -f "$p" ]] && { cp "$p" "$MACOS/dragonxd"; chmod +x "$MACOS/dragonxd"; info " Bundled dragonxd"; break; }
|
||||||
done
|
done
|
||||||
local hushd_paths=(
|
local cli_paths=(
|
||||||
"$SCRIPT_DIR/../hushd"
|
"$SCRIPT_DIR/../dragonx-cli"
|
||||||
"$HOME/hush3/src/hushd"
|
"$HOME/dragonx/src/dragonx-cli"
|
||||||
)
|
)
|
||||||
for p in "${hushd_paths[@]}"; do
|
for p in "${cli_paths[@]}"; do
|
||||||
[[ -f "$p" ]] && { cp "$p" "$MACOS/hushd"; chmod +x "$MACOS/hushd"; info " Bundled hushd"; break; }
|
[[ -f "$p" ]] && { cp "$p" "$MACOS/dragonx-cli"; chmod +x "$MACOS/dragonx-cli"; info " Bundled dragonx-cli"; break; }
|
||||||
done
|
done
|
||||||
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
|
||||||
|
|
||||||
# asmap.dat
|
# xmrig binary (from prebuilt-binaries/xmrig-hac/)
|
||||||
find_asmap 2>/dev/null && cp "$ASMAP_DAT" "$RESOURCES/asmap.dat" && info " Bundled asmap.dat"
|
local XMRIG_MAC="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac/xmrig"
|
||||||
|
if [[ -f "$XMRIG_MAC" ]]; then
|
||||||
|
cp "$XMRIG_MAC" "$MACOS/xmrig"
|
||||||
|
chmod +x "$MACOS/xmrig"
|
||||||
|
info " Bundled xmrig"
|
||||||
|
else
|
||||||
|
warn "xmrig not found — mining unavailable in .app"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# asmap.dat — placed in MacOS/ so the daemon finds it next to its binary
|
||||||
|
find_asmap 2>/dev/null && {
|
||||||
|
cp "$ASMAP_DAT" "$MACOS/asmap.dat"
|
||||||
|
info " Bundled asmap.dat"
|
||||||
|
}
|
||||||
|
|
||||||
# Bundle SDL3 dylib
|
# Bundle SDL3 dylib
|
||||||
local sdl_dylib=""
|
local sdl_dylib=""
|
||||||
@@ -965,6 +1052,13 @@ PLIST
|
|||||||
|
|
||||||
info ".app bundle created: $APP"
|
info ".app bundle created: $APP"
|
||||||
|
|
||||||
|
# ── Zip the .app bundle ──────────────────────────────────────────────────
|
||||||
|
local APP_ZIP="ObsidianDragon-${VERSION}-macOS-${MAC_ARCH}.app.zip"
|
||||||
|
if command -v zip &>/dev/null; then
|
||||||
|
(cd "$out" && zip -r "$APP_ZIP" "ObsidianDragon.app")
|
||||||
|
info "App zip: $out/$APP_ZIP ($(du -h "$out/$APP_ZIP" | cut -f1))"
|
||||||
|
fi
|
||||||
|
|
||||||
# ── Create DMG ───────────────────────────────────────────────────────────
|
# ── Create DMG ───────────────────────────────────────────────────────────
|
||||||
local DMG_NAME="DragonX_Wallet-${VERSION}-macOS-${MAC_ARCH}.dmg"
|
local DMG_NAME="DragonX_Wallet-${VERSION}-macOS-${MAC_ARCH}.dmg"
|
||||||
|
|
||||||
|
|||||||
671
docs/codebase-audit-2026-04-27.md
Normal file
@@ -0,0 +1,671 @@
|
|||||||
|
# Codebase Cleanup Audit
|
||||||
|
|
||||||
|
Current as of 2026-04-27 for ObsidianDragon `1.2.0-rc1`.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This audit covers architecture, threading, UI/layout code, build/resource integration, RPC/daemon behavior, and general cleanup opportunities. It is a static maintainability audit, not a runtime performance profile or security penetration test.
|
||||||
|
|
||||||
|
Evidence was gathered from source-tree scans, largest-file metrics, targeted code inspection, and focused architecture/UI/build review passes.
|
||||||
|
|
||||||
|
## Snapshot
|
||||||
|
|
||||||
|
- `src/` contains about 77,639 handwritten C++ source/header lines, excluding generated language outputs.
|
||||||
|
- Generated language headers previously accounted for about 34,675 source-tree lines under `src/embedded/lang_*.h`; Phase 4 moved those outputs to `build/generated/embedded/`.
|
||||||
|
- Largest handwritten files are concentrated in UI and application orchestration:
|
||||||
|
- `src/ui/windows/balance_tab.cpp`: 3,434 lines
|
||||||
|
- `src/app.cpp`: 3,097 lines
|
||||||
|
- `src/ui/windows/mining_tab.cpp`: 2,544 lines
|
||||||
|
- `src/ui/pages/settings_page.cpp`: 2,092 lines
|
||||||
|
- `src/main.cpp`: 2,014 lines
|
||||||
|
- `src/app_security.cpp`: 1,918 lines
|
||||||
|
- `src/app_network.cpp`: 1,912 lines
|
||||||
|
- `src/ui/windows/console_tab.cpp`: 1,572 lines
|
||||||
|
- `src/app_wizard.cpp`: 1,378 lines
|
||||||
|
- `src/daemon/embedded_daemon.cpp`: 1,213 lines
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
|
||||||
|
The codebase has a solid functional foundation: RPC work is already separated from UI-thread callbacks, the schema-driven UI system is useful, and embedded resource handling is well established. The largest opportunities are mostly structural rather than feature bugs.
|
||||||
|
|
||||||
|
The main cleanup theme is ownership. `App` currently coordinates lifecycle, rendering, daemon state, RPC refreshes, wallet security, dialogs, shutdown, mining, and first-run flow. That centralization makes features easy to wire in the short term, but it also pushes thread lifetime, error propagation, UI state, and background polling into a few very large files.
|
||||||
|
|
||||||
|
The second theme is consistency. Dialogs mostly use Material overlay helpers, but some still use raw ImGui modals. Many UI dimensions are schema-driven, but dialog/button/icon sizes still have scattered literals. RPC auth exists in more than one place, generated resources live partly in source and partly in build output, and error handling ranges from user-visible reporting to silent `catch (...)` blocks.
|
||||||
|
|
||||||
|
## Implementation Status
|
||||||
|
|
||||||
|
### Phase 1 Completed On 2026-04-27
|
||||||
|
|
||||||
|
- Replaced time-seeded `std::rand()` RPC credential generation in `src/rpc/connection.cpp` with libsodium-backed random generation.
|
||||||
|
- Replaced direct UI `system()` explorer/about launch calls with `util::Platform::openUrl()`.
|
||||||
|
- Reworked `util::Platform::openUrl()` and `openFolder()` so macOS/Linux launchers use `posix_spawnp()` arguments instead of shell-built command strings, and folder creation uses `std::filesystem::create_directories()`.
|
||||||
|
- Added URL scheme validation for platform URL opens.
|
||||||
|
- Added CMake visibility for missing `xxd` and Python theme expansion dependencies.
|
||||||
|
- Added `xxd` and Python checks/package coverage to `setup.sh`.
|
||||||
|
- Added shared `App::sendStopCommandSafely()` logging helper and routed repeated daemon `stop` RPC calls through it.
|
||||||
|
- Confirmed `libs/incbin.h` is already represented in `THIRD_PARTY_LICENSES`, so no additional license entry was needed.
|
||||||
|
- Verified with staged builds after logical batches, ending with `cd build && make -j$(nproc)` successfully linking `bin/ObsidianDragon`.
|
||||||
|
|
||||||
|
Remaining high-priority follow-ups from this audit include the larger `App`/security/refresh service boundaries and tests around the newly centralized runtime behavior.
|
||||||
|
|
||||||
|
### Phase 2 Completed On 2026-04-27
|
||||||
|
|
||||||
|
- Added schema-backed dialog layout tokens in `res/themes/ui.toml` and central accessors in `src/ui/layout.h` for common dialog widths, form width, action width/gap, max height ratio, and compact bottom alignment.
|
||||||
|
- Registered the `dialog` UI schema section in `src/ui/schema/ui_schema.cpp` so those tokens are available through the existing schema cache.
|
||||||
|
- Extended `material::BeginOverlayDialog()` with an optional ID suffix and added shared overlay action/footer helpers in `src/ui/material/draw_helpers.h`.
|
||||||
|
- Migrated address book Add/Edit address dialogs in `src/ui/windows/address_book_dialog.cpp` from raw `ImGui::BeginPopupModal()` usage to `material::BeginOverlayDialog()` with one shared form/action renderer.
|
||||||
|
- Added `src/ui/material/project_icons.h` as the wallet icon registry and moved pickaxe-specific font rendering behind that helper.
|
||||||
|
- Kept `AddressLabelDialog::drawIconByName()` and `iconGlyphForName()` as compatibility wrappers for existing call sites.
|
||||||
|
- Verified with `cd build && make -j$(nproc)` successfully linking `bin/ObsidianDragon`; diagnostics were clean on Phase 2 touched files, `git diff --check` passed, and scans found no remaining Add/Edit raw modal usage or local address-label icon arrays.
|
||||||
|
|
||||||
|
### Phase 3 Completed On 2026-04-27
|
||||||
|
|
||||||
|
- Added `src/util/async_task_manager.h/.cpp` as a named task owner with cancellation tokens, completed-task reaping, and join-on-shutdown behavior.
|
||||||
|
- Routed App-owned daemon maintenance, wizard daemon stop/check, encryption daemon restart, and decrypt restart/import background work through `AsyncTaskManager` instead of detached App threads.
|
||||||
|
- Kept `Bootstrap`'s worker joinable so its existing destructor cancellation can join the download/extract thread instead of losing ownership through `detach()`.
|
||||||
|
- Added `src/daemon/daemon_controller.h/.cpp` as the first daemon ownership boundary; it now owns `EmbeddedDaemon`, syncs settings into the daemon, and centralizes start/stop calls while `App` keeps a non-owning bridge pointer for low-churn follow-up migration.
|
||||||
|
- Extended `rpc::ConnectionConfig` with auth-source tracking and `use_tls`, parsed `rpctls`/`rpcssl`-style config flags, and centralized `.cookie` auth retry construction in `Connection::buildCookieAuthConfig()`.
|
||||||
|
- Updated main, fast-lane, temporary stop, wizard stop, and decrypt-import RPC clients to pass the TLS flag to `RPCClient`.
|
||||||
|
- Added a one-per-session runtime warning and Settings-page warning when a non-localhost RPC host is configured without TLS.
|
||||||
|
- Verified after each logical batch with `cmake .. && make -j$(nproc)` or `make -j$(nproc)` successfully linking `bin/ObsidianDragon`; diagnostics were clean on Phase 3 touched files, `git diff --check` passed, and scans found no remaining App-owned detached background tasks or App-level manual `.cookie` fallback.
|
||||||
|
|
||||||
|
### Phase 4 Completed On 2026-04-27
|
||||||
|
|
||||||
|
- Added `src/services/refresh_scheduler.h/.cpp` and moved refresh interval/timer policy out of `App`, while leaving RPC refresh bodies behavior-preserving in the existing app/network methods.
|
||||||
|
- Replaced App refresh timer fields with `services::RefreshScheduler`, including page-specific refresh policy, wallet mutation refresh marking, transaction-age throttling, OPID polling cadence, price refresh cadence, and fast mining/rescan ticks.
|
||||||
|
- Moved generated language headers from tracked `src/embedded/lang_*.h` files into `${CMAKE_BINARY_DIR}/generated/embedded/`, keeping the existing `embedded/lang_*.h` include strings working through the generated include directory.
|
||||||
|
- Added CTest infrastructure and `ObsidianDragonTests` with focused coverage for connection config parsing, cookie fallback and plaintext/TLS checks, payment URI parsing, fixed amount formatting, spendable wallet address filtering, and scheduler behavior.
|
||||||
|
- Added `src/util/amount_format.h/.cpp` for shared fixed-decimal amount formatting and used it in transaction send payload construction.
|
||||||
|
- Added pure spendability helpers in `src/data/wallet_state.h/.cpp` and routed Send-tab source address selection through them.
|
||||||
|
- Split low-risk helpers out of large UI tabs: balance helper formatting/drawing code, mining formatting/estimate/thread helpers, and the console RPC command reference registry.
|
||||||
|
- Verified focused tests with `cd build && cmake .. && make -j$(nproc) && ctest --output-on-failure`, then verified the UI split batch with `cd build && cmake .. && make -j$(nproc)` successfully linking `bin/ObsidianDragon`.
|
||||||
|
|
||||||
|
### Phase 5 Completed On 2026-04-27
|
||||||
|
|
||||||
|
- Added `src/services/wallet_security_controller.h/.cpp` and moved deferred wizard encryption/PIN state, connection retry throttling, PIN validation, and secure clearing behind a wallet-security boundary while keeping the existing RPC/UI behavior in `App`.
|
||||||
|
- Added `src/services/network_refresh_service.h/.cpp` as the fuller refresh boundary around `RefreshScheduler`, named refresh jobs, in-flight job guards, and explicit queue-pressure skips for core, address, transaction, mining, and peer refreshes.
|
||||||
|
- Extended `DaemonController` with shutdown/external-daemon policy decisions and routed `App::beginShutdown()` through that boundary.
|
||||||
|
- Grouped Settings-page static UI state into `SettingsPageState` and first-run wizard static UI state into `WizardUiState`, preserving existing file-local behavior while making state ownership explicit.
|
||||||
|
- Continued low-risk renderer splitting with console layout helpers, balance recent-transaction visual/amount helpers, and mining active-state/thread clamp helpers.
|
||||||
|
- Expanded focused tests for daemon shutdown policy, wallet security transitions, network refresh job guards, renderer helpers, and generated resource fallback behavior.
|
||||||
|
- Documented the `src/config/version.h` policy: it remains committed source for editor/release tooling compatibility, and CMake regenerates it from `src/config/version.h.in` during configure.
|
||||||
|
- Verified Phase 5 batches with repeated `cd build && cmake .. && make -j$(nproc) && ctest --output-on-failure` runs.
|
||||||
|
|
||||||
|
### Phase 6 Completed On 2026-04-27
|
||||||
|
|
||||||
|
- Expanded `WalletSecurityController` with mockable RPC and secure-vault gateways for deferred encryption, unlock, export, import, key classification, import messaging, decrypt export naming, and secure handoff behavior.
|
||||||
|
- Routed wallet encryption, deferred encryption retry, unlock, and key import/export classification through the wallet-security service boundary while preserving existing App-visible behavior.
|
||||||
|
- Extended `DaemonController` with lifecycle decisions for manual restart, rescan, blockchain-data deletion, and bootstrap daemon stop sequencing, then routed the corresponding App flows through those decisions.
|
||||||
|
- Promoted `NetworkRefreshService` to dispatch-ticket ownership with queue-depth telemetry, queue-pressure skips, in-flight skips, completion stats, cancellation, and stale-callback detection for named refresh jobs.
|
||||||
|
- Continued renderer splitting with stable modules for recent transaction presentation, pool-worker default selection, and console output filtering.
|
||||||
|
- Removed Settings-page compatibility aliases so the page now uses `SettingsPageState` fields directly.
|
||||||
|
- Expanded focused integration-style tests with mock wallet-security RPC/vault collaborators, daemon lifecycle policy checks, refresh dispatch telemetry/stale-callback edges, and UI helper coverage.
|
||||||
|
- Verified with `cd build && cmake .. && make -j$(nproc) && ctest --output-on-failure`.
|
||||||
|
|
||||||
|
Phase 7 completed on 2026-04-27:
|
||||||
|
- `DaemonController` now owns lifecycle execution ordering for restart, rescan, blockchain-data deletion, and bootstrap stops through mockable `LifecycleRuntime` and `LifecycleTaskContext` collaborators.
|
||||||
|
- App removed the temporary non-owning `embedded_daemon_` bridge; daemon state/output reads now route through `DaemonController` wrappers or explicit controller access.
|
||||||
|
- Added `WalletSecurityWorkflow` for decrypt/export/import dialog phase, step, import-active state, and wallet file planning, so the dialog state can be tested without constructing `App`.
|
||||||
|
- `NetworkRefreshService` now owns enqueue/callback wrapping for named refresh jobs, including queue-depth sampling, queue-pressure skips, stale callback suppression, and UI-thread callback handoff.
|
||||||
|
- Core, address, transaction, mining, peer, and encryption refresh jobs now use the service enqueue wrapper instead of app-local dispatch-ticket boilerplate.
|
||||||
|
- Split additional UI helpers into `balance_address_list`, `mining_benchmark`, and `console_input_model`, with focused tests for filtering/sorting, benchmark state estimates, history navigation, and autocomplete.
|
||||||
|
- Expanded `ObsidianDragonTests` with mock daemon lifecycle execution tests, wallet workflow tests, refresh enqueue/stale-callback tests, and the new UI module coverage.
|
||||||
|
- Verified after each logical batch with `cd build && cmake .. && make -j$(nproc) && ctest --output-on-failure`.
|
||||||
|
|
||||||
|
Phase 8 completed on 2026-04-27:
|
||||||
|
- Added `WalletSecurityWorkflowExecutor` with mockable RPC, import, file, daemon, and secure-vault cleanup gateways; decrypt wallet unlock/export/backup/restart/import/cleanup sequencing now runs through that executor instead of nested App-local orchestration.
|
||||||
|
- Completed named refresh enqueue coverage by routing price refresh through `NetworkRefreshService::enqueue(Job::Price)` and connect-time `getinfo`/`getwalletinfo` prefetch through `Job::ConnectionInit`.
|
||||||
|
- Added daemon lifecycle collaborators for async/immediate task contexts and blockchain-data cleanup, reducing App-specific lifecycle runtime code where the extracted pieces are directly testable.
|
||||||
|
- Continued large-view reduction with balance address-row layout/USD helpers, mining benchmark transition and pool saved/default helpers, and console command parse/result classification helpers.
|
||||||
|
- Expanded `ObsidianDragonTests` for workflow executor edges, ConnectionInit enqueue coverage, daemon lifecycle adapters, and the new UI helper/model behavior.
|
||||||
|
- Verified each logical Phase 8 batch with `cd build && cmake .. && make -j$(nproc) && ctest --output-on-failure`.
|
||||||
|
|
||||||
|
Phase 9 completed on 2026-04-27:
|
||||||
|
- Added typed refresh result models, JSON parsers, and service-owned apply contracts on `NetworkRefreshService` for connection info, wallet encryption, core balance/sync, mining, peers, and price refreshes.
|
||||||
|
- Routed `App` refresh callbacks through those contracts for connect-time prefetch, warmup info application, core refresh, encryption refresh, mining refresh, peer refresh, and price refresh while preserving the existing RPC call ordering.
|
||||||
|
- Expanded `ObsidianDragonTests` with focused refresh result parsing/application coverage for balance/sync state, connection metadata, wallet lock state, mining history, peer lists, banned peers, and price history.
|
||||||
|
- Documented remaining intentional process-wide ImGui state and legacy compatibility wrappers in `docs/ui-static-state.md`, and linked that policy from `docs/codebase-overview.md`.
|
||||||
|
- Verified the Phase 9 implementation batch with `cd build && cmake .. && make -j$(nproc) && ctest --output-on-failure`.
|
||||||
|
|
||||||
|
Phase 10 completed on 2026-04-27:
|
||||||
|
- Added `NetworkRefreshService::AddressRefreshResult` and routed address snapshot application through `applyAddressRefreshResult()` while preserving App-owned RPC collection and dirty-flag behavior.
|
||||||
|
- Added service-owned transaction view-cache models plus `TransactionRefreshResult`/`TransactionCacheUpdate` so transaction refresh callbacks now apply `WalletState::transactions`, `last_tx_update`, `last_tx_block_height_`, `viewtx_cache_`, `send_txids_`, and confirmed transaction caches through one cache-update contract.
|
||||||
|
- Moved z-viewtransaction output parsing and outgoing-send enrichment into `NetworkRefreshService` helpers, leaving only the RPC call order and per-cycle throttling in `App`.
|
||||||
|
- Evaluated the remaining decrypt workflow orchestration and kept the nested UI step handoffs App-owned because those boundaries still carry progress updates, worker-thread callback ordering, and shutdown/token cancellation semantics.
|
||||||
|
- Did not touch high-churn tab/dialog statics in this phase; `docs/ui-static-state.md` remains the policy for future behavior-adjacent UI state changes.
|
||||||
|
- Expanded `ObsidianDragonTests` with address/transaction applicator coverage, view-transaction enrichment/cache-update tests, stale callback cancellation coverage, additional remote/TLS config parsing, and shutdown-cancellation lifecycle coverage.
|
||||||
|
- Verified the Phase 10 implementation batch with `cd build && cmake .. && make -j$(nproc) && ctest --output-on-failure`.
|
||||||
|
|
||||||
|
Phase 11 completed on 2026-04-27:
|
||||||
|
- Revisited the decrypt restart/import flow and kept the remaining UI handoff choreography App-owned because each boundary still corresponds to an explicit progress step, cancellation check, or background import transition. No partial executor move was made.
|
||||||
|
- Moved additional address refresh construction into `NetworkRefreshService` helpers for shielded-address validation results, transparent address parsing, and unspent-output balance application while preserving the existing daemon RPC call order in `App::refreshAddressData()`.
|
||||||
|
- Moved transaction pre-refresh snapshot construction and list parsing helpers into `NetworkRefreshService`, including shielded address snapshots, fully enriched txid snapshots, transparent transaction parsing, shielded receive parsing, and final transaction sorting. `App::refreshTransactionData()` still owns RPC call order and per-cycle `z_viewtransaction` throttling.
|
||||||
|
- Expanded `ObsidianDragonTests` with focused coverage for the new address/transaction snapshot helpers, worker callback ordering across independent refresh jobs, and reconnect-style stale transaction callbacks.
|
||||||
|
- Did not migrate additional tab/dialog statics because Phase 11 did not touch those views for behavior changes.
|
||||||
|
- Verified with `cd build && cmake .. && make -j$(nproc) && ctest --output-on-failure`.
|
||||||
|
|
||||||
|
Phase 12 completed on 2026-04-27:
|
||||||
|
- Added a shared ordered mock refresh RPC fixture in `ObsidianDragonTests` so service-level refresh collectors can assert exact daemon method ordering and parameters.
|
||||||
|
- Introduced `NetworkRefreshService::RefreshRpcGateway` plus `collectAddressRefreshResult()` and `collectTransactionRefreshResult()`; address and transaction refresh RPC collection now lives behind the service boundary while preserving the same daemon call order and per-cycle `z_viewtransaction` cap.
|
||||||
|
- Routed `App::refreshAddressData()` and `App::refreshTransactionData()` through a small `RPCClient` adapter for those collectors, keeping App focused on enqueueing and applying the returned results.
|
||||||
|
- Expanded refresh lifecycle coverage for ordered callbacks, reconnect-style stale transaction callbacks, and collector ordering around address validation, z-balance fallback, shielded receive polling, cached viewtransaction entries, fresh `z_viewtransaction`, and `gettransaction` enrichment.
|
||||||
|
- Revisited decrypt restart/import orchestration again and made no extra move because the remaining boundaries are still the explicit progress/cancellation/import handoff points.
|
||||||
|
- Did not migrate additional tab/dialog statics because Phase 12 did not touch those views for behavior changes.
|
||||||
|
- Verified with `cd build && cmake .. && make -j$(nproc) && ctest --output-on-failure`.
|
||||||
|
|
||||||
|
Phase 13 completed on 2026-04-27:
|
||||||
|
- Added service-owned core and peer refresh collectors behind `NetworkRefreshService::RefreshRpcGateway`, moving the fixed `z_gettotalbalance`/`getblockchaininfo` and `getpeerinfo`/`listbanned` RPC bodies out of `App`.
|
||||||
|
- Extended the ordered mock RPC fixture tests to assert the exact core and peer daemon call order, empty parameter arrays, parsed result values, and partial-failure behavior where the second RPC still runs after the first fails.
|
||||||
|
- Kept warmup `getinfo`, mining refresh, connection init, and price fetch orchestration App-owned because those paths either have UI status handoffs, cadence-specific behavior, or non-RPC HTTP/callback details that were not part of this small collector move.
|
||||||
|
- Did not add new reconnect/shutdown lifecycle tests because Phase 13 did not change async cancellation, daemon restart, or worker lifecycle ownership.
|
||||||
|
- Revisited decrypt restart/import orchestration and high-churn UI state migrations by scope only; no behavior work touched those complete progress/cancellation or tab/dialog seams.
|
||||||
|
- Verified with `cd build && cmake .. && make -j$(nproc) && ctest --output-on-failure`.
|
||||||
|
|
||||||
|
Phase 14 completed on 2026-04-27:
|
||||||
|
- Added `NetworkRefreshService::collectMiningRefreshResult()` behind `RefreshRpcGateway`; the service now owns the ordered `getlocalsolps` plus optional `getmininginfo` RPC collection while `App` still owns the fast/slow cadence decision and daemon-memory snapshot.
|
||||||
|
- Added `NetworkRefreshService::collectConnectionInitResult()` so the initial `getinfo` then `getwalletinfo` prefetch is service-owned while `App::onConnected()` keeps the high-priority enqueue and `encryption_state_prefetched_` lock-screen timing flag.
|
||||||
|
- Extended ordered mock RPC tests for mining slow ticks, mining fast-only ticks, mining partial failure, connection-init success, and connection-init wallet-info prefetch after `getinfo` failure.
|
||||||
|
- Did not add reconnect/shutdown lifecycle tests because Phase 14 did not change worker callback ownership, shutdown, reconnect, daemon restart, or cancellation behavior.
|
||||||
|
- Did not touch decrypt orchestration or high-churn UI state because no complete behavior-preserving step in those areas was part of the batch.
|
||||||
|
- Verified with `cd build && cmake .. && make -j$(nproc) && ctest --output-on-failure`.
|
||||||
|
|
||||||
|
Phase 15 completed on 2026-04-27:
|
||||||
|
- Added `NetworkRefreshService::WarmupPollResult` and `collectWarmupPollResult()` behind `RefreshRpcGateway`; the service now owns the single warmup `getinfo` call and preserves either parsed connection info or the raw RPC error string.
|
||||||
|
- Routed the warmup branch of `App::refreshCoreData()` through the new collector while keeping UI status translation, daemon block-height decoration, `connection_status_`, and the `refreshData()` transition App-owned.
|
||||||
|
- Extended ordered mock RPC tests for warmup success and warmup failure so the `getinfo` call, empty params, parsed ready state, and error propagation are directly covered.
|
||||||
|
- Reviewed price HTTP fetching and kept it App-owned because the current worker callback, libcurl setup, HTTP-status handling, logging, and parse/apply handoff are clearer in one place; only JSON response parsing remains service-owned.
|
||||||
|
- Left command-style RPC actions App-owned because Phase 15 did not include behavior changes for send, address creation, mining toggles, ban operations, or import/export commands.
|
||||||
|
- Did not add reconnect/shutdown lifecycle tests because worker callback ownership, shutdown, reconnect, daemon restart, and cancellation behavior did not change.
|
||||||
|
- Did not touch decrypt orchestration or high-churn UI state because no complete behavior-preserving step in those areas was part of the batch.
|
||||||
|
- Verified with `cd build && cmake .. && make -j$(nproc) && ctest --output-on-failure`.
|
||||||
|
|
||||||
|
Phase 16 completed on 2026-04-27:
|
||||||
|
- Added `NetworkRefreshService::PriceHttpResponse`, `PriceHttpResult`, and `parsePriceHttpResponse()` so libcurl transport status, HTTP status, parse success, and failure messages are represented as a focused service-owned boundary.
|
||||||
|
- Kept libcurl initialization, request execution, worker callback ownership, successful price logging, and UI-thread price application in `App::refreshPrice()`.
|
||||||
|
- Expanded focused tests for successful price HTTP parsing, HTTP non-200 failures, transport failures, and unrecognized response bodies.
|
||||||
|
- Kept command-style RPC actions App-owned because Phase 16 did not include a complete behavior change for send, address creation, mining toggles, ban operations, or import/export commands.
|
||||||
|
- Did not add reconnect/shutdown lifecycle tests because callback ownership, cancellation, shutdown, reconnect, and daemon restart behavior did not change.
|
||||||
|
- Did not touch decrypt orchestration or high-churn UI state because no behavior work touched those workflows.
|
||||||
|
- Verified with `cd build && cmake .. && make -j$(nproc) && ctest --output-on-failure`.
|
||||||
|
|
||||||
|
Phase 17 completed on 2026-04-27:
|
||||||
|
- Reviewed command-style RPC actions for send, address creation, mining toggles, ban operations, and key import/export; kept them App-owned because no complete behavior change created a focused extraction boundary with tests.
|
||||||
|
- Reviewed lifecycle, reconnect, shutdown, and daemon restart ownership; no new lifecycle tests were added because callback ownership, cancellation, shutdown, reconnect, and daemon restart behavior did not change.
|
||||||
|
- Reviewed decrypt and high-churn UI-state seams by scope; no changes were made because Phase 17 did not directly touch those workflows for behavior.
|
||||||
|
- Kept refresh and price boundaries stable because the current service collectors/result helpers already cover the testable seams introduced in prior phases, and no new smaller behavior seam appeared.
|
||||||
|
- Verified the stable-boundary decision with `cd build && cmake .. && make -j$(nproc) && ctest --output-on-failure`.
|
||||||
|
|
||||||
|
Phase 18 completed on 2026-04-27:
|
||||||
|
- Re-reviewed the remaining command-style RPC actions (`setgenerate`, `setban`, new address creation, key import, send) and kept them App-owned because Phase 18 did not include a real behavior change that supplied a focused extraction boundary and tests.
|
||||||
|
- Re-reviewed reconnect, shutdown, daemon restart, and callback ownership; no lifecycle tests were added because those ownership and cancellation behaviors did not change.
|
||||||
|
- Re-reviewed decrypt and high-churn UI-state seams by scope; no code changes were made because those workflows were not directly touched for behavior.
|
||||||
|
- Preserved the stable refresh and price boundaries introduced in prior phases because no new behavior made a smaller tested seam useful.
|
||||||
|
- Verified the feature-driven cleanup decision with `cd build && cmake .. && make -j$(nproc) && ctest --output-on-failure`.
|
||||||
|
|
||||||
|
Phase 19 completed on 2026-04-27:
|
||||||
|
- Treated the audit as feature-scoped maintenance rather than an extraction-only phase and made no source changes because no real behavior change created a new focused test seam.
|
||||||
|
- Re-reviewed command-style RPC actions (`setgenerate`, `setban`, new address creation, key import, send) and kept them App-owned because their current boundaries are command-specific and tied to immediate UI/app-state updates.
|
||||||
|
- Re-reviewed lifecycle, reconnect, shutdown, daemon restart, decrypt, and high-churn UI-state seams; no tests or migrations were added because those behaviors did not change directly.
|
||||||
|
- Preserved the stable refresh and price service boundaries because the existing collectors/result helpers already cover the testable seams created by earlier work.
|
||||||
|
- Verified the maintenance decision with `cd build && cmake .. && make -j$(nproc) && ctest --output-on-failure`.
|
||||||
|
|
||||||
|
Phase 20 completed on 2026-04-27:
|
||||||
|
- Moved the cleanup audit into maintenance mode and avoided additional extraction-only work because no concrete behavior change created a smaller tested seam.
|
||||||
|
- Kept command-style RPC actions App-owned until a complete command workflow change supplies focused tests.
|
||||||
|
- Added no lifecycle, decrypt, or UI-state tests because callback ownership, cancellation, daemon restart, decrypt flow, and UI state behavior did not change directly.
|
||||||
|
- Preserved stable refresh and price service boundaries because no new behavior exposed a better tested boundary.
|
||||||
|
- Verified maintenance mode with `cd build && cmake .. && make -j$(nproc) && ctest --output-on-failure`.
|
||||||
|
|
||||||
|
Phase 21 completed on 2026-04-27:
|
||||||
|
- Sustained maintenance mode and avoided extraction-only work because no concrete feature change created a focused tested seam.
|
||||||
|
- Kept command-style RPC actions App-owned until a complete command workflow change supplies focused tests.
|
||||||
|
- Added no lifecycle, decrypt, or UI-state coverage because callback ownership, cancellation, daemon restart, decrypt flow, and UI state behavior did not change directly.
|
||||||
|
- Preserved stable refresh and price service boundaries because no new behavior exposed a smaller tested seam.
|
||||||
|
- Verified sustained maintenance mode with `cd build && cmake .. && make -j$(nproc) && ctest --output-on-failure`.
|
||||||
|
|
||||||
|
Phase 22 completed on 2026-04-27:
|
||||||
|
- Continued sustained maintenance mode and avoided extraction-only work because no concrete feature change created a focused tested seam.
|
||||||
|
- Kept command-style RPC actions App-owned until a complete command workflow change supplies focused tests.
|
||||||
|
- Added no lifecycle, decrypt, or UI-state coverage because callback ownership, cancellation, daemon restart, decrypt flow, and UI state behavior did not change directly.
|
||||||
|
- Preserved stable refresh and price service boundaries because no new behavior exposed a smaller tested seam.
|
||||||
|
- Verified the maintenance checkpoint with `cd build && cmake .. && make -j$(nproc) && ctest --output-on-failure`.
|
||||||
|
|
||||||
|
Remaining Phase 23 follow-ups:
|
||||||
|
- Continue sustained maintenance mode and avoid extraction-only phases unless concrete feature work creates a focused tested seam.
|
||||||
|
- Keep command-style RPC actions App-owned until a complete command workflow change supplies focused tests.
|
||||||
|
- Add lifecycle, decrypt, or UI-state coverage only when directly changing callback ownership, cancellation, daemon restart, decrypt flow, or UI state behavior.
|
||||||
|
- Preserve stable refresh and price service boundaries unless new behavior exposes a smaller tested seam.
|
||||||
|
|
||||||
|
## High-Priority Findings
|
||||||
|
|
||||||
|
### 1. `App` Is Carrying Too Many Responsibilities
|
||||||
|
|
||||||
|
Evidence:
|
||||||
|
- `src/app.h`, `src/app.cpp`, `src/app_network.cpp`, `src/app_security.cpp`, and `src/app_wizard.cpp` form a broad partial-class style application layer.
|
||||||
|
- `src/app.cpp` is 3,141 lines, with lifecycle, rendering, daemon/RPC startup, shutdown, modal rendering, and restart flows.
|
||||||
|
- `src/app_network.cpp` is 1,926 lines and contains many refresh, polling, send, mining, market, peer, and address metadata paths.
|
||||||
|
- `src/app_security.cpp` is 1,819 lines and combines wallet encryption, PIN handling, secure vault, import/export, and restart flows.
|
||||||
|
|
||||||
|
Why it matters:
|
||||||
|
- Changes in one workflow can easily affect unrelated lifecycle or UI behavior.
|
||||||
|
- It is hard to unit-test isolated flows without instantiating most of the app.
|
||||||
|
- Shutdown and async callback ownership are difficult to reason about because many subsystems capture or mutate `App` state directly.
|
||||||
|
|
||||||
|
Recommended cleanup:
|
||||||
|
- Extract a `DaemonController` for embedded/external daemon ownership, restart, port ownership, and shutdown sequencing.
|
||||||
|
- Extract a `WalletSecurityController` for PIN, encryption, secure vault, and key import/export flow state.
|
||||||
|
- Extract a `NetworkRefreshService` or `WalletRefreshScheduler` for balance/address/transaction/mining/market polling.
|
||||||
|
- Keep `App` focused on initialization, top-level navigation, frame dispatch, and service composition.
|
||||||
|
|
||||||
|
Suggested first slice:
|
||||||
|
- Completed in Phase 3: add the first `DaemonController` boundary around `EmbeddedDaemon` ownership, settings sync, and start/stop behavior. Follow-up work should move more restart/shutdown sequencing and external-daemon policy into the controller.
|
||||||
|
|
||||||
|
### 2. Background Thread Lifetime Is Not Centrally Owned
|
||||||
|
|
||||||
|
Evidence:
|
||||||
|
- Detached threads appear in `src/app.cpp`, `src/app_security.cpp`, `src/app_wizard.cpp`, `src/util/bootstrap.cpp`, and `src/main.cpp`.
|
||||||
|
- Several lambdas capture `this` and then call back after waits or daemon operations.
|
||||||
|
- Managed thread members also exist (`shutdown_thread_`, `daemon_restart_thread_`, `wizard_stop_thread_`, monitor threads), but ownership patterns vary by subsystem.
|
||||||
|
|
||||||
|
Why it matters:
|
||||||
|
- Detached work can outlive the `App` object or UI state it touches.
|
||||||
|
- Shutdown order is harder to guarantee.
|
||||||
|
- Cancellation is inconsistent, especially across daemon restart, wallet import, bootstrap download, and PIN unlock flows.
|
||||||
|
|
||||||
|
Recommended cleanup:
|
||||||
|
- Introduce an `AsyncTaskManager` owned by `App`, with named tasks, cancellation flags/tokens, and join-on-shutdown behavior.
|
||||||
|
- Replace new detached threads with submitted tasks and explicit cancellation.
|
||||||
|
- Require background callbacks to hop through the existing UI callback queue before touching UI state.
|
||||||
|
- Add shutdown assertions or logging for tasks that fail to stop promptly.
|
||||||
|
|
||||||
|
Suggested first slice:
|
||||||
|
- Completed in Phase 3: convert App-owned daemon restart/import/wizard background work to `AsyncTaskManager` and keep `Bootstrap`'s worker joinable. Follow-up work should move remaining subsystem monitor/watchdog policies behind explicit owners where useful.
|
||||||
|
|
||||||
|
### 3. RPC Credential Generation And Remote RPC Handling Need Hardening
|
||||||
|
|
||||||
|
Evidence:
|
||||||
|
- Before Phase 1, `src/rpc/connection.cpp` generated default `rpcuser`/`rpcpassword` with `std::srand(std::time(nullptr))` and `std::rand()`.
|
||||||
|
- Phase 1 replaced that path with libsodium-backed random generation.
|
||||||
|
- Before Phase 3, `src/rpc/rpc_client.cpp` always constructed an `http://host:port/` URL.
|
||||||
|
- Before Phase 3, `.cookie` auth handling appeared in `src/rpc/connection.cpp` and also as a 401 fallback in `src/app_network.cpp`.
|
||||||
|
|
||||||
|
Why it matters:
|
||||||
|
- `std::rand()` is not appropriate for credentials.
|
||||||
|
- HTTP is acceptable for strict localhost daemon RPC, but remote hosts can expose credentials unless the UI explicitly warns or supports TLS.
|
||||||
|
- Duplicate auth fallback paths make it harder to reason about precedence and failures.
|
||||||
|
|
||||||
|
Recommended cleanup:
|
||||||
|
- Completed in Phase 1: generate RPC credentials with libsodium-backed random generation.
|
||||||
|
- Completed in Phase 3: centralize `.cookie` retry construction in `Connection` after config-password failure.
|
||||||
|
- Completed in Phase 3: add a `use_tls` transport flag to `ConnectionConfig` and pass it through `RPCClient` connections.
|
||||||
|
- Completed in Phase 3: warn at runtime and in Settings when a non-localhost RPC host uses plaintext HTTP.
|
||||||
|
|
||||||
|
Suggested first slice:
|
||||||
|
- Completed in Phase 1 and Phase 3: replace `std::rand()` credential generation, centralize cookie fallback, and add remote plaintext/TLS handling.
|
||||||
|
|
||||||
|
### 4. Shell Launching Uses `system()` In UI And Platform Paths
|
||||||
|
|
||||||
|
Evidence:
|
||||||
|
- Before Phase 1, `src/util/platform.cpp` used `system()` for URL/folder opening and directory creation on some platforms.
|
||||||
|
- Before Phase 1, `src/ui/windows/transactions_tab.cpp`, `src/ui/windows/transaction_details_dialog.cpp`, and `src/ui/windows/about_dialog.cpp` called `system()` directly.
|
||||||
|
- Phase 1 routed these UI calls through `util::Platform::openUrl()` and removed shell-built launcher strings from `Platform` on macOS/Linux.
|
||||||
|
|
||||||
|
Why it matters:
|
||||||
|
- Shell invocation increases quoting and command-injection risk.
|
||||||
|
- Direct calls bypass the existing platform abstraction.
|
||||||
|
- Behavior varies by shell and desktop environment.
|
||||||
|
|
||||||
|
Recommended cleanup:
|
||||||
|
- Route all URL/file/folder launch behavior through `Platform` helpers.
|
||||||
|
- Replace shell strings with platform APIs where possible: `ShellExecute` on Windows, `open` or `xdg-open` via `fork/exec` or `posix_spawn` on Unix-like systems.
|
||||||
|
- Validate URL schemes before opening external links.
|
||||||
|
- Remove direct `system()` calls from UI windows.
|
||||||
|
|
||||||
|
Suggested first slice:
|
||||||
|
- Completed in Phase 1: update About, transaction details, and transaction tab actions to use `Platform::openUrl()` and safer platform launcher behavior.
|
||||||
|
|
||||||
|
## Medium-Priority Findings
|
||||||
|
|
||||||
|
### 5. Silent And Broad Exception Handling Is Common
|
||||||
|
|
||||||
|
Evidence:
|
||||||
|
- Empty or near-empty `catch (...)` blocks appear in `src/app.cpp`, `src/app_network.cpp`, `src/app_security.cpp`, `src/app_wizard.cpp`, `src/rpc/rpc_worker.cpp`, `src/rpc/rpc_client.cpp`, `src/main.cpp`, and several UI files.
|
||||||
|
- Repeated `try { rpc_->call("stop"); } catch (...) {}` patterns appear in daemon stop flows.
|
||||||
|
|
||||||
|
Why it matters:
|
||||||
|
- Failures vanish from logs and UI, making support and diagnosis difficult.
|
||||||
|
- Some swallowed failures are acceptable during best-effort shutdown, but the code does not consistently explain that intent.
|
||||||
|
- Repeated patterns invite copy-paste drift.
|
||||||
|
|
||||||
|
Recommended cleanup:
|
||||||
|
- Add small helpers such as `stopDaemonSafely(context)` and `parseFloatOrDefault(value, fallback, context)`.
|
||||||
|
- Log best-effort failures with enough context to debug without spamming normal shutdown.
|
||||||
|
- Prefer typed catches where the thrown type is known.
|
||||||
|
- For RPC work, propagate categorized failures to the notification system when they affect user-visible state.
|
||||||
|
|
||||||
|
Suggested first slice:
|
||||||
|
- Completed in Phase 1: replace repeated daemon stop catch blocks with a single helper that logs at debug/verbose level.
|
||||||
|
|
||||||
|
### 6. UI Dialogs Have Mixed Modal Systems And Repeated Layout Literals
|
||||||
|
|
||||||
|
Evidence:
|
||||||
|
- Most dialogs use `Material::BeginOverlayDialog()`, but `src/ui/windows/address_book_dialog.cpp` still uses raw `ImGui::BeginPopupModal()` for Add/Edit address modals.
|
||||||
|
- Dialog widths such as `480.0f`, `500.0f`, `620.0f`, and `660.0f` appear across UI files.
|
||||||
|
- Button widths and padding literals such as `140.0f`, `160.0f`, and `24.0f` are repeated.
|
||||||
|
- Icon font sizes are hardcoded in `src/ui/material/typography.cpp` as 14, 18, 24, and 40 px families.
|
||||||
|
|
||||||
|
Why it matters:
|
||||||
|
- The schema system already solves this class of problem, but not all dimensions use it yet.
|
||||||
|
- Modal behavior diverges when some dialogs bypass overlay scrim/input blocking helpers.
|
||||||
|
- Theme and density changes require hunting through many files.
|
||||||
|
|
||||||
|
Recommended cleanup:
|
||||||
|
- Add schema-backed dialog tokens under `res/themes/ui.toml`, such as `globals.dialog.width-default`, `width-lg`, `min-width`, and `max-height-ratio`.
|
||||||
|
- Add schema-backed action widths under a shared `button` or `globals.button-sizes` section.
|
||||||
|
- Move icon size selection into schema or a single icon token helper.
|
||||||
|
- Migrate address book Add/Edit dialogs to `BeginOverlayDialog()` and extract one shared form renderer.
|
||||||
|
|
||||||
|
Suggested first slice:
|
||||||
|
- Completed in Phase 2: refactor the address book Add/Edit dialog because it had duplicated modal form code and was still outside the overlay helper path.
|
||||||
|
|
||||||
|
### 7. UI State Uses Many Static Locals And File-Scoped Globals
|
||||||
|
|
||||||
|
Evidence:
|
||||||
|
- `src/ui/pages/settings_page.cpp` has many `sp_*` static variables for page state.
|
||||||
|
- `src/app_wizard.cpp` uses local statics for first-run appearance and daemon checks.
|
||||||
|
- Theme/color/effect modules use static state for current theme and effect initialization.
|
||||||
|
|
||||||
|
Why it matters:
|
||||||
|
- Static state is hard to reset in tests.
|
||||||
|
- It makes multi-window or future session-reset behavior harder.
|
||||||
|
- Initialization order and stale state can become subtle bugs after hot reloads or account switching.
|
||||||
|
|
||||||
|
Recommended cleanup:
|
||||||
|
- Introduce `SettingsPageState` and `WizardState` structures owned by `App` or a UI state container.
|
||||||
|
- Keep global/static state only for immutable constants or explicit process-wide singletons.
|
||||||
|
- Add reset/apply methods for state structures to support theme reload and test setup.
|
||||||
|
|
||||||
|
Suggested first slice:
|
||||||
|
- Move settings-page statics into a single struct without changing behavior. This is mostly mechanical and improves readability.
|
||||||
|
|
||||||
|
### 8. Build And Generated Resource Lifecycle Is Split Across Source, Build, And Scripts
|
||||||
|
|
||||||
|
Evidence:
|
||||||
|
- `src/config/version.h` is generated from `src/config/version.h.in` but the generated file is present in source control.
|
||||||
|
- CMake generates `src/embedded/lang_*.h` from `res/lang/*.json` with `xxd`.
|
||||||
|
- `setup.sh` does not appear to check for `xxd`.
|
||||||
|
- `find_package(Python3 QUIET COMPONENTS Interpreter)` is used for theme expansion; CMake falls back when Python is unavailable.
|
||||||
|
- Font `OBJECT_DEPENDS` are listed manually in `CMakeLists.txt`.
|
||||||
|
- `src/resources/embedded_resources.cpp` conditionally includes `embedded_data.h`, which is produced by release/build scripts for some platforms.
|
||||||
|
|
||||||
|
Why it matters:
|
||||||
|
- Generated files under `src/` make the source tree noisier and can cause stale diffs.
|
||||||
|
- Missing tools fail late or silently reduce build output quality.
|
||||||
|
- Resource embedding behavior differs by platform without one obvious matrix.
|
||||||
|
|
||||||
|
Recommended cleanup:
|
||||||
|
- Generate language headers under `build/generated/embedded/` and include that directory instead of writing under `src/embedded/`.
|
||||||
|
- Decide whether `src/config/version.h` should be generated-only or committed, then enforce that decision with `.gitignore` and documentation.
|
||||||
|
- Add explicit `xxd` checks to `setup.sh` and CMake configure output.
|
||||||
|
- Make the Python theme-expansion fallback a warning, not a quiet behavior change.
|
||||||
|
- Replace manual font `OBJECT_DEPENDS` with a generated list or CMake glob configured for the font directory.
|
||||||
|
- Document the embedded resource matrix by platform and build type.
|
||||||
|
|
||||||
|
Suggested first slice:
|
||||||
|
- Completed in Phase 1: add `xxd` detection and make missing Python theme expansion noisy.
|
||||||
|
|
||||||
|
### 9. Polling And Refresh Flow Would Benefit From A Scheduler Boundary
|
||||||
|
|
||||||
|
Evidence:
|
||||||
|
- `src/app_network.cpp` coordinates many refresh paths and timings.
|
||||||
|
- `src/app.cpp` also contains timed daemon/mining/rescan update logic.
|
||||||
|
- `src/rpc/rpc_worker.cpp` already provides a worker queue, but refresh orchestration is still spread through app-level methods.
|
||||||
|
|
||||||
|
Why it matters:
|
||||||
|
- Polling order, throttle behavior, and cancellation are difficult to audit.
|
||||||
|
- Adding one more refresh path can accidentally affect responsiveness or RPC queue pressure.
|
||||||
|
- Console fast-lane RPC is a good pattern, but regular refresh batches still need a clear scheduler owner.
|
||||||
|
|
||||||
|
Recommended cleanup:
|
||||||
|
- Add a `PollingScheduler` or `RefreshScheduler` that owns timer intervals, dependencies, cancellation, and rate limiting.
|
||||||
|
- Model refreshes as named jobs with last-run time, minimum interval, and in-flight state.
|
||||||
|
- Keep `RPCWorker` as execution plumbing while moving scheduling decisions out of `App`.
|
||||||
|
|
||||||
|
Suggested first slice:
|
||||||
|
- Completed in Phase 4: added `RefreshScheduler` for refresh timers, page intervals, transaction age throttling, OPID cadence, price refresh, and fast tick behavior.
|
||||||
|
- Completed in Phase 7 and Phase 8: `NetworkRefreshService` now owns enqueue/callback wrapping for core, address, transaction, mining, peer, encryption, price, and connection-init jobs, including queue pressure and stale callback suppression.
|
||||||
|
|
||||||
|
### 10. Large UI Tabs Need Feature-Sliced Components
|
||||||
|
|
||||||
|
Evidence:
|
||||||
|
- `src/ui/windows/balance_tab.cpp` is 3,562 lines.
|
||||||
|
- `src/ui/windows/mining_tab.cpp` is 2,842 lines.
|
||||||
|
- `src/ui/windows/console_tab.cpp` is 1,861 lines.
|
||||||
|
- Several of these files combine state management, drawing, filtering, formatting, and modal coordination.
|
||||||
|
|
||||||
|
Why it matters:
|
||||||
|
- UI changes become difficult to review because unrelated drawing and state logic share one file.
|
||||||
|
- Reusable patterns stay local and get reimplemented elsewhere.
|
||||||
|
- Testing smaller formatting/state helpers is harder when they are buried in render functions.
|
||||||
|
|
||||||
|
Recommended cleanup:
|
||||||
|
- Split large tabs by feature area: panels, table/list renderers, local state structs, formatting helpers, and action handlers.
|
||||||
|
- Keep ImGui draw calls close to the view but move data preparation and command decisions into smaller helpers.
|
||||||
|
- Avoid introducing inheritance-heavy UI abstractions; simple namespaces and structs should be enough.
|
||||||
|
|
||||||
|
Suggested first slice:
|
||||||
|
- Completed in Phase 4 through Phase 8: extracted low-risk balance helpers, balance address-list/recent-transaction models, mining benchmark/pool helpers, console command/input/output models, and additional address-row/mining/console execution helpers. Remaining work should continue moving draw-heavy action glue only when it lowers review/test cost.
|
||||||
|
|
||||||
|
## Low-Priority And Quick-Win Findings
|
||||||
|
|
||||||
|
### 11. Test Infrastructure Is Missing Or Not Obvious
|
||||||
|
|
||||||
|
Before Phase 4, no test target was visible in the workspace snapshot. Phase 4 added a small CTest executable covering connection config parsing, `.cookie` fallback priority, plaintext/TLS detection, wallet state filtering, amount formatting, payment URI parsing, and scheduler behavior.
|
||||||
|
|
||||||
|
Recommended cleanup:
|
||||||
|
- Completed in Phase 4: add a small CTest target with a lightweight assertion harness for pure utility/RPC/scheduler helpers.
|
||||||
|
- Consider migrating to Catch2 or GoogleTest if the test suite grows beyond a few focused files.
|
||||||
|
- Add mocks for `RPCClient` and daemon process state once service boundaries exist.
|
||||||
|
|
||||||
|
### 12. Translation Key Usage Is Inconsistent
|
||||||
|
|
||||||
|
`TR()` and `TrId()` coexist in places such as Settings. This is workable, but it increases typo risk and makes missing translations harder to audit.
|
||||||
|
|
||||||
|
Recommended cleanup:
|
||||||
|
- Define constants for high-traffic translation keys.
|
||||||
|
- Prefer one translation call style inside each module.
|
||||||
|
- Add a script that compares referenced keys against `res/lang/*.json`.
|
||||||
|
|
||||||
|
### 13. Project Icon Handling Needs One Registry
|
||||||
|
|
||||||
|
Material icons are referenced directly in many files, while the pickaxe icon uses a special one-glyph font path. The special case is now documented in `docs/codebase-overview.md`, but a code-level registry would make future icon work cleaner.
|
||||||
|
|
||||||
|
Recommended cleanup:
|
||||||
|
- Completed in Phase 2: add `src/ui/material/project_icons.h` for app-specific icon names.
|
||||||
|
- Completed in Phase 2: encapsulate pickaxe rendering behind one helper, so feature code does not need to know which font supplies it.
|
||||||
|
|
||||||
|
### 14. Release/Portability Docs Can Be Closer To The Real Build
|
||||||
|
|
||||||
|
`build.sh` contains much more platform-specific behavior than the README currently explains. AppImage dependency bundling, macOS universal build defaults, Windows MinGW assumptions, embedded daemon/resource behavior, and third-party license sync deserve their own build notes.
|
||||||
|
|
||||||
|
Recommended cleanup:
|
||||||
|
- Add a focused `docs/build-and-release.md`.
|
||||||
|
- Document development build vs release packaging per platform.
|
||||||
|
- Add a checklist for third-party license updates, including `libs/incbin.h`.
|
||||||
|
|
||||||
|
## Suggested Roadmap
|
||||||
|
|
||||||
|
### Phase 1: Small High-Value Fixes
|
||||||
|
|
||||||
|
- [x] Replace weak RPC credential generation in `src/rpc/connection.cpp`.
|
||||||
|
- [x] Route direct URL/open actions away from raw `system()` calls.
|
||||||
|
- [x] Add `xxd` checks and noisy Python theme-expansion warnings.
|
||||||
|
- [x] Confirm missing third-party license entry for INCBIN is not needed because `THIRD_PARTY_LICENSES` already includes it.
|
||||||
|
- [x] Extract repeated daemon stop try/catch blocks into a helper with contextual logging.
|
||||||
|
|
||||||
|
### Phase 2: UI Consistency Pass
|
||||||
|
|
||||||
|
- [x] Move common dialog sizes and action button widths into `res/themes/ui.toml` or existing layout helpers.
|
||||||
|
- [x] Migrate address book Add/Edit dialogs to `BeginOverlayDialog()`.
|
||||||
|
- [x] Add a shared dialog content/footer helper.
|
||||||
|
- [x] Add an icon registry and hide the pickaxe font special case behind it.
|
||||||
|
|
||||||
|
### Phase 3: Ownership And Runtime Boundaries
|
||||||
|
|
||||||
|
- [x] Introduce `AsyncTaskManager` for background work.
|
||||||
|
- [x] Extract `DaemonController` from `App`.
|
||||||
|
- [x] Consolidate RPC auth and connection fallback behavior.
|
||||||
|
- [x] Add warning/TLS configuration for non-localhost RPC.
|
||||||
|
|
||||||
|
### Phase 4: Larger Maintainability Work
|
||||||
|
|
||||||
|
- [x] Extract `NetworkRefreshService` or `RefreshScheduler`.
|
||||||
|
- [x] Split `balance_tab.cpp`, `mining_tab.cpp`, and `console_tab.cpp` into smaller feature files where safe.
|
||||||
|
- [x] Move generated language headers into `build/generated`.
|
||||||
|
- [x] Add focused unit tests for connection config, URI parsing, amount formatting, wallet address spendability filtering, and scheduler behavior.
|
||||||
|
|
||||||
|
### Phase 5: Service Boundaries And Test Depth
|
||||||
|
|
||||||
|
- [x] Move wallet encryption, PIN, secure vault, key import/export, and restart flow state toward a `WalletSecurityController`.
|
||||||
|
- [x] Move more daemon restart/shutdown policy and external-daemon behavior from `App` into `DaemonController`.
|
||||||
|
- [x] Evolve `RefreshScheduler` into a fuller network refresh service by modeling named refresh jobs, in-flight state, and RPC queue pressure explicitly.
|
||||||
|
- [x] Split larger UI feature renderers after the low-risk helper extractions, starting with balance address/recent transaction panels and mining benchmark/pool panels.
|
||||||
|
- [x] Move settings/wizard static UI state into explicit state structs.
|
||||||
|
- [x] Add deeper tests around daemon/RPC service boundaries, scheduler edge cases, wallet security state transitions, and generated resource behavior.
|
||||||
|
- [x] Decide and document whether `src/config/version.h` is committed source or generated-only output.
|
||||||
|
|
||||||
|
### Phase 6: Deeper Service Extraction And Integration Tests
|
||||||
|
|
||||||
|
- [x] Move wallet-security RPC orchestration, secure-vault handoffs, key import/export state, and daemon restart sequencing behind service interfaces that can be exercised without constructing the full UI `App`.
|
||||||
|
- [x] Move daemon restart/rescan/bootstrap shutdown sequencing and external-daemon ownership policy into `DaemonController` with mockable collaborators.
|
||||||
|
- [x] Promote `NetworkRefreshService` from refresh policy/guard owner to dispatcher owner for named RPC jobs, queue-pressure telemetry, and stale-callback handling.
|
||||||
|
- [x] Split large UI files into stable feature modules, especially balance address-list/recent-transaction rendering, mining benchmark/pool rendering, and console output/input rendering.
|
||||||
|
- [x] Replace remaining compatibility aliases around extracted UI state with direct state-struct use.
|
||||||
|
- [x] Add integration tests with mock RPC/daemon collaborators for wallet security, daemon lifecycle, and refresh dispatch edges.
|
||||||
|
|
||||||
|
### Phase 7: Service-Owned Execution And UI Module Finish
|
||||||
|
|
||||||
|
- [x] Move async daemon lifecycle execution, restart delays, rescan flags, blockchain-data deletion, bootstrap stops, and external-daemon ownership enforcement deeper into `DaemonController` with mock daemon/filesystem/task collaborators.
|
||||||
|
- [x] Extract wallet decrypt/export/import dialog workflow state and secure-vault restart handoffs into a wallet-security workflow service that can be tested without constructing `App`.
|
||||||
|
- [x] Promote `NetworkRefreshService` from dispatch-ticket/telemetry owner to the enqueue/callback wrapper for named RPC refresh jobs, including stale callback suppression and queue-pressure reporting at the service boundary.
|
||||||
|
- [x] Continue splitting balance address-list rendering, mining benchmark controls, and console input/history/autocomplete into stable modules with tests.
|
||||||
|
- [x] Remove temporary App compatibility bridges around daemon ownership and any remaining extracted UI state once direct service/state use is complete.
|
||||||
|
|
||||||
|
### Phase 8: Workflow Executors And Large-View Reduction
|
||||||
|
|
||||||
|
- [x] Move wallet decrypt import/restart RPC orchestration out of `App`'s nested lambdas into a workflow executor with mock RPC/daemon/vault/file collaborators.
|
||||||
|
- [x] Split concrete daemon lifecycle runtime responsibilities into smaller adapters only where it lowers App coupling without adding indirection for its own sake.
|
||||||
|
- [x] Complete named refresh enqueue coverage for price/market refresh and connect-time one-off polling.
|
||||||
|
- [x] Continue feature-slicing large ImGui views by extracting address-row, mining benchmark/pool, and console command execution helper/model seams.
|
||||||
|
- [x] Add focused tests around the new workflow, refresh, daemon lifecycle, and UI helper seams.
|
||||||
|
|
||||||
|
### Phase 9: Typed Refresh Results And State Cleanup
|
||||||
|
|
||||||
|
- [x] Evolve refresh jobs toward typed result models and service-owned result application contracts for refresh paths that still require complex app-local snapshots.
|
||||||
|
- [x] Review remaining static UI state and compatibility glue, then move it to explicit state structs or document what is intentionally process-wide.
|
||||||
|
- [x] Reviewed draw-heavy ImGui bodies and deferred additional slicing to Phase 10 unless adjacent to a required behavior-preserving change.
|
||||||
|
- [x] Add focused tests around refresh result application, reconnect/stale-callback edges, and any state-struct migrations.
|
||||||
|
|
||||||
|
### Phase 10: Transaction Refresh And State Follow-Through
|
||||||
|
|
||||||
|
- [x] Evaluate typed address/transaction refresh result models and cache-update applicators for the refresh paths that still build large App-local snapshots.
|
||||||
|
- [x] Reduce decrypt workflow orchestration in `App` further only where executor-owned operations can preserve progress reporting and cancellation behavior; Phase 10 evaluated the remaining chain and deferred extra movement because the current App handoffs are the progress/cancellation boundary.
|
||||||
|
- [x] Convert remaining high-churn tab/dialog statics to explicit state structs as those views are touched, following `docs/ui-static-state.md`; Phase 10 did not touch those views for behavior changes.
|
||||||
|
- [x] Add stronger reconnect/stale-callback, remote/TLS RPC, and shutdown-cancellation tests.
|
||||||
|
|
||||||
|
### Phase 11: Workflow And UI State Hardening
|
||||||
|
|
||||||
|
- [x] Revisit decrypt restart/import orchestration for one complete executor-owned step if progress and cancellation can stay explicit.
|
||||||
|
- [x] Continue high-churn tab/dialog state-struct migrations only when those views are touched for real behavior changes.
|
||||||
|
- [x] Move more address/transaction RPC snapshot construction into testable helpers if it reduces App coupling without duplicating daemon call ordering.
|
||||||
|
- [x] Keep broadening refresh/RPC lifecycle tests around realistic worker callback ordering and reconnect races.
|
||||||
|
|
||||||
|
### Phase 12: Integration Fixtures And Remaining Orchestration Edges
|
||||||
|
|
||||||
|
- [x] Add shared mock RPC fixtures only if they let tests assert daemon call ordering directly instead of duplicating production sequencing in test setup.
|
||||||
|
- [x] Move more refresh body construction out of `App` only where call ordering remains obvious and covered by tests.
|
||||||
|
- [x] Revisit decrypt restart/import orchestration only if a complete progress/cancellation step can be executor-owned end to end.
|
||||||
|
- [x] Continue UI state-struct migrations opportunistically when behavior work touches those tabs/dialogs.
|
||||||
|
|
||||||
|
### Phase 13: Ordered Collectors And Lifecycle Coverage
|
||||||
|
|
||||||
|
- [x] Extend ordered mock RPC coverage to additional refresh collectors only when those collectors move beyond App-owned call bodies.
|
||||||
|
- [x] Keep refresh call ordering explicit in tests whenever daemon call sequencing moves into a service.
|
||||||
|
- [x] Broaden reconnect/shutdown lifecycle coverage around async refresh cancellation only when those flows are touched.
|
||||||
|
- [x] Continue decrypt and UI-state hardening opportunistically at complete, behavior-preserving seams.
|
||||||
|
|
||||||
|
### Phase 14: Mining And Connection Edge Review
|
||||||
|
|
||||||
|
- [x] Move mining refresh collection only if the fast/slow polling cadence remains explicit and covered by ordered mock RPC tests.
|
||||||
|
- [x] Move connection-init prefetch only if lock-screen timing and `getinfo`/`getwalletinfo` ordering remain obvious and tested.
|
||||||
|
- [x] Add lifecycle coverage only when changing worker callback, shutdown, reconnect, or daemon restart ownership.
|
||||||
|
- [x] Continue decrypt and UI-state work only when a complete behavior-preserving step is touched.
|
||||||
|
|
||||||
|
### Phase 15: Remaining App-Owned Edges
|
||||||
|
|
||||||
|
- [x] Review warmup status polling only if the UI status/progress handoff can stay explicit and covered.
|
||||||
|
- [x] Review price HTTP fetching only if network parsing, callback behavior, and failure handling become easier to test without hiding libcurl details.
|
||||||
|
- [x] Leave command-style RPC actions App-owned unless a complete behavior change creates a focused extraction boundary.
|
||||||
|
- [x] Add lifecycle, decrypt, or UI-state tests only when touching those behaviors directly.
|
||||||
|
|
||||||
|
### Phase 16: Price And Command Boundary Review
|
||||||
|
|
||||||
|
- [x] Revisit price HTTP fetching only if libcurl status/error behavior can be represented as a focused testable boundary.
|
||||||
|
- [x] Keep send, address creation, mining toggle, ban, and import/export command RPC actions App-owned unless a complete behavior change justifies extraction.
|
||||||
|
- [x] Add reconnect/shutdown lifecycle coverage only when callback ownership or cancellation behavior changes.
|
||||||
|
- [x] Continue decrypt and UI-state work only when directly touching those workflows for behavior.
|
||||||
|
|
||||||
|
### Phase 17: Command And Lifecycle Guardrails
|
||||||
|
|
||||||
|
- [x] Review command-style RPC actions only when a complete behavior change creates a focused extraction boundary.
|
||||||
|
- [x] Add reconnect/shutdown lifecycle coverage only when callback ownership, cancellation, shutdown, reconnect, or daemon restart behavior changes.
|
||||||
|
- [x] Continue decrypt and UI-state work only when directly touching those workflows for behavior.
|
||||||
|
- [x] Keep existing refresh and price boundaries stable unless new behavior creates a smaller tested seam.
|
||||||
|
|
||||||
|
### Phase 18: Feature-Driven Cleanup Only
|
||||||
|
|
||||||
|
- [x] Move command-style RPC actions only when a real behavior change supplies a focused extraction boundary and tests.
|
||||||
|
- [x] Add lifecycle tests only when callback ownership, cancellation, shutdown, reconnect, or daemon restart behavior changes.
|
||||||
|
- [x] Continue decrypt and UI-state cleanup only when those workflows are touched for behavior.
|
||||||
|
- [x] Preserve stable refresh and price boundaries unless new behavior makes a smaller tested seam useful.
|
||||||
|
|
||||||
|
### Phase 19: Feature-Scoped Maintenance
|
||||||
|
|
||||||
|
- [x] Prefer feature-scoped maintenance over additional extraction-only phases.
|
||||||
|
- [x] Move command-style RPC actions only with a complete behavior change and focused tests.
|
||||||
|
- [x] Add lifecycle/decrypt/UI-state coverage only when those behaviors change directly.
|
||||||
|
- [x] Preserve existing refresh and price service boundaries unless new behavior exposes a smaller tested seam.
|
||||||
|
|
||||||
|
### Phase 20: Maintenance Mode
|
||||||
|
|
||||||
|
- [x] Convert the cleanup roadmap from extraction phases to maintenance-mode guidance.
|
||||||
|
- [x] Keep command-style RPC actions App-owned unless a complete feature change supplies focused tests.
|
||||||
|
- [x] Add lifecycle/decrypt/UI-state coverage only when those behaviors change directly.
|
||||||
|
- [x] Preserve stable refresh and price service boundaries unless new behavior exposes a smaller tested seam.
|
||||||
|
|
||||||
|
### Phase 21: Sustained Maintenance Mode
|
||||||
|
|
||||||
|
- [x] Continue maintenance-mode cleanup only when concrete feature work creates a focused tested seam.
|
||||||
|
- [x] Keep command-style RPC actions App-owned unless a complete command workflow change supplies focused tests.
|
||||||
|
- [x] Add lifecycle/decrypt/UI-state coverage only when those behaviors change directly.
|
||||||
|
- [x] Preserve stable refresh and price service boundaries unless new behavior exposes a smaller tested seam.
|
||||||
|
|
||||||
|
### Phase 22: Maintenance Checkpoint
|
||||||
|
|
||||||
|
- [x] Continue sustained maintenance mode and avoid extraction-only phases unless concrete feature work creates a focused tested seam.
|
||||||
|
- [x] Keep command-style RPC actions App-owned unless a complete command workflow change supplies focused tests.
|
||||||
|
- [x] Add lifecycle/decrypt/UI-state coverage only when those behaviors change directly.
|
||||||
|
- [x] Preserve stable refresh and price service boundaries unless new behavior exposes a smaller tested seam.
|
||||||
|
|
||||||
|
### Phase 23: Maintenance Checkpoint
|
||||||
|
|
||||||
|
- [ ] Continue sustained maintenance mode and avoid extraction-only phases unless concrete feature work creates a focused tested seam.
|
||||||
|
- [ ] Keep command-style RPC actions App-owned unless a complete command workflow change supplies focused tests.
|
||||||
|
- [ ] Add lifecycle/decrypt/UI-state coverage only when those behaviors change directly.
|
||||||
|
- [ ] Preserve stable refresh and price service boundaries unless new behavior exposes a smaller tested seam.
|
||||||
|
|
||||||
|
## Residual Risk
|
||||||
|
|
||||||
|
Several findings are maintainability risks rather than confirmed user-facing bugs. Remote/plaintext RPC handling now warns and has a TLS config path, and Phase 10 added focused parsing tests, but real daemon TLS setups still deserve manual validation.
|
||||||
|
|
||||||
|
The safest next step is Phase 23: continue sustained maintenance mode, preserving tested boundaries and tying future cleanup to concrete feature work.
|
||||||
76
docs/codebase-overview.md
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
# Codebase Overview
|
||||||
|
|
||||||
|
Current as of 2026-04-27 for ObsidianDragon `1.2.0-rc1`.
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
ObsidianDragon is a Dear ImGui full-node wallet for DragonX (DRGX). It manages an embedded or external `dragonxd`, renders a schema-driven desktop UI, and provides shielded transactions, mining, market data, address management, explorer tools, and bootstrap download support.
|
||||||
|
|
||||||
|
## Runtime Architecture
|
||||||
|
|
||||||
|
- `src/main.cpp` initializes SDL3, graphics backends, ImGui, and the main loop.
|
||||||
|
- `src/app.cpp` owns application lifecycle, navigation, rendering, dialogs, and daemon/RPC startup.
|
||||||
|
- `src/app_network.cpp` contains refresh and transaction flows, including balance, address, transaction, mining, market, peer, and address metadata updates.
|
||||||
|
- `src/app_security.cpp` covers wallet encryption, PIN unlock, auto-lock, and secure vault integration.
|
||||||
|
- `src/app_wizard.cpp` handles first-run setup and bootstrap/encryption flow.
|
||||||
|
|
||||||
|
The UI thread renders ImGui and drains callback queues. RPC work runs through `RPCWorker`, which posts worker-thread RPC calls and returns UI-thread callbacks. Console commands can use a separate fast-lane RPC client/worker so they do not queue behind regular refresh batches.
|
||||||
|
|
||||||
|
`NetworkRefreshService` owns refresh timing, named job enqueue/callback guards, typed refresh result models, and service-owned result applicators for connection, core balance/sync, encryption, mining, peers, price, address snapshots, and transaction cache updates. Warmup polling, connection-init, core, mining, peer, address, and transaction refresh collection now run through a testable `RefreshRpcGateway`, and price HTTP response evaluation has a service-owned status/error/result model. `App` keeps enqueueing, UI timing decisions, cadence decisions, libcurl execution, and application handoff code for those paths.
|
||||||
|
|
||||||
|
## Source Map
|
||||||
|
|
||||||
|
| Path | Role |
|
||||||
|
|------|------|
|
||||||
|
| `src/config/` | Settings JSON persistence and generated `version.h` |
|
||||||
|
| `src/data/` | Wallet state, address book, exchange info |
|
||||||
|
| `src/rpc/` | libcurl JSON-RPC client, connection config, worker queue |
|
||||||
|
| `src/daemon/` | Embedded `dragonxd` manager and xmrig pool miner manager |
|
||||||
|
| `src/ui/windows/` | Main tabs and modal dialogs |
|
||||||
|
| `src/ui/pages/` | Page-style screens such as Settings |
|
||||||
|
| `src/ui/schema/` | TOML schema loader, skin manager, color resolver |
|
||||||
|
| `src/ui/material/` | Material-style typography, layout, drawing, components |
|
||||||
|
| `src/ui/effects/` | Acrylic, blur, noise, theme effects, low-spec fallback |
|
||||||
|
| `src/resources/` | Embedded resource extraction for params, daemon assets, themes, images |
|
||||||
|
| `src/platform/` | Windows DX11 and backdrop helpers |
|
||||||
|
| `src/util/` | i18n, logging, platform paths, bootstrap, vault, URI/base64/texture utilities |
|
||||||
|
|
||||||
|
## Build And Resources
|
||||||
|
|
||||||
|
- CMake uses C++17 and outputs `build/bin/ObsidianDragon`.
|
||||||
|
- Version comes from `project(... VERSION 1.2.0)` plus `DRAGONX_VERSION_SUFFIX=-rc1`.
|
||||||
|
- `src/config/version.h` is intentionally committed source for editor/release tooling compatibility, but CMake regenerates it from `src/config/version.h.in` during configure. Version bumps should update the CMake version/suffix and commit the regenerated header with the template if it changes.
|
||||||
|
- SDL3 is found from the system first, then fetched by CMake if unavailable.
|
||||||
|
- nlohmann/json and toml++ are fetched with CMake FetchContent.
|
||||||
|
- libcurl is system-provided on Linux/macOS and fetched statically for Windows.
|
||||||
|
- libsodium is system-provided on Linux or local under `libs/libsodium/`, `libs/libsodium-win/`, or `libs/libsodium-mac/`.
|
||||||
|
- Fonts are embedded with INCBIN: Ubuntu, Material Icons, a one-glyph MDI pickaxe subset, and Noto CJK subset.
|
||||||
|
- `res/themes/ui.toml` is embedded as a fallback and expanded into build themes with `scripts/expand_themes.py`.
|
||||||
|
- `res/default_banlist.txt` is embedded into `build/generated/default_banlist_embedded.h`.
|
||||||
|
|
||||||
|
## RPC And Daemon Notes
|
||||||
|
|
||||||
|
- Default RPC port is `21769`.
|
||||||
|
- `RPCClient` uses local HTTP with Basic auth. TLS is not assumed for localhost daemon RPC.
|
||||||
|
- DragonX daemon config paths:
|
||||||
|
- Linux: `~/.hush/DRAGONX/DRAGONX.conf`
|
||||||
|
- Windows: `%APPDATA%/Hush/DRAGONX/DRAGONX.conf`
|
||||||
|
- macOS: `~/Library/Application Support/Hush/DRAGONX/DRAGONX.conf`
|
||||||
|
- `Connection::autoDetectConfig()` creates missing config files, appends `exportdir`, `experimentalfeatures=1`, and `developerencryptwallet=1`, and falls back to `.cookie` auth if no `rpcpassword` is configured.
|
||||||
|
- The embedded daemon detects an external daemon on the RPC port and connects to it instead of taking ownership.
|
||||||
|
- Chain args include TLS-only mode, adaptive `-dbcache`, DragonX asset parameters, node seeds `node.dragonx.is` through `node4.dragonx.is`, and optional `-maxconnections=<n>` from Settings.
|
||||||
|
|
||||||
|
## UI And Data Notes
|
||||||
|
|
||||||
|
- Sidebar navigation is driven by `NavPage`: Overview, Send, Receive, History, Mining, Market, Console, Network, Explorer, Settings.
|
||||||
|
- Explorer lives in `src/ui/windows/explorer_tab.cpp`; Settings uses `src/ui/pages/settings_page.cpp`.
|
||||||
|
- Address labels, icons, favorites, hidden state, and manual ordering are persisted in Settings, especially `address_meta`.
|
||||||
|
- `AddressInfo::has_spending_key` tracks view-only shielded addresses; send flows filter or reject non-spendable z-addresses.
|
||||||
|
- The pickaxe icon is not a normal `ICON_MD_*` glyph. Use `AddressLabelDialog::drawIconByName()` or `Typography::pickaxeFontForSize()` for that special case.
|
||||||
|
- Remaining process-wide ImGui state and legacy compatibility wrappers are documented in `docs/ui-static-state.md`.
|
||||||
|
- Warmup, connection-init, core, mining, peer, address, and transaction refreshes use typed `NetworkRefreshService` result contracts plus gateway-backed collectors. Price refresh keeps libcurl setup/execution and callback ownership in App, while `NetworkRefreshService` owns JSON parsing plus HTTP status/error result evaluation. App still owns warmup status handoffs, mining fast/slow cadence, and command-style RPC actions. Transaction application updates `WalletState`, `viewtx_cache_`, `send_txids_`, the confirmed transaction cache, and block-height markers as one cache-update operation.
|
||||||
|
|
||||||
|
## Remaining Work
|
||||||
|
|
||||||
|
- Investigate `todo.md`: determine whether DragonX/Komodo wallet storage supports a safe compaction or consolidation workflow for wallets with too many addresses.
|
||||||
|
- Phase 23 should continue sustained maintenance mode: keep refresh and price boundaries stable, and revisit command/lifecycle/decrypt/UI-state seams only when concrete feature work creates a focused tested boundary.
|
||||||
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
17
docs/ui-static-state.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# UI Static State Policy
|
||||||
|
|
||||||
|
The wallet currently runs one `App` instance inside one ImGui context. Some UI state is still file-static because it represents process-wide tab or modal state that must persist across frames. Phase 9 reviewed these remaining cases and treats them as intentional until the owning view is next refactored.
|
||||||
|
|
||||||
|
## Intentional Process-Wide State
|
||||||
|
|
||||||
|
- Modal payload state in `src/ui/windows/*_dialog*` files owns one open dialog instance at a time. These statics are acceptable for singleton dialogs, but new async workflow state should live in a service or App-owned controller.
|
||||||
|
- Tab-local form, filter, and selection state in views such as Send, Receive, Market, Mining, Peers, Transactions, and Explorer persists user input across frames. When those views are touched for behavior changes, prefer an explicit `*TabState` struct over adding more independent statics.
|
||||||
|
- Material/theme/effect singletons hold process-wide rendering caches or current theme state. They should remain resettable only through their owning theme/effect APIs.
|
||||||
|
- Compatibility glue remains where external call sites still rely on legacy names: `App::setCurrentTab()`/`getCurrentTab()`, `WalletState` balance aliases, layout `k*` accessors, and icon helper wrappers. Remove these only after all call sites have moved to the newer names.
|
||||||
|
|
||||||
|
## Rules For New UI Code
|
||||||
|
|
||||||
|
- Add new mutable UI state to an existing explicit state struct when one exists.
|
||||||
|
- Use file-static state only for singleton UI surfaces that cannot have multiple instances in the current app model.
|
||||||
|
- Keep long-running workflow progress outside render functions and expose it through services or App-owned controllers.
|
||||||
|
- Document any new compatibility wrapper with the call sites it protects and delete it when the migration is complete.
|
||||||
@@ -40,7 +40,7 @@ extern "C" {
|
|||||||
* read-only segment just like a normal const array would.
|
* read-only segment just like a normal const array would.
|
||||||
*/
|
*/
|
||||||
#if defined(__APPLE__)
|
#if defined(__APPLE__)
|
||||||
# define INCBIN_SECTION ".const_data"
|
# define INCBIN_SECTION "__DATA,__const"
|
||||||
# define INCBIN_MANGLE "_"
|
# define INCBIN_MANGLE "_"
|
||||||
#else
|
#else
|
||||||
# define INCBIN_SECTION ".rodata"
|
# define INCBIN_SECTION ".rodata"
|
||||||
|
|||||||
51
res/ObsidianDragon.manifest
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||||
|
|
||||||
|
<!-- Application identity —————————————————————————————— -->
|
||||||
|
<assemblyIdentity
|
||||||
|
type="win32"
|
||||||
|
name="DragonX.ObsidianDragon.Wallet"
|
||||||
|
version="1.2.0.0"
|
||||||
|
processorArchitecture="amd64"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<description>ObsidianDragon Wallet</description>
|
||||||
|
|
||||||
|
<!-- Common Controls v6 (themed buttons, etc.) ————————— -->
|
||||||
|
<dependency>
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity
|
||||||
|
type="win32"
|
||||||
|
name="Microsoft.Windows.Common-Controls"
|
||||||
|
version="6.0.0.0"
|
||||||
|
processorArchitecture="*"
|
||||||
|
publicKeyToken="6595b64144ccf1df"
|
||||||
|
language="*"
|
||||||
|
/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- DPI awareness (Per-Monitor V2) ————————————————————— -->
|
||||||
|
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||||
|
<windowsSettings>
|
||||||
|
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
|
||||||
|
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2,PerMonitor</dpiAwareness>
|
||||||
|
<activeCodePage xmlns="http://schemas.microsoft.com/SMI/2019/WindowsSettings">UTF-8</activeCodePage>
|
||||||
|
</windowsSettings>
|
||||||
|
</application>
|
||||||
|
|
||||||
|
<!-- Supported OS declarations (Windows 7 → 11) ———————— -->
|
||||||
|
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||||
|
<application>
|
||||||
|
<!-- Windows 7 -->
|
||||||
|
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
|
||||||
|
<!-- Windows 8 -->
|
||||||
|
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
|
||||||
|
<!-- Windows 8.1 -->
|
||||||
|
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
|
||||||
|
<!-- Windows 10 / 11 -->
|
||||||
|
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
|
||||||
|
</application>
|
||||||
|
</compatibility>
|
||||||
|
|
||||||
|
</assembly>
|
||||||
51
res/ObsidianDragon.manifest.in
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||||
|
|
||||||
|
<!-- Application identity —————————————————————————————— -->
|
||||||
|
<assemblyIdentity
|
||||||
|
type="win32"
|
||||||
|
name="DragonX.ObsidianDragon.Wallet"
|
||||||
|
version="@PROJECT_VERSION_MAJOR@.@PROJECT_VERSION_MINOR@.@PROJECT_VERSION_PATCH@.0"
|
||||||
|
processorArchitecture="amd64"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<description>ObsidianDragon Wallet</description>
|
||||||
|
|
||||||
|
<!-- Common Controls v6 (themed buttons, etc.) ————————— -->
|
||||||
|
<dependency>
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity
|
||||||
|
type="win32"
|
||||||
|
name="Microsoft.Windows.Common-Controls"
|
||||||
|
version="6.0.0.0"
|
||||||
|
processorArchitecture="*"
|
||||||
|
publicKeyToken="6595b64144ccf1df"
|
||||||
|
language="*"
|
||||||
|
/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- DPI awareness (Per-Monitor V2) ————————————————————— -->
|
||||||
|
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||||
|
<windowsSettings>
|
||||||
|
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
|
||||||
|
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2,PerMonitor</dpiAwareness>
|
||||||
|
<activeCodePage xmlns="http://schemas.microsoft.com/SMI/2019/WindowsSettings">UTF-8</activeCodePage>
|
||||||
|
</windowsSettings>
|
||||||
|
</application>
|
||||||
|
|
||||||
|
<!-- Supported OS declarations (Windows 7 → 11) ———————— -->
|
||||||
|
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||||
|
<application>
|
||||||
|
<!-- Windows 7 -->
|
||||||
|
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
|
||||||
|
<!-- Windows 8 -->
|
||||||
|
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
|
||||||
|
<!-- Windows 8.1 -->
|
||||||
|
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
|
||||||
|
<!-- Windows 10 / 11 -->
|
||||||
|
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
|
||||||
|
</application>
|
||||||
|
</compatibility>
|
||||||
|
|
||||||
|
</assembly>
|
||||||
@@ -4,6 +4,13 @@
|
|||||||
// Use numeric ordinal 1 so LoadIcon(hInst, MAKEINTRESOURCE(1)) finds it.
|
// Use numeric ordinal 1 so LoadIcon(hInst, MAKEINTRESOURCE(1)) finds it.
|
||||||
1 ICON "@OBSIDIAN_ICO_PATH@"
|
1 ICON "@OBSIDIAN_ICO_PATH@"
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Application Manifest — declares DPI awareness, common controls v6,
|
||||||
|
// UTF-8 code page, and application identity. Without this, Windows may
|
||||||
|
// fall back to legacy process grouping in Task Manager.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
1 24 "@OBSIDIAN_MANIFEST_PATH@"
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// VERSIONINFO — sets the description shown in Task Manager, Explorer
|
// VERSIONINFO — sets the description shown in Task Manager, Explorer
|
||||||
// "Details" tab, and other Windows tools. Without this, MinGW-w64
|
// "Details" tab, and other Windows tools. Without this, MinGW-w64
|
||||||
@@ -12,8 +19,8 @@
|
|||||||
#include <winver.h>
|
#include <winver.h>
|
||||||
|
|
||||||
VS_VERSION_INFO VERSIONINFO
|
VS_VERSION_INFO VERSIONINFO
|
||||||
FILEVERSION @DRAGONX_VER_MAJOR@,@DRAGONX_VER_MINOR@,@DRAGONX_VER_PATCH@,0
|
FILEVERSION @PROJECT_VERSION_MAJOR@,@PROJECT_VERSION_MINOR@,@PROJECT_VERSION_PATCH@,0
|
||||||
PRODUCTVERSION @DRAGONX_VER_MAJOR@,@DRAGONX_VER_MINOR@,@DRAGONX_VER_PATCH@,0
|
PRODUCTVERSION @PROJECT_VERSION_MAJOR@,@PROJECT_VERSION_MINOR@,@PROJECT_VERSION_PATCH@,0
|
||||||
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
|
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
|
||||||
FILEFLAGS 0x0L
|
FILEFLAGS 0x0L
|
||||||
FILEOS VOS_NT_WINDOWS32
|
FILEOS VOS_NT_WINDOWS32
|
||||||
@@ -24,14 +31,14 @@ BEGIN
|
|||||||
BEGIN
|
BEGIN
|
||||||
BLOCK "040904B0" // US-English, Unicode
|
BLOCK "040904B0" // US-English, Unicode
|
||||||
BEGIN
|
BEGIN
|
||||||
VALUE "CompanyName", "The Hush Developers\0"
|
VALUE "CompanyName", "DragonX Developers\0"
|
||||||
VALUE "FileDescription", "ObsidianDragon Wallet\0"
|
VALUE "FileDescription", "ObsidianDragon Wallet\0"
|
||||||
VALUE "FileVersion", "@DRAGONX_VERSION@\0"
|
VALUE "FileVersion", "@PROJECT_VERSION@\0"
|
||||||
VALUE "InternalName", "ObsidianDragon\0"
|
VALUE "InternalName", "ObsidianDragon\0"
|
||||||
VALUE "LegalCopyright", "Copyright 2024-2026 The Hush Developers. GPLv3.\0"
|
VALUE "LegalCopyright", "Copyright 2024-2026 DragonX Developers. GPLv3.\0"
|
||||||
VALUE "OriginalFilename", "ObsidianDragon.exe\0"
|
VALUE "OriginalFilename", "ObsidianDragon.exe\0"
|
||||||
VALUE "ProductName", "ObsidianDragon\0"
|
VALUE "ProductName", "ObsidianDragon\0"
|
||||||
VALUE "ProductVersion", "@DRAGONX_VERSION@\0"
|
VALUE "ProductVersion", "@PROJECT_VERSION@\0"
|
||||||
END
|
END
|
||||||
END
|
END
|
||||||
BLOCK "VarFileInfo"
|
BLOCK "VarFileInfo"
|
||||||
|
|||||||
10
res/default_banlist.txt
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# Default Ban List — DragonX Wallet
|
||||||
|
# IPs listed here are banned automatically when the wallet connects.
|
||||||
|
# One IP or subnet per line. Comments start with #. Blank lines are ignored.
|
||||||
|
#
|
||||||
|
# Examples:
|
||||||
|
# 192.168.1.100
|
||||||
|
# 10.0.0.0/8
|
||||||
|
# 203.0.113.42
|
||||||
|
#
|
||||||
|
# Rebuild the wallet after editing this file.
|
||||||
BIN
res/fonts/MaterialDesignIcons-Pickaxe-Subset.ttf
Normal file
BIN
res/fonts/NotoSansCJK-Subset.otf
Normal file
BIN
res/fonts/NotoSansCJK-Subset.ttf
Normal file
BIN
res/img/ObsidianDragon.icns
Normal file
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 201 KiB |
|
Before Width: | Height: | Size: 129 KiB After Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 556 KiB After Width: | Height: | Size: 370 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 15 KiB |
1040
res/lang/de.json
1064
res/lang/es.json
1064
res/lang/fr.json
1066
res/lang/ja.json
1064
res/lang/ko.json
1064
res/lang/pt.json
1064
res/lang/ru.json
1040
res/lang/zh.json
@@ -2,7 +2,7 @@
|
|||||||
name = "Color Pop Dark"
|
name = "Color Pop Dark"
|
||||||
author = "The Hush Developers"
|
author = "The Hush Developers"
|
||||||
dark = true
|
dark = true
|
||||||
elevation = { --elevation-0 = "#121218", --elevation-1 = "#1C1C24", --elevation-2 = "#26262E", --elevation-3 = "#303038", --elevation-4 = "#3A3A44" }
|
elevation = { --elevation-0 = "#121218", --elevation-1 = "#1C1C24", --elevation-2 = "#282836", --elevation-3 = "#303038", --elevation-4 = "#3A3A44" }
|
||||||
images = { background_image = "backgrounds/texture/pop-dark_bg.png", logo = "logos/logo_ObsidianDragon_dark.png" }
|
images = { background_image = "backgrounds/texture/pop-dark_bg.png", logo = "logos/logo_ObsidianDragon_dark.png" }
|
||||||
|
|
||||||
[theme.palette]
|
[theme.palette]
|
||||||
@@ -14,7 +14,7 @@ images = { background_image = "backgrounds/texture/pop-dark_bg.png", logo = "log
|
|||||||
--secondary-light = "#FF9CDC"
|
--secondary-light = "#FF9CDC"
|
||||||
--background = "#0E0E14"
|
--background = "#0E0E14"
|
||||||
--surface = "#161620"
|
--surface = "#161620"
|
||||||
--surface-variant = "#20202C"
|
--surface-variant = "#282836"
|
||||||
--on-primary = "#FFFFFF"
|
--on-primary = "#FFFFFF"
|
||||||
--on-secondary = "#FFFFFF"
|
--on-secondary = "#FFFFFF"
|
||||||
--on-background = "#E8E6F0"
|
--on-background = "#E8E6F0"
|
||||||
@@ -35,7 +35,7 @@ images = { background_image = "backgrounds/texture/pop-dark_bg.png", logo = "log
|
|||||||
--surface-active = "rgba(200,190,240,0.10)"
|
--surface-active = "rgba(200,190,240,0.10)"
|
||||||
--glass-button = "rgba(124,108,255,0.08)"
|
--glass-button = "rgba(124,108,255,0.08)"
|
||||||
--glass-button-hover = "rgba(124,108,255,0.16)"
|
--glass-button-hover = "rgba(124,108,255,0.16)"
|
||||||
--card-border = "rgba(200,190,240,0.10)"
|
--card-border = "rgba(200,190,240,0.24)"
|
||||||
--text-shadow = "rgba(0,0,0,0.45)"
|
--text-shadow = "rgba(0,0,0,0.45)"
|
||||||
--input-overlay-text = "rgba(232,230,240,0.25)"
|
--input-overlay-text = "rgba(232,230,240,0.25)"
|
||||||
--slider-text = "rgba(232,230,240,0.82)"
|
--slider-text = "rgba(232,230,240,0.82)"
|
||||||
@@ -48,13 +48,13 @@ images = { background_image = "backgrounds/texture/pop-dark_bg.png", logo = "log
|
|||||||
--tooltip-bg = "rgba(14,14,22,0.94)"
|
--tooltip-bg = "rgba(14,14,22,0.94)"
|
||||||
--tooltip-border = "rgba(124,108,255,0.18)"
|
--tooltip-border = "rgba(124,108,255,0.18)"
|
||||||
--glass-fill = "rgba(200,190,240,0.06)"
|
--glass-fill = "rgba(200,190,240,0.06)"
|
||||||
--glass-border = "rgba(200,190,240,0.10)"
|
--glass-border = "rgba(124,108,255,0.28)"
|
||||||
--glass-noise-tint = "rgba(124,108,255,0.03)"
|
--glass-noise-tint = "rgba(124,108,255,0.03)"
|
||||||
--tactile-top = "rgba(200,190,240,0.07)"
|
--tactile-top = "rgba(200,190,240,0.07)"
|
||||||
--tactile-bottom = "rgba(200,190,240,0.0)"
|
--tactile-bottom = "rgba(200,190,240,0.0)"
|
||||||
--hover-overlay = "rgba(124,108,255,0.05)"
|
--hover-overlay = "rgba(124,108,255,0.05)"
|
||||||
--active-overlay = "rgba(124,108,255,0.10)"
|
--active-overlay = "rgba(124,108,255,0.10)"
|
||||||
--rim-light = "rgba(124,108,255,0.10)"
|
--rim-light = "rgba(124,108,255,0.16)"
|
||||||
--status-divider = "rgba(200,190,240,0.06)"
|
--status-divider = "rgba(200,190,240,0.06)"
|
||||||
--sidebar-hover = "rgba(124,108,255,0.10)"
|
--sidebar-hover = "rgba(124,108,255,0.10)"
|
||||||
--sidebar-icon = "rgba(232,230,240,0.45)"
|
--sidebar-icon = "rgba(232,230,240,0.45)"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
name = "Dark"
|
name = "Dark"
|
||||||
author = "The Hush Developers"
|
author = "The Hush Developers"
|
||||||
dark = true
|
dark = true
|
||||||
elevation = { --elevation-0 = "#161618", --elevation-1 = "#222224", --elevation-2 = "#2C2C2E", --elevation-3 = "#363638", --elevation-4 = "#404044" }
|
elevation = { --elevation-0 = "#161618", --elevation-1 = "#222224", --elevation-2 = "#2E2E32", --elevation-3 = "#363638", --elevation-4 = "#404044" }
|
||||||
images = { background_image = "backgrounds/texture/dark_bg.png", logo = "logos/logo_ObsidianDragon_dark.png" }
|
images = { background_image = "backgrounds/texture/dark_bg.png", logo = "logos/logo_ObsidianDragon_dark.png" }
|
||||||
|
|
||||||
[theme.palette]
|
[theme.palette]
|
||||||
@@ -14,7 +14,7 @@ images = { background_image = "backgrounds/texture/dark_bg.png", logo = "logos/l
|
|||||||
--secondary-light = "#9DC5BE"
|
--secondary-light = "#9DC5BE"
|
||||||
--background = "#141416"
|
--background = "#141416"
|
||||||
--surface = "#1A1A1C"
|
--surface = "#1A1A1C"
|
||||||
--surface-variant = "#262628"
|
--surface-variant = "#2E2E32"
|
||||||
--on-primary = "#000000"
|
--on-primary = "#000000"
|
||||||
--on-secondary = "#000000"
|
--on-secondary = "#000000"
|
||||||
--on-background = "#D0D0D4"
|
--on-background = "#D0D0D4"
|
||||||
@@ -35,7 +35,7 @@ images = { background_image = "backgrounds/texture/dark_bg.png", logo = "logos/l
|
|||||||
--surface-active = "rgba(220,220,225,0.08)"
|
--surface-active = "rgba(220,220,225,0.08)"
|
||||||
--glass-button = "rgba(220,220,225,0.05)"
|
--glass-button = "rgba(220,220,225,0.05)"
|
||||||
--glass-button-hover = "rgba(220,220,225,0.10)"
|
--glass-button-hover = "rgba(220,220,225,0.10)"
|
||||||
--card-border = "rgba(220,220,225,0.12)"
|
--card-border = "rgba(220,220,225,0.24)"
|
||||||
--text-shadow = "rgba(0,0,0,0.40)"
|
--text-shadow = "rgba(0,0,0,0.40)"
|
||||||
--input-overlay-text = "rgba(208,208,212,0.28)"
|
--input-overlay-text = "rgba(208,208,212,0.28)"
|
||||||
--slider-text = "rgba(208,208,212,0.85)"
|
--slider-text = "rgba(208,208,212,0.85)"
|
||||||
@@ -48,13 +48,13 @@ images = { background_image = "backgrounds/texture/dark_bg.png", logo = "logos/l
|
|||||||
--tooltip-bg = "rgba(18,18,22,0.92)"
|
--tooltip-bg = "rgba(18,18,22,0.92)"
|
||||||
--tooltip-border = "rgba(220,220,225,0.10)"
|
--tooltip-border = "rgba(220,220,225,0.10)"
|
||||||
--glass-fill = "rgba(220,220,225,0.07)"
|
--glass-fill = "rgba(220,220,225,0.07)"
|
||||||
--glass-border = "rgba(220,220,225,0.12)"
|
--glass-border = "rgba(154,175,200,0.28)"
|
||||||
--glass-noise-tint = "rgba(220,220,225,0.03)"
|
--glass-noise-tint = "rgba(220,220,225,0.03)"
|
||||||
--tactile-top = "rgba(220,220,225,0.06)"
|
--tactile-top = "rgba(220,220,225,0.06)"
|
||||||
--tactile-bottom = "rgba(220,220,225,0.0)"
|
--tactile-bottom = "rgba(220,220,225,0.0)"
|
||||||
--hover-overlay = "rgba(220,220,225,0.04)"
|
--hover-overlay = "rgba(220,220,225,0.04)"
|
||||||
--active-overlay = "rgba(220,220,225,0.08)"
|
--active-overlay = "rgba(220,220,225,0.08)"
|
||||||
--rim-light = "rgba(220,220,225,0.08)"
|
--rim-light = "rgba(220,220,225,0.14)"
|
||||||
--status-divider = "rgba(220,220,225,0.06)"
|
--status-divider = "rgba(220,220,225,0.06)"
|
||||||
--sidebar-hover = "rgba(220,220,225,0.08)"
|
--sidebar-hover = "rgba(220,220,225,0.08)"
|
||||||
--sidebar-icon = "rgba(220,220,225,0.40)"
|
--sidebar-icon = "rgba(220,220,225,0.40)"
|
||||||
|
|||||||
@@ -2,19 +2,19 @@
|
|||||||
name = "Obsidian"
|
name = "Obsidian"
|
||||||
author = "The Hush Developers"
|
author = "The Hush Developers"
|
||||||
dark = true
|
dark = true
|
||||||
elevation = { --elevation-0 = "#0E0B14", --elevation-1 = "#17121E", --elevation-2 = "#1C1625", --elevation-3 = "#211A2C", --elevation-4 = "#261E33" }
|
elevation = { --elevation-0 = "#0E0B14", --elevation-1 = "#17121E", --elevation-2 = "#252030", --elevation-3 = "#2D2838", --elevation-4 = "#353040" }
|
||||||
images = { background_image = "backgrounds/texture/obsidian_bg.png", logo = "logos/logo_ObsidianDragon_dark.png" }
|
images = { background_image = "backgrounds/texture/obsidian_bg.png", logo = "logos/logo_ObsidianDragon_dark.png" }
|
||||||
|
|
||||||
[theme.palette]
|
[theme.palette]
|
||||||
--primary = "#AB47BC"
|
--primary = "#9A6BA8"
|
||||||
--primary-variant = "#8E24AA"
|
--primary-variant = "#7A4888"
|
||||||
--primary-light = "#CE93D8"
|
--primary-light = "#C5A8CC"
|
||||||
--secondary = "#B388FF"
|
--secondary = "#A898D8"
|
||||||
--secondary-variant = "#7C4DFF"
|
--secondary-variant = "#8074C8"
|
||||||
--secondary-light = "#D1C4E9"
|
--secondary-light = "#CCC4D9"
|
||||||
--background = "#0A0810"
|
--background = "#0A0810"
|
||||||
--surface = "#110E18"
|
--surface = "#110E18"
|
||||||
--surface-variant = "#1C1625"
|
--surface-variant = "#252030"
|
||||||
--on-primary = "#FFFFFF"
|
--on-primary = "#FFFFFF"
|
||||||
--on-secondary = "#000000"
|
--on-secondary = "#000000"
|
||||||
--on-background = "#E8E0F0"
|
--on-background = "#E8E0F0"
|
||||||
@@ -35,7 +35,7 @@ images = { background_image = "backgrounds/texture/obsidian_bg.png", logo = "log
|
|||||||
--surface-active = "rgba(200,180,255,0.10)"
|
--surface-active = "rgba(200,180,255,0.10)"
|
||||||
--glass-button = "rgba(200,180,255,0.06)"
|
--glass-button = "rgba(200,180,255,0.06)"
|
||||||
--glass-button-hover = "rgba(200,180,255,0.12)"
|
--glass-button-hover = "rgba(200,180,255,0.12)"
|
||||||
--card-border = "rgba(200,180,255,0.14)"
|
--card-border = "rgba(200,180,255,0.26)"
|
||||||
--text-shadow = "rgba(0,0,0,0.50)"
|
--text-shadow = "rgba(0,0,0,0.50)"
|
||||||
--input-overlay-text = "rgba(232,224,240,0.30)"
|
--input-overlay-text = "rgba(232,224,240,0.30)"
|
||||||
--slider-text = "rgba(232,224,240,0.85)"
|
--slider-text = "rgba(232,224,240,0.85)"
|
||||||
@@ -48,13 +48,13 @@ images = { background_image = "backgrounds/texture/obsidian_bg.png", logo = "log
|
|||||||
--tooltip-bg = "rgba(14,11,20,0.92)"
|
--tooltip-bg = "rgba(14,11,20,0.92)"
|
||||||
--tooltip-border = "rgba(200,180,255,0.12)"
|
--tooltip-border = "rgba(200,180,255,0.12)"
|
||||||
--glass-fill = "rgba(200,180,255,0.08)"
|
--glass-fill = "rgba(200,180,255,0.08)"
|
||||||
--glass-border = "rgba(200,180,255,0.14)"
|
--glass-border = "rgba(154,107,168,0.30)"
|
||||||
--glass-noise-tint = "rgba(200,180,255,0.03)"
|
--glass-noise-tint = "rgba(200,180,255,0.03)"
|
||||||
--tactile-top = "rgba(200,180,255,0.06)"
|
--tactile-top = "rgba(200,180,255,0.06)"
|
||||||
--tactile-bottom = "rgba(200,180,255,0.0)"
|
--tactile-bottom = "rgba(200,180,255,0.0)"
|
||||||
--hover-overlay = "rgba(200,180,255,0.05)"
|
--hover-overlay = "rgba(200,180,255,0.05)"
|
||||||
--active-overlay = "rgba(200,180,255,0.10)"
|
--active-overlay = "rgba(200,180,255,0.10)"
|
||||||
--rim-light = "rgba(200,180,255,0.08)"
|
--rim-light = "rgba(200,180,255,0.14)"
|
||||||
--status-divider = "rgba(200,180,255,0.08)"
|
--status-divider = "rgba(200,180,255,0.08)"
|
||||||
--sidebar-hover = "rgba(200,180,255,0.10)"
|
--sidebar-hover = "rgba(200,180,255,0.10)"
|
||||||
--sidebar-icon = "rgba(200,180,255,0.42)"
|
--sidebar-icon = "rgba(200,180,255,0.42)"
|
||||||
@@ -65,19 +65,19 @@ images = { background_image = "backgrounds/texture/obsidian_bg.png", logo = "log
|
|||||||
--window-control-hover = "rgba(200,180,255,0.12)"
|
--window-control-hover = "rgba(200,180,255,0.12)"
|
||||||
--window-close-hover = "rgba(232,17,35,0.78)"
|
--window-close-hover = "rgba(232,17,35,0.78)"
|
||||||
--spinner-track = "rgba(200,180,255,0.10)"
|
--spinner-track = "rgba(200,180,255,0.10)"
|
||||||
--spinner-active = "rgba(179,136,255,0.85)"
|
--spinner-active = "rgba(168,152,216,0.85)"
|
||||||
--shutdown-panel-bg = "rgba(10,8,16,0.90)"
|
--shutdown-panel-bg = "rgba(10,8,16,0.90)"
|
||||||
--shutdown-panel-border = "rgba(200,180,255,0.07)"
|
--shutdown-panel-border = "rgba(200,180,255,0.07)"
|
||||||
--ram-bar-app = "#AB47BC"
|
--ram-bar-app = "#9A6BA8"
|
||||||
--ram-bar-system = "rgba(255,255,255,0.18)"
|
--ram-bar-system = "rgba(255,255,255,0.18)"
|
||||||
--accent-total = "#CE93D8"
|
--accent-total = "#C5A8CC"
|
||||||
--accent-shielded = "#80CBC4"
|
--accent-shielded = "#8AB8B4"
|
||||||
--accent-transparent = "#FFAB91"
|
--accent-transparent = "#D8B0A0"
|
||||||
--accent-action = "#AB47BC"
|
--accent-action = "#9A6BA8"
|
||||||
--accent-market = "#80CBC4"
|
--accent-market = "#8AB8B4"
|
||||||
--accent-portfolio = "#B388FF"
|
--accent-portfolio = "#A898D8"
|
||||||
--toast-info-accent = "#AB47BC"
|
--toast-info-accent = "#9A6BA8"
|
||||||
--toast-info-text = "#CE93D8"
|
--toast-info-text = "#C5A8CC"
|
||||||
--toast-success-accent = "rgba(50,180,80,1.0)"
|
--toast-success-accent = "rgba(50,180,80,1.0)"
|
||||||
--toast-success-text = "rgba(180,255,180,1.0)"
|
--toast-success-text = "rgba(180,255,180,1.0)"
|
||||||
--toast-warning-accent = "rgba(204,166,50,1.0)"
|
--toast-warning-accent = "rgba(204,166,50,1.0)"
|
||||||
@@ -86,10 +86,10 @@ images = { background_image = "backgrounds/texture/obsidian_bg.png", logo = "log
|
|||||||
--toast-error-text = "rgba(255,153,153,1.0)"
|
--toast-error-text = "rgba(255,153,153,1.0)"
|
||||||
--snackbar-bg = "rgba(40,35,55,0.95)"
|
--snackbar-bg = "rgba(40,35,55,0.95)"
|
||||||
--snackbar-text = "rgba(232,224,240,0.87)"
|
--snackbar-text = "rgba(232,224,240,0.87)"
|
||||||
--snackbar-action = "rgba(179,136,255,1.0)"
|
--snackbar-action = "rgba(168,152,216,1.0)"
|
||||||
--snackbar-action-hover = "rgba(206,147,216,1.0)"
|
--snackbar-action-hover = "rgba(197,168,204,1.0)"
|
||||||
--switch-track-off = "rgba(200,180,255,0.12)"
|
--switch-track-off = "rgba(200,180,255,0.12)"
|
||||||
--switch-track-on = "rgba(171,71,188,0.50)"
|
--switch-track-on = "rgba(154,107,168,0.50)"
|
||||||
--switch-thumb-off = "#B0A0C0"
|
--switch-thumb-off = "#B0A0C0"
|
||||||
--switch-thumb-on = "#E8E0F0"
|
--switch-thumb-on = "#E8E0F0"
|
||||||
--control-shadow = "rgba(0,0,0,0.24)"
|
--control-shadow = "rgba(0,0,0,0.24)"
|
||||||
@@ -144,18 +144,18 @@ gradient-border-enabled = { size = 1.0 }
|
|||||||
gradient-border-speed = { size = 0.12 }
|
gradient-border-speed = { size = 0.12 }
|
||||||
gradient-border-thickness = { size = 1.5 }
|
gradient-border-thickness = { size = 1.5 }
|
||||||
gradient-border-alpha = { size = 0.55 }
|
gradient-border-alpha = { size = 0.55 }
|
||||||
gradient-border-color-a = { color = "#CE93D8" }
|
gradient-border-color-a = { color = "#C5A8CC" }
|
||||||
gradient-border-color-b = { color = "#3F51B5" }
|
gradient-border-color-b = { color = "#6878A8" }
|
||||||
|
|
||||||
ember-rise-enabled = { size = 0.0 }
|
ember-rise-enabled = { size = 0.0 }
|
||||||
|
|
||||||
# Shader-like viewport overlay — deep indigo crystal atmosphere
|
# Shader-like viewport overlay — deep indigo crystal atmosphere
|
||||||
viewport-wash-enabled = { size = 1.0 }
|
viewport-wash-enabled = { size = 1.0 }
|
||||||
viewport-wash-alpha = { size = 0.05 }
|
viewport-wash-alpha = { size = 0.05 }
|
||||||
viewport-wash-tl = { color = "#4A148C" }
|
viewport-wash-tl = { color = "#3A2860" }
|
||||||
viewport-wash-tr = { color = "#1A237E" }
|
viewport-wash-tr = { color = "#2A3058" }
|
||||||
viewport-wash-bl = { color = "#311B92" }
|
viewport-wash-bl = { color = "#302860" }
|
||||||
viewport-wash-br = { color = "#6A1B9A" }
|
viewport-wash-br = { color = "#4A3068" }
|
||||||
viewport-wash-rotate = { size = 0.015 }
|
viewport-wash-rotate = { size = 0.015 }
|
||||||
viewport-wash-pulse = { size = 0.0 }
|
viewport-wash-pulse = { size = 0.0 }
|
||||||
viewport-wash-pulse-depth = { size = 0.0 }
|
viewport-wash-pulse-depth = { size = 0.0 }
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ spacing-tokens = { xs = 2.0, sm = 4.0, md = 8.0, lg = 12.0, xl = 16.0, xxl = 24.
|
|||||||
name = "DragonX"
|
name = "DragonX"
|
||||||
author = "DanS"
|
author = "DanS"
|
||||||
dark = true
|
dark = true
|
||||||
elevation = { --elevation-0 = "#120A08", --elevation-1 = "#1A0F0C", --elevation-2 = "#201410", --elevation-3 = "#261914", --elevation-4 = "#2C1E18" }
|
elevation = { --elevation-0 = "#120A08", --elevation-1 = "#1A0F0C", --elevation-2 = "#281C16", --elevation-3 = "#30241C", --elevation-4 = "#382C22" }
|
||||||
images = { background_image = "backgrounds/texture/drgx_bg.png", logo = "logos/logo_ObsidianDragon_dark.png" }
|
images = { background_image = "backgrounds/texture/drgx_bg.png", logo = "logos/logo_ObsidianDragon_dark.png" }
|
||||||
|
|
||||||
[theme.palette]
|
[theme.palette]
|
||||||
@@ -32,7 +32,7 @@ images = { background_image = "backgrounds/texture/drgx_bg.png", logo = "logos/l
|
|||||||
--secondary-light = "#FF9E40"
|
--secondary-light = "#FF9E40"
|
||||||
--background = "#0C0606"
|
--background = "#0C0606"
|
||||||
--surface = "#120A08"
|
--surface = "#120A08"
|
||||||
--surface-variant = "#201410"
|
--surface-variant = "#281C16"
|
||||||
--on-primary = "#FFFFFF"
|
--on-primary = "#FFFFFF"
|
||||||
--on-secondary = "#000000"
|
--on-secondary = "#000000"
|
||||||
--on-background = "#F0E0D8"
|
--on-background = "#F0E0D8"
|
||||||
@@ -66,13 +66,13 @@ images = { background_image = "backgrounds/texture/drgx_bg.png", logo = "logos/l
|
|||||||
--tooltip-bg = "rgba(12,8,6,0.92)"
|
--tooltip-bg = "rgba(12,8,6,0.92)"
|
||||||
--tooltip-border = "rgba(255,180,140,0.12)"
|
--tooltip-border = "rgba(255,180,140,0.12)"
|
||||||
--glass-fill = "rgba(255,180,140,0.08)"
|
--glass-fill = "rgba(255,180,140,0.08)"
|
||||||
--glass-border = "rgba(255,180,140,0.13)"
|
--glass-border = "rgba(211,47,47,0.26)"
|
||||||
--glass-noise-tint = "rgba(255,180,140,0.03)"
|
--glass-noise-tint = "rgba(255,180,140,0.03)"
|
||||||
--tactile-top = "rgba(255,180,140,0.06)"
|
--tactile-top = "rgba(255,180,140,0.06)"
|
||||||
--tactile-bottom = "rgba(255,180,140,0.0)"
|
--tactile-bottom = "rgba(255,180,140,0.0)"
|
||||||
--hover-overlay = "rgba(255,180,140,0.05)"
|
--hover-overlay = "rgba(255,180,140,0.05)"
|
||||||
--active-overlay = "rgba(255,180,140,0.10)"
|
--active-overlay = "rgba(255,180,140,0.10)"
|
||||||
--rim-light = "rgba(255,180,140,0.08)"
|
--rim-light = "rgba(255,180,140,0.14)"
|
||||||
--status-divider = "rgba(255,180,140,0.08)"
|
--status-divider = "rgba(255,180,140,0.08)"
|
||||||
--sidebar-hover = "rgba(255,180,140,0.10)"
|
--sidebar-hover = "rgba(255,180,140,0.10)"
|
||||||
--sidebar-icon = "rgba(255,180,140,0.36)"
|
--sidebar-icon = "rgba(255,180,140,0.36)"
|
||||||
@@ -485,12 +485,12 @@ suggestion-trunc-len = { size = 50 }
|
|||||||
fee-rounding = { size = 10.0 }
|
fee-rounding = { size = 10.0 }
|
||||||
amount-bar-max-btn-width = { size = 80.0 }
|
amount-bar-max-btn-width = { size = 80.0 }
|
||||||
amount-bar-height = { size = 22.0 }
|
amount-bar-height = { size = 22.0 }
|
||||||
confirm-popup-max-width = { size = 420.0 }
|
confirm-popup-max-width = { size = 560.0 }
|
||||||
confirm-addr-card-height = { size = 28.0 }
|
confirm-addr-card-height = { size = 28.0 }
|
||||||
confirm-amount-card-height = { size = 70.0 }
|
confirm-amount-card-height = { size = 96.0 }
|
||||||
confirm-row-step = { size = 16.0 }
|
confirm-row-step = { size = 24.0 }
|
||||||
confirm-val-col-x = { size = 90.0 }
|
confirm-val-col-x = { size = 150.0 }
|
||||||
confirm-usd-col-x = { size = 80.0 }
|
confirm-usd-col-x = { size = 116.0 }
|
||||||
progress-card-height = { size = 36.0 }
|
progress-card-height = { size = 36.0 }
|
||||||
progress-card-height-txid = { size = 52.0 }
|
progress-card-height-txid = { size = 52.0 }
|
||||||
progress-card-pad-x = { size = 12.0 }
|
progress-card-pad-x = { size = 12.0 }
|
||||||
@@ -516,10 +516,10 @@ error-icon-inset = { size = 20.0 }
|
|||||||
error-btn-rounding = { size = 4.0 }
|
error-btn-rounding = { size = 4.0 }
|
||||||
progress-glass-rounding-ratio = { size = 0.75 }
|
progress-glass-rounding-ratio = { size = 0.75 }
|
||||||
confirm-addr-card-min-height = { size = 24.0 }
|
confirm-addr-card-min-height = { size = 24.0 }
|
||||||
confirm-val-col-min-x = { size = 70.0 }
|
confirm-val-col-min-x = { size = 112.0 }
|
||||||
confirm-usd-col-min-x = { size = 60.0 }
|
confirm-usd-col-min-x = { size = 92.0 }
|
||||||
confirm-amount-card-min-height = { size = 54.0 }
|
confirm-amount-card-min-height = { size = 82.0 }
|
||||||
confirm-row-step-min = { size = 12.0 }
|
confirm-row-step-min = { size = 20.0 }
|
||||||
action-btn-min-height = { size = 26.0 }
|
action-btn-min-height = { size = 26.0 }
|
||||||
recent-icon-min-size = { size = 3.5 }
|
recent-icon-min-size = { size = 3.5 }
|
||||||
recent-icon-size = { size = 5.0 }
|
recent-icon-size = { size = 5.0 }
|
||||||
@@ -566,7 +566,7 @@ txid-trunc-len = { size = 14 }
|
|||||||
txid-label-x-offset = { size = 20.0 }
|
txid-label-x-offset = { size = 20.0 }
|
||||||
txid-copy-btn-right-offset = { size = 50.0 }
|
txid-copy-btn-right-offset = { size = 50.0 }
|
||||||
txid-copy-btn-y-offset = { size = 2.0 }
|
txid-copy-btn-y-offset = { size = 2.0 }
|
||||||
confirm-popup-width-ratio = { size = 0.85 }
|
confirm-popup-width-ratio = { size = 0.92 }
|
||||||
confirm-glass-rounding-ratio = { size = 0.75 }
|
confirm-glass-rounding-ratio = { size = 0.75 }
|
||||||
confirm-addr-trunc-len = { size = 48 }
|
confirm-addr-trunc-len = { size = 48 }
|
||||||
confirm-divider-thickness = { size = 1.0 }
|
confirm-divider-thickness = { size = 1.0 }
|
||||||
@@ -766,7 +766,7 @@ control-card-min-height = { size = 60.0 }
|
|||||||
active-cell-border-thickness = { size = 1.5 }
|
active-cell-border-thickness = { size = 1.5 }
|
||||||
cell-border-thickness = { size = 1.0 }
|
cell-border-thickness = { size = 1.0 }
|
||||||
button-icon-y-ratio = { size = 0.42 }
|
button-icon-y-ratio = { size = 0.42 }
|
||||||
button-label-y-ratio = { size = 0.78 }
|
button-label-y-ratio = { size = 0.72 }
|
||||||
chart-line-thickness = { size = 1.5 }
|
chart-line-thickness = { size = 1.5 }
|
||||||
details-card-min-height = { size = 50.0 }
|
details-card-min-height = { size = 50.0 }
|
||||||
ram-bar = { height = 6.0, rounding = 3.0, opacity = 0.65 }
|
ram-bar = { height = 6.0, rounding = 3.0, opacity = 0.65 }
|
||||||
@@ -779,8 +779,11 @@ pool-url-input = { width = 300.0, height = 28.0 }
|
|||||||
pool-worker-input = { width = 200.0, height = 28.0 }
|
pool-worker-input = { width = 200.0, height = 28.0 }
|
||||||
log-panel-height = { size = 120.0 }
|
log-panel-height = { size = 120.0 }
|
||||||
log-panel-min = { size = 60.0 }
|
log-panel-min = { size = 60.0 }
|
||||||
|
idle-row-height = { size = 28.0 }
|
||||||
|
idle-combo-width = { size = 64.0 }
|
||||||
|
|
||||||
[tabs.peers]
|
[tabs.peers]
|
||||||
|
refresh-button = { size = 110.0 }
|
||||||
table-min-height = 150.0
|
table-min-height = 150.0
|
||||||
table-height-ratio = 0.45
|
table-height-ratio = 0.45
|
||||||
version-column-width = 150.0
|
version-column-width = 150.0
|
||||||
@@ -807,6 +810,7 @@ dir-pill-padding = { size = 4.0 }
|
|||||||
dir-pill-y-offset = { size = 1.0 }
|
dir-pill-y-offset = { size = 1.0 }
|
||||||
dir-pill-y-bottom = { size = 3.0 }
|
dir-pill-y-bottom = { size = 3.0 }
|
||||||
dir-pill-rounding = { size = 3.0 }
|
dir-pill-rounding = { size = 3.0 }
|
||||||
|
seed-badge-padding = { size = 3.0 }
|
||||||
tls-badge-min-width = { size = 20.0 }
|
tls-badge-min-width = { size = 20.0 }
|
||||||
tls-badge-width = { size = 28.0 }
|
tls-badge-width = { size = 28.0 }
|
||||||
tls-badge-rounding = { size = 3.0 }
|
tls-badge-rounding = { size = 3.0 }
|
||||||
@@ -890,6 +894,18 @@ pair-bar-arrow-size = { size = 28.0 }
|
|||||||
exchange-combo-width = { size = 180.0 }
|
exchange-combo-width = { size = 180.0 }
|
||||||
exchange-top-gap = { size = 0.0 }
|
exchange-top-gap = { size = 0.0 }
|
||||||
|
|
||||||
|
[tabs.explorer]
|
||||||
|
search-input-width = { size = 400.0 }
|
||||||
|
search-button-width = { size = 100.0 }
|
||||||
|
search-bar-height = { size = 32.0 }
|
||||||
|
row-height = { size = 32.0 }
|
||||||
|
row-rounding = { size = 4.0 }
|
||||||
|
tx-row-height = { size = 28.0 }
|
||||||
|
label-column = { size = 160.0 }
|
||||||
|
detail-modal-width = { size = 700.0 }
|
||||||
|
detail-max-height = { size = 600.0 }
|
||||||
|
scroll-fade-zone = { size = 24.0 }
|
||||||
|
|
||||||
[tabs.console]
|
[tabs.console]
|
||||||
input-area-padding = 8.0
|
input-area-padding = 8.0
|
||||||
output-line-spacing = 2.0
|
output-line-spacing = 2.0
|
||||||
@@ -1152,6 +1168,8 @@ key-input = { height = 150 }
|
|||||||
rescan-height-input = { width = 100 }
|
rescan-height-input = { width = 100 }
|
||||||
import-button = { width = 120, font = "button" }
|
import-button = { width = 120, font = "button" }
|
||||||
close-button = { width = 100, font = "button" }
|
close-button = { width = 100, font = "button" }
|
||||||
|
paste-preview-alpha = { size = 0.3 }
|
||||||
|
paste-preview-max-chars = { size = 200 }
|
||||||
|
|
||||||
[components]
|
[components]
|
||||||
|
|
||||||
@@ -1212,9 +1230,10 @@ width = { size = 140.0 }
|
|||||||
collapsed-width = { size = 64.0 }
|
collapsed-width = { size = 64.0 }
|
||||||
collapse-anim-speed = { size = 10.0 }
|
collapse-anim-speed = { size = 10.0 }
|
||||||
auto-collapse-threshold = { size = 800.0 }
|
auto-collapse-threshold = { size = 800.0 }
|
||||||
section-gap = { size = 4.0 }
|
section-gap = { size = 8.0 }
|
||||||
section-label-pad-left = { size = 16.0 }
|
section-label-pad-left = { size = 16.0 }
|
||||||
item-height = { size = 42.0 }
|
section-label-pad-bottom = { size = 4.0 }
|
||||||
|
item-height = { size = 36.0 }
|
||||||
item-pad-x = { size = 8.0 }
|
item-pad-x = { size = 8.0 }
|
||||||
min-height = { size = 360.0 }
|
min-height = { size = 360.0 }
|
||||||
margin-top = { size = -12 }
|
margin-top = { size = -12 }
|
||||||
@@ -1230,8 +1249,8 @@ icon-half-size = { size = 7.0 }
|
|||||||
icon-label-gap = { size = 8.0 }
|
icon-label-gap = { size = 8.0 }
|
||||||
badge-radius-dot = { size = 4.0 }
|
badge-radius-dot = { size = 4.0 }
|
||||||
badge-radius-number = { size = 8.0 }
|
badge-radius-number = { size = 8.0 }
|
||||||
button-spacing = { size = 4.0 }
|
button-spacing = { size = 6.0 }
|
||||||
bottom-padding = { size = 0.0 }
|
bottom-padding = { size = 4.0 }
|
||||||
exit-icon-gap = { size = 4.0 }
|
exit-icon-gap = { size = 4.0 }
|
||||||
cutout-shadow-alpha = { size = 55 }
|
cutout-shadow-alpha = { size = 55 }
|
||||||
cutout-highlight-alpha = { size = 8 }
|
cutout-highlight-alpha = { size = 8 }
|
||||||
@@ -1247,8 +1266,8 @@ inset-shadow-fade-ratio = { size = 5.0 }
|
|||||||
[components.content-area]
|
[components.content-area]
|
||||||
padding-x = 0.0
|
padding-x = 0.0
|
||||||
padding-y = 0.0
|
padding-y = 0.0
|
||||||
margin-top = { size = 4.0 }
|
margin-top = { size = 6.0 }
|
||||||
margin-bottom = { size = -40.0 }
|
margin-bottom = { size = -39.0 }
|
||||||
edge-fade-zone = { size = 0.0 }
|
edge-fade-zone = { size = 0.0 }
|
||||||
|
|
||||||
[components.content-area.window]
|
[components.content-area.window]
|
||||||
@@ -1260,10 +1279,10 @@ page-fade-speed = { size = 8.0 }
|
|||||||
collapse-hysteresis = { size = 60.0 }
|
collapse-hysteresis = { size = 60.0 }
|
||||||
header-icon = { icon-dark = "logos/logo_ObsidianDragon_dark.png", icon-light = "logos/logo_ObsidianDragon_light.png" }
|
header-icon = { icon-dark = "logos/logo_ObsidianDragon_dark.png", icon-light = "logos/logo_ObsidianDragon_light.png" }
|
||||||
coin-icon = { icon = "logos/logo_dragonx_128.png" }
|
coin-icon = { icon = "logos/logo_dragonx_128.png" }
|
||||||
header-title = { font = "subtitle1", size = 14.0, pad-x = 16.0, pad-y = 12.0, logo-gap = 8.0, opacity = 0.7 }
|
header-title = { font = "subtitle1", size = 12.0, pad-x = 8.0, pad-y = 10.0, logo-gap = 4.0, opacity = 0.7, offset-y = -2.0 }
|
||||||
|
|
||||||
[components.main-window.window]
|
[components.main-window.window]
|
||||||
padding = [12, 38]
|
padding = [12, 36]
|
||||||
|
|
||||||
[components.shutdown]
|
[components.shutdown]
|
||||||
content-height = { height = 120.0 }
|
content-height = { height = 120.0 }
|
||||||
@@ -1307,6 +1326,8 @@ rpc-label-width = { size = 85.0 }
|
|||||||
security-combo-width = { size = 120.0 }
|
security-combo-width = { size = 120.0 }
|
||||||
port-input-min-width = { size = 60.0 }
|
port-input-min-width = { size = 60.0 }
|
||||||
port-input-width-ratio = { size = 0.4 }
|
port-input-width-ratio = { size = 0.4 }
|
||||||
|
idle-combo-width = { size = 64.0 }
|
||||||
|
about-logo-size = { size = 64.0 }
|
||||||
|
|
||||||
[components.main-layout]
|
[components.main-layout]
|
||||||
app-bar-height = { size = 64.0 }
|
app-bar-height = { size = 64.0 }
|
||||||
@@ -1357,6 +1378,17 @@ summary = { min-width = 280.0, max-width = 400.0, width-ratio = 0.32, min-height
|
|||||||
side-panel = { min-width = 280.0, max-width = 450.0, width-ratio = 0.4, min-height = 200.0, max-height = 350.0, height-ratio = 0.8 }
|
side-panel = { min-width = 280.0, max-width = 450.0, width-ratio = 0.4, min-height = 200.0, max-height = 350.0, height-ratio = 0.8 }
|
||||||
table = { min-height = 150.0, height-ratio = 0.45, min-remaining = 100.0, default-reserve = 30.0 }
|
table = { min-height = 150.0, height-ratio = 0.45, min-remaining = 100.0, default-reserve = 30.0 }
|
||||||
|
|
||||||
|
[dialog]
|
||||||
|
width-default = 480.0
|
||||||
|
width-lg = 600.0
|
||||||
|
width-xl = 660.0
|
||||||
|
min-width = 280.0
|
||||||
|
form-width = 400.0
|
||||||
|
action-width = 100.0
|
||||||
|
action-gap = 8.0
|
||||||
|
max-height-ratio = 0.94
|
||||||
|
compact-bottom-ratio = 0.64
|
||||||
|
|
||||||
[button]
|
[button]
|
||||||
min-width = 180.0
|
min-width = 180.0
|
||||||
width = 140.0
|
width = 140.0
|
||||||
|
|||||||
2135
scripts/add_missing_translations.py
Normal file
131
scripts/build_cjk_subset.py
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Build a NotoSansCJK subset font containing all characters used by
|
||||||
|
the zh, ja, and ko translation files, plus common CJK punctuation
|
||||||
|
and symbols.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 scripts/build_cjk_subset.py
|
||||||
|
|
||||||
|
Requires: pip install fonttools brotli
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from fontTools.ttLib import TTFont
|
||||||
|
from fontTools import subset as ftsubset
|
||||||
|
|
||||||
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
LANG_DIR = os.path.join(ROOT, 'res', 'lang')
|
||||||
|
SOURCE_FONT = '/tmp/NotoSansCJKsc-Regular.otf'
|
||||||
|
OUTPUT_FONT = os.path.join(ROOT, 'res', 'fonts', 'NotoSansCJK-Subset.ttf')
|
||||||
|
|
||||||
|
# Collect all characters used in CJK translation files
|
||||||
|
needed = set()
|
||||||
|
for lang in ['zh', 'ja', 'ko']:
|
||||||
|
path = os.path.join(LANG_DIR, f'{lang}.json')
|
||||||
|
if not os.path.exists(path):
|
||||||
|
continue
|
||||||
|
with open(path, 'r', encoding='utf-8') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
for v in data.values():
|
||||||
|
if isinstance(v, str):
|
||||||
|
for c in v:
|
||||||
|
cp = ord(c)
|
||||||
|
if cp > 0x7F: # non-ASCII only (ASCII handled by Ubuntu font)
|
||||||
|
needed.add(cp)
|
||||||
|
|
||||||
|
# Also add common CJK ranges that future translations might use:
|
||||||
|
# - CJK punctuation and symbols (3000-303F)
|
||||||
|
# - Hiragana (3040-309F)
|
||||||
|
# - Katakana (30A0-30FF)
|
||||||
|
# - Bopomofo (3100-312F)
|
||||||
|
# - CJK quotation marks, brackets
|
||||||
|
for cp in range(0x3000, 0x3100):
|
||||||
|
needed.add(cp)
|
||||||
|
for cp in range(0x3100, 0x3130):
|
||||||
|
needed.add(cp)
|
||||||
|
# Fullwidth ASCII variants (commonly mixed in CJK text)
|
||||||
|
for cp in range(0xFF01, 0xFF5F):
|
||||||
|
needed.add(cp)
|
||||||
|
|
||||||
|
print(f"Total non-ASCII characters to include: {len(needed)}")
|
||||||
|
|
||||||
|
# Check which of these the source font supports
|
||||||
|
font = TTFont(SOURCE_FONT)
|
||||||
|
cmap = font.getBestCmap()
|
||||||
|
supportable = needed & set(cmap.keys())
|
||||||
|
unsupported = needed - set(cmap.keys())
|
||||||
|
|
||||||
|
print(f"Supported by source font: {len(supportable)}")
|
||||||
|
if unsupported:
|
||||||
|
print(f"Not in source font (will use fallback): {len(unsupported)}")
|
||||||
|
for cp in sorted(unsupported)[:10]:
|
||||||
|
print(f" U+{cp:04X} {chr(cp)}")
|
||||||
|
|
||||||
|
# Build the subset using pyftsubset CLI-style API
|
||||||
|
args = [
|
||||||
|
SOURCE_FONT,
|
||||||
|
f'--output-file={OUTPUT_FONT}',
|
||||||
|
f'--unicodes={",".join(f"U+{cp:04X}" for cp in sorted(supportable))}',
|
||||||
|
'--no-hinting',
|
||||||
|
'--desubroutinize',
|
||||||
|
]
|
||||||
|
|
||||||
|
ftsubset.main(args)
|
||||||
|
|
||||||
|
# Convert CFF outlines to TrueType (glyf) outlines.
|
||||||
|
# stb_truetype (used by ImGui) doesn't handle CID-keyed CFF fonts properly.
|
||||||
|
from fontTools.pens.cu2quPen import Cu2QuPen
|
||||||
|
from fontTools.pens.ttGlyphPen import TTGlyphPen
|
||||||
|
from fontTools.ttLib import newTable
|
||||||
|
|
||||||
|
tmp_otf = OUTPUT_FONT + '.tmp.otf'
|
||||||
|
os.rename(OUTPUT_FONT, tmp_otf)
|
||||||
|
|
||||||
|
conv = TTFont(tmp_otf)
|
||||||
|
if 'CFF ' in conv:
|
||||||
|
print("Converting CFF -> TrueType outlines...")
|
||||||
|
glyphOrder = conv.getGlyphOrder()
|
||||||
|
glyphSet = conv.getGlyphSet()
|
||||||
|
glyf_table = newTable("glyf")
|
||||||
|
glyf_table.glyphs = {}
|
||||||
|
glyf_table.glyphOrder = glyphOrder
|
||||||
|
loca_table = newTable("loca")
|
||||||
|
from fontTools.ttLib.tables._g_l_y_f import Glyph as TTGlyph
|
||||||
|
for gname in glyphOrder:
|
||||||
|
try:
|
||||||
|
ttPen = TTGlyphPen(glyphSet)
|
||||||
|
cu2quPen = Cu2QuPen(ttPen, max_err=1.0, reverse_direction=True)
|
||||||
|
glyphSet[gname].draw(cu2quPen)
|
||||||
|
glyf_table.glyphs[gname] = ttPen.glyph()
|
||||||
|
except Exception:
|
||||||
|
glyf_table.glyphs[gname] = TTGlyph()
|
||||||
|
del conv['CFF ']
|
||||||
|
if 'VORG' in conv:
|
||||||
|
del conv['VORG']
|
||||||
|
conv['glyf'] = glyf_table
|
||||||
|
conv['loca'] = loca_table
|
||||||
|
conv['head'].indexToLocFormat = 1
|
||||||
|
if 'maxp' in conv:
|
||||||
|
conv['maxp'].version = 0x00010000
|
||||||
|
conv.sfntVersion = "\x00\x01\x00\x00"
|
||||||
|
conv.save(OUTPUT_FONT)
|
||||||
|
conv.close()
|
||||||
|
os.remove(tmp_otf)
|
||||||
|
|
||||||
|
size = os.path.getsize(OUTPUT_FONT)
|
||||||
|
print(f"\nOutput: {OUTPUT_FONT}")
|
||||||
|
print(f"Size: {size / 1024:.0f} KB")
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
verify = TTFont(OUTPUT_FONT)
|
||||||
|
verify_cmap = set(verify.getBestCmap().keys())
|
||||||
|
still_missing = needed - verify_cmap
|
||||||
|
print(f"Verified glyphs in subset: {len(verify_cmap)}")
|
||||||
|
if still_missing:
|
||||||
|
# These are chars not in the source font - expected for some Hangul/Hiragana
|
||||||
|
print(f"Not coverable by this font: {len(still_missing)} (need additional font)")
|
||||||
|
for cp in sorted(still_missing)[:10]:
|
||||||
|
print(f" U+{cp:04X} {chr(cp)}")
|
||||||
|
else:
|
||||||
|
print("All needed characters are covered!")
|
||||||
64
scripts/check_cjk_coverage.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Check which characters in translation files fall outside the font glyph ranges."""
|
||||||
|
import json
|
||||||
|
import unicodedata
|
||||||
|
import glob
|
||||||
|
import os
|
||||||
|
|
||||||
|
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
LANG_DIR = os.path.join(SCRIPT_DIR, '..', 'res', 'lang')
|
||||||
|
|
||||||
|
# Glyph ranges from typography.cpp (regular font + CJK merge)
|
||||||
|
RANGES = [
|
||||||
|
# Regular font ranges
|
||||||
|
(0x0020, 0x00FF), # Basic Latin + Latin-1 Supplement
|
||||||
|
(0x0100, 0x024F), # Latin Extended-A + B
|
||||||
|
(0x0370, 0x03FF), # Greek and Coptic
|
||||||
|
(0x0400, 0x04FF), # Cyrillic
|
||||||
|
(0x0500, 0x052F), # Cyrillic Supplement
|
||||||
|
(0x2000, 0x206F), # General Punctuation
|
||||||
|
(0x2190, 0x21FF), # Arrows
|
||||||
|
(0x2200, 0x22FF), # Mathematical Operators
|
||||||
|
(0x2600, 0x26FF), # Miscellaneous Symbols
|
||||||
|
# CJK ranges
|
||||||
|
(0x2E80, 0x2FDF), # CJK Radicals
|
||||||
|
(0x3000, 0x30FF), # CJK Symbols, Hiragana, Katakana
|
||||||
|
(0x3100, 0x312F), # Bopomofo
|
||||||
|
(0x31F0, 0x31FF), # Katakana Extensions
|
||||||
|
(0x3400, 0x4DBF), # CJK Extension A
|
||||||
|
(0x4E00, 0x9FFF), # CJK Unified Ideographs
|
||||||
|
(0xAC00, 0xD7AF), # Hangul Syllables
|
||||||
|
(0xFF00, 0xFFEF), # Fullwidth Forms
|
||||||
|
]
|
||||||
|
|
||||||
|
def in_ranges(cp):
|
||||||
|
return any(lo <= cp <= hi for lo, hi in RANGES)
|
||||||
|
|
||||||
|
for path in sorted(glob.glob(os.path.join(LANG_DIR, '*.json'))):
|
||||||
|
lang = os.path.basename(path).replace('.json', '')
|
||||||
|
with open(path, 'r', encoding='utf-8') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
missing = {}
|
||||||
|
for key, val in data.items():
|
||||||
|
if not isinstance(val, str):
|
||||||
|
continue
|
||||||
|
for c in val:
|
||||||
|
cp = ord(c)
|
||||||
|
if cp > 0x7F and not in_ranges(cp):
|
||||||
|
if c not in missing:
|
||||||
|
missing[c] = []
|
||||||
|
missing[c].append(key)
|
||||||
|
|
||||||
|
if missing:
|
||||||
|
print(f"\n=== {lang}.json: {len(missing)} missing characters ===")
|
||||||
|
for c in sorted(missing, key=lambda x: ord(x)):
|
||||||
|
cp = ord(c)
|
||||||
|
name = unicodedata.name(c, 'UNKNOWN')
|
||||||
|
keys = missing[c][:3]
|
||||||
|
key_str = ', '.join(keys)
|
||||||
|
if len(missing[c]) > 3:
|
||||||
|
key_str += f' (+{len(missing[c])-3} more)'
|
||||||
|
print(f" U+{cp:04X} {c} ({name}) — used in: {key_str}")
|
||||||
|
else:
|
||||||
|
print(f"=== {lang}.json: OK (all characters covered) ===")
|
||||||
47
scripts/check_font_coverage.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Check which characters needed by translations are missing from bundled fonts."""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from fontTools.ttLib import TTFont
|
||||||
|
|
||||||
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
FONTS_DIR = os.path.join(ROOT, 'res', 'fonts')
|
||||||
|
LANG_DIR = os.path.join(ROOT, 'res', 'lang')
|
||||||
|
|
||||||
|
# Load font cmaps
|
||||||
|
cjk = TTFont(os.path.join(FONTS_DIR, 'NotoSansCJK-Subset.ttf'))
|
||||||
|
cjk_cmap = set(cjk.getBestCmap().keys())
|
||||||
|
|
||||||
|
ubuntu = TTFont(os.path.join(FONTS_DIR, 'Ubuntu-R.ttf'))
|
||||||
|
ubuntu_cmap = set(ubuntu.getBestCmap().keys())
|
||||||
|
|
||||||
|
combined = cjk_cmap | ubuntu_cmap
|
||||||
|
|
||||||
|
print(f"CJK subset font glyphs: {len(cjk_cmap)}")
|
||||||
|
print(f"Ubuntu font glyphs: {len(ubuntu_cmap)}")
|
||||||
|
print(f"Combined: {len(combined)}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
for lang in ['zh', 'ja', 'ko', 'ru', 'de', 'es', 'fr', 'pt']:
|
||||||
|
path = os.path.join(LANG_DIR, f'{lang}.json')
|
||||||
|
if not os.path.exists(path):
|
||||||
|
continue
|
||||||
|
with open(path, 'r', encoding='utf-8') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
needed = set()
|
||||||
|
for v in data.values():
|
||||||
|
if isinstance(v, str):
|
||||||
|
for c in v:
|
||||||
|
needed.add(ord(c))
|
||||||
|
|
||||||
|
missing = sorted(needed - combined)
|
||||||
|
if missing:
|
||||||
|
print(f"{lang}.json: {len(needed)} chars needed, {len(missing)} MISSING")
|
||||||
|
for cp in missing[:20]:
|
||||||
|
c = chr(cp)
|
||||||
|
print(f" U+{cp:04X} {c}")
|
||||||
|
if len(missing) > 20:
|
||||||
|
print(f" ... and {len(missing) - 20} more")
|
||||||
|
else:
|
||||||
|
print(f"{lang}.json: OK ({len(needed)} chars, all covered)")
|
||||||
214
scripts/convert_cjk_to_ttf.py
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Convert CJK subset from CID-keyed CFF/OTF to TrueType/TTF.
|
||||||
|
|
||||||
|
stb_truetype (used by ImGui) doesn't handle CID-keyed CFF fonts properly,
|
||||||
|
so we need glyf-based TrueType outlines instead.
|
||||||
|
|
||||||
|
Two approaches:
|
||||||
|
1. Direct CFF->TTF conversion via cu2qu (fontTools)
|
||||||
|
2. Download NotoSansSC-Regular.ttf (already TTF) and re-subset
|
||||||
|
|
||||||
|
This script tries approach 1 first, falls back to approach 2.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import glob
|
||||||
|
|
||||||
|
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
PROJECT_ROOT = os.path.dirname(SCRIPT_DIR)
|
||||||
|
FONT_DIR = os.path.join(PROJECT_ROOT, "res", "fonts")
|
||||||
|
LANG_DIR = os.path.join(PROJECT_ROOT, "res", "lang")
|
||||||
|
|
||||||
|
SRC_OTF = os.path.join(FONT_DIR, "NotoSansCJK-Subset.otf")
|
||||||
|
DST_TTF = os.path.join(FONT_DIR, "NotoSansCJK-Subset.ttf")
|
||||||
|
|
||||||
|
|
||||||
|
def get_needed_codepoints():
|
||||||
|
"""Collect all unique codepoints from CJK translation files."""
|
||||||
|
codepoints = set()
|
||||||
|
for lang_file in glob.glob(os.path.join(LANG_DIR, "*.json")):
|
||||||
|
with open(lang_file, "r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
for value in data.values():
|
||||||
|
if isinstance(value, str):
|
||||||
|
for ch in value:
|
||||||
|
cp = ord(ch)
|
||||||
|
# Include CJK + Hangul + fullwidth + CJK symbols/kana
|
||||||
|
if cp >= 0x2E80:
|
||||||
|
codepoints.add(cp)
|
||||||
|
return codepoints
|
||||||
|
|
||||||
|
|
||||||
|
def convert_cff_to_ttf():
|
||||||
|
"""Convert existing OTF/CFF font to TTF using fontTools cu2qu."""
|
||||||
|
from fontTools.ttLib import TTFont
|
||||||
|
from fontTools.pens.cu2quPen import Cu2QuPen
|
||||||
|
from fontTools.pens.ttGlyphPen import TTGlyphPen
|
||||||
|
|
||||||
|
print(f"Loading {SRC_OTF}...")
|
||||||
|
font = TTFont(SRC_OTF)
|
||||||
|
|
||||||
|
# Verify it's CFF
|
||||||
|
if "CFF " not in font:
|
||||||
|
print("Font is not CFF, skipping conversion")
|
||||||
|
return False
|
||||||
|
|
||||||
|
cff = font["CFF "]
|
||||||
|
top = cff.cff.topDictIndex[0]
|
||||||
|
print(f"ROS: {getattr(top, 'ROS', None)}")
|
||||||
|
print(f"CID-keyed: {getattr(top, 'FDSelect', None) is not None}")
|
||||||
|
|
||||||
|
glyphOrder = font.getGlyphOrder()
|
||||||
|
print(f"Glyphs: {len(glyphOrder)}")
|
||||||
|
|
||||||
|
# Use fontTools' built-in otf2ttf if available
|
||||||
|
try:
|
||||||
|
from fontTools.otf2ttf import otf_to_ttf
|
||||||
|
otf_to_ttf(font)
|
||||||
|
font.save(DST_TTF)
|
||||||
|
print(f"Saved TTF: {DST_TTF} ({os.path.getsize(DST_TTF)} bytes)")
|
||||||
|
font.close()
|
||||||
|
return True
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Manual conversion using cu2qu
|
||||||
|
print("Using manual CFF->TTF conversion with cu2qu...")
|
||||||
|
|
||||||
|
from fontTools.pens.recordingPen import RecordingPen
|
||||||
|
from fontTools.pens.pointPen import SegmentToPointPen
|
||||||
|
from fontTools import ttLib
|
||||||
|
from fontTools.ttLib.tables._g_l_y_f import Glyph as TTGlyph
|
||||||
|
import struct
|
||||||
|
|
||||||
|
# Get glyph set
|
||||||
|
glyphSet = font.getGlyphSet()
|
||||||
|
|
||||||
|
# Create new glyf table
|
||||||
|
from fontTools.ttLib import newTable
|
||||||
|
|
||||||
|
glyf_table = newTable("glyf")
|
||||||
|
glyf_table.glyphs = {}
|
||||||
|
glyf_table.glyphOrder = glyphOrder
|
||||||
|
|
||||||
|
loca_table = newTable("loca")
|
||||||
|
|
||||||
|
max_error = 1.0 # em-units tolerance for cubic->quadratic
|
||||||
|
|
||||||
|
for gname in glyphOrder:
|
||||||
|
try:
|
||||||
|
ttPen = TTGlyphPen(glyphSet)
|
||||||
|
cu2quPen = Cu2QuPen(ttPen, max_err=max_error, reverse_direction=True)
|
||||||
|
glyphSet[gname].draw(cu2quPen)
|
||||||
|
glyf_table.glyphs[gname] = ttPen.glyph()
|
||||||
|
except Exception as e:
|
||||||
|
# Fallback: empty glyph
|
||||||
|
glyf_table.glyphs[gname] = TTGlyph()
|
||||||
|
|
||||||
|
# Replace CFF with glyf
|
||||||
|
del font["CFF "]
|
||||||
|
if "VORG" in font:
|
||||||
|
del font["VORG"]
|
||||||
|
|
||||||
|
font["glyf"] = glyf_table
|
||||||
|
font["loca"] = loca_table
|
||||||
|
|
||||||
|
# Add required tables for TTF
|
||||||
|
# head table needs indexToLocFormat
|
||||||
|
font["head"].indexToLocFormat = 1 # long format
|
||||||
|
|
||||||
|
# Create maxp for TrueType
|
||||||
|
if "maxp" in font:
|
||||||
|
font["maxp"].version = 0x00010000
|
||||||
|
|
||||||
|
# Update sfntVersion
|
||||||
|
font.sfntVersion = "\x00\x01\x00\x00" # TrueType
|
||||||
|
|
||||||
|
font.save(DST_TTF)
|
||||||
|
print(f"Saved TTF: {DST_TTF} ({os.path.getsize(DST_TTF)} bytes)")
|
||||||
|
font.close()
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def download_and_subset():
|
||||||
|
"""Download NotoSansSC-Regular.ttf and subset it."""
|
||||||
|
import urllib.request
|
||||||
|
from fontTools.ttLib import TTFont
|
||||||
|
from fontTools import subset
|
||||||
|
|
||||||
|
# Google Fonts provides static TTF files
|
||||||
|
url = "https://github.com/notofonts/noto-cjk/raw/main/Sans/SubsetOTF/SC/NotoSansSC-Regular.otf"
|
||||||
|
# Actually, we want TTF. Let's try the variable font approach.
|
||||||
|
# Or better: use google-fonts API for static TTF
|
||||||
|
|
||||||
|
# NotoSansSC static TTF from Google Fonts CDN
|
||||||
|
tmp_font = "/tmp/NotoSansSC-Regular.ttf"
|
||||||
|
|
||||||
|
if not os.path.exists(tmp_font):
|
||||||
|
print(f"Downloading NotoSansSC-Regular.ttf...")
|
||||||
|
url = "https://github.com/notofonts/noto-cjk/raw/main/Sans/OTC/NotoSansCJK-Regular.ttc"
|
||||||
|
# This is a TTC (font collection), too large.
|
||||||
|
# Use the OTF we already have and convert it.
|
||||||
|
return False
|
||||||
|
|
||||||
|
print(f"Using {tmp_font}")
|
||||||
|
font = TTFont(tmp_font)
|
||||||
|
cmap = font.getBestCmap()
|
||||||
|
print(f"Source has {len(cmap)} cmap entries")
|
||||||
|
|
||||||
|
needed = get_needed_codepoints()
|
||||||
|
print(f"Need {len(needed)} CJK codepoints")
|
||||||
|
|
||||||
|
# Subset
|
||||||
|
subsetter = subset.Subsetter()
|
||||||
|
subsetter.populate(unicodes=needed)
|
||||||
|
subsetter.subset(font)
|
||||||
|
|
||||||
|
font.save(DST_TTF)
|
||||||
|
print(f"Saved: {DST_TTF} ({os.path.getsize(DST_TTF)} bytes)")
|
||||||
|
font.close()
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def verify_result():
|
||||||
|
"""Verify the output TTF has glyf outlines and correct characters."""
|
||||||
|
from fontTools.ttLib import TTFont
|
||||||
|
|
||||||
|
font = TTFont(DST_TTF)
|
||||||
|
cmap = font.getBestCmap()
|
||||||
|
|
||||||
|
print(f"\n--- Verification ---")
|
||||||
|
print(f"Format: {font.sfntVersion!r}")
|
||||||
|
print(f"Has glyf: {'glyf' in font}")
|
||||||
|
print(f"Has CFF: {'CFF ' in font}")
|
||||||
|
print(f"Cmap entries: {len(cmap)}")
|
||||||
|
|
||||||
|
# Check key characters
|
||||||
|
test_chars = {
|
||||||
|
"历": 0x5386, "史": 0x53F2, # Chinese: history
|
||||||
|
"概": 0x6982, "述": 0x8FF0, # Chinese: overview
|
||||||
|
"设": 0x8BBE, "置": 0x7F6E, # Chinese: settings
|
||||||
|
}
|
||||||
|
for name, cp in test_chars.items():
|
||||||
|
status = "YES" if cp in cmap else "NO"
|
||||||
|
print(f" {name} (U+{cp:04X}): {status}")
|
||||||
|
|
||||||
|
size = os.path.getsize(DST_TTF)
|
||||||
|
print(f"File size: {size} bytes ({size/1024:.1f} KB)")
|
||||||
|
font.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print("=== CJK Font CFF -> TTF Converter ===\n")
|
||||||
|
|
||||||
|
if convert_cff_to_ttf():
|
||||||
|
verify_result()
|
||||||
|
else:
|
||||||
|
print("Direct conversion failed, trying download approach...")
|
||||||
|
if download_and_subset():
|
||||||
|
verify_result()
|
||||||
|
else:
|
||||||
|
print("ERROR: Could not convert font")
|
||||||
|
sys.exit(1)
|
||||||
@@ -8,7 +8,7 @@ set -e
|
|||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
BUILD_DIR="${SCRIPT_DIR}/build/linux"
|
BUILD_DIR="${SCRIPT_DIR}/build/linux"
|
||||||
APPDIR="${BUILD_DIR}/AppDir"
|
APPDIR="${BUILD_DIR}/AppDir"
|
||||||
VERSION="1.0.0"
|
VERSION="1.2.0"
|
||||||
|
|
||||||
# Colors
|
# Colors
|
||||||
GREEN='\033[0;32m'
|
GREEN='\033[0;32m'
|
||||||
|
|||||||
122
scripts/expand_themes.py
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Build-time theme expander — merges layout sections from ui.toml into skin files.
|
||||||
|
|
||||||
|
Called by CMake during build. Reads source skin files + ui.toml, writes merged
|
||||||
|
output files to the build directory. Source files are never modified.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 expand_themes.py <source_themes_dir> <output_themes_dir>
|
||||||
|
|
||||||
|
For each .toml file in source_themes_dir (except ui.toml), the script:
|
||||||
|
1. Copies the skin file contents (theme/palette/backdrop/effects)
|
||||||
|
2. Appends all layout sections from ui.toml (fonts, tabs, components, etc.)
|
||||||
|
3. Writes the merged result to output_themes_dir/<filename>
|
||||||
|
|
||||||
|
ui.toml itself is copied unchanged.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Sections to SKIP when extracting from ui.toml (theme-specific, already in skins)
|
||||||
|
SKIP_SECTIONS = {"theme", "theme.palette", "backdrop", "effects"}
|
||||||
|
|
||||||
|
|
||||||
|
def extract_layout_sections(ui_toml_path):
|
||||||
|
"""Extract non-theme sections from ui.toml as a string."""
|
||||||
|
sections = []
|
||||||
|
current_section = None
|
||||||
|
current_lines = []
|
||||||
|
section_re = re.compile(r'^\[{1,2}([^\]]+)\]{1,2}\s*$')
|
||||||
|
|
||||||
|
with open(ui_toml_path, 'r') as f:
|
||||||
|
for line in f:
|
||||||
|
m = section_re.match(line.strip())
|
||||||
|
if m:
|
||||||
|
if current_section is not None or current_lines:
|
||||||
|
sections.append((current_section, current_lines))
|
||||||
|
current_section = m.group(1).strip()
|
||||||
|
current_lines = [line]
|
||||||
|
else:
|
||||||
|
current_lines.append(line)
|
||||||
|
if current_section is not None or current_lines:
|
||||||
|
sections.append((current_section, current_lines))
|
||||||
|
|
||||||
|
layout_parts = []
|
||||||
|
for section_name, lines in sections:
|
||||||
|
if section_name is None:
|
||||||
|
# Preamble: only include top-level key=value lines
|
||||||
|
kv_lines = [l for l in lines
|
||||||
|
if l.strip() and not l.strip().startswith('#') and '=' in l]
|
||||||
|
if kv_lines:
|
||||||
|
layout_parts.append(''.join(kv_lines))
|
||||||
|
continue
|
||||||
|
if section_name in SKIP_SECTIONS:
|
||||||
|
continue
|
||||||
|
layout_parts.append(''.join(lines))
|
||||||
|
|
||||||
|
return '\n'.join(layout_parts)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) != 3:
|
||||||
|
print(f"Usage: {sys.argv[0]} <source_themes_dir> <output_themes_dir>")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
src_dir = sys.argv[1]
|
||||||
|
out_dir = sys.argv[2]
|
||||||
|
ui_toml = os.path.join(src_dir, "ui.toml")
|
||||||
|
|
||||||
|
if not os.path.exists(ui_toml):
|
||||||
|
print(f"ERROR: ui.toml not found at {ui_toml}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
os.makedirs(out_dir, exist_ok=True)
|
||||||
|
|
||||||
|
layout_content = extract_layout_sections(ui_toml)
|
||||||
|
|
||||||
|
separator = (
|
||||||
|
"\n# ===========================================================================\n"
|
||||||
|
"# Layout & Component Properties\n"
|
||||||
|
"# All values below can be customized per-theme. Edit and save to see\n"
|
||||||
|
"# changes reflected in the app in real time via hot-reload.\n"
|
||||||
|
"# ===========================================================================\n\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
for fname in sorted(os.listdir(src_dir)):
|
||||||
|
if not fname.endswith('.toml'):
|
||||||
|
continue
|
||||||
|
src_path = os.path.join(src_dir, fname)
|
||||||
|
dst_path = os.path.join(out_dir, fname)
|
||||||
|
|
||||||
|
if fname == "ui.toml":
|
||||||
|
# Copy ui.toml unchanged
|
||||||
|
with open(src_path, 'r') as f:
|
||||||
|
content = f.read()
|
||||||
|
with open(dst_path, 'w') as f:
|
||||||
|
f.write(content)
|
||||||
|
print(f" COPY {fname}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Skin file — append layout sections
|
||||||
|
with open(src_path, 'r') as f:
|
||||||
|
skin = f.read()
|
||||||
|
if not skin.endswith('\n'):
|
||||||
|
skin += '\n'
|
||||||
|
|
||||||
|
merged = skin + separator + layout_content
|
||||||
|
if not merged.endswith('\n'):
|
||||||
|
merged += '\n'
|
||||||
|
|
||||||
|
with open(dst_path, 'w') as f:
|
||||||
|
f.write(merged)
|
||||||
|
lines = merged.count('\n')
|
||||||
|
print(f" MERGE {fname} → {lines} lines")
|
||||||
|
|
||||||
|
print("Done.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -50,12 +50,20 @@ if [[ ! -f "$TARBALL" ]]; then
|
|||||||
curl -fSL -o "$TARBALL" "$SODIUM_URL"
|
curl -fSL -o "$TARBALL" "$SODIUM_URL"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Verify checksum
|
# Verify checksum (sha256sum on Linux, shasum on macOS)
|
||||||
echo "$SODIUM_SHA256 $TARBALL" | sha256sum -c - || {
|
if command -v sha256sum &>/dev/null; then
|
||||||
echo "[fetch-libsodium] ERROR: SHA256 mismatch! Removing corrupted download."
|
echo "$SODIUM_SHA256 $TARBALL" | sha256sum -c - || {
|
||||||
rm -f "$TARBALL"
|
echo "[fetch-libsodium] ERROR: SHA256 mismatch! Removing corrupted download."
|
||||||
exit 1
|
rm -f "$TARBALL"
|
||||||
}
|
exit 1
|
||||||
|
}
|
||||||
|
elif command -v shasum &>/dev/null; then
|
||||||
|
echo "$SODIUM_SHA256 $TARBALL" | shasum -a 256 -c - || {
|
||||||
|
echo "[fetch-libsodium] ERROR: SHA256 mismatch! Removing corrupted download."
|
||||||
|
rm -f "$TARBALL"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
|
||||||
# ── Extract ─────────────────────────────────────────────────────────────────
|
# ── Extract ─────────────────────────────────────────────────────────────────
|
||||||
if [[ ! -d "$SRC_DIR" ]]; then
|
if [[ ! -d "$SRC_DIR" ]]; then
|
||||||
@@ -77,7 +85,7 @@ case "$TARGET" in
|
|||||||
mac)
|
mac)
|
||||||
# Cross-compile for macOS via osxcross
|
# Cross-compile for macOS via osxcross
|
||||||
if [[ -z "${OSXCROSS:-}" ]]; then
|
if [[ -z "${OSXCROSS:-}" ]]; then
|
||||||
for try in "$HOME/osxcross" "/opt/osxcross" "$PROJECT_DIR/osxcross"; do
|
for try in "$PROJECT_DIR/external/osxcross" "$HOME/osxcross" "/opt/osxcross" "$PROJECT_DIR/osxcross"; do
|
||||||
[[ -d "$try/target" ]] && OSXCROSS="$try" && break
|
[[ -d "$try/target" ]] && OSXCROSS="$try" && break
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
@@ -115,6 +123,69 @@ case "$TARGET" in
|
|||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
# ── Native macOS: build universal binary (arm64 + x86_64) ───────────────────
|
||||||
|
IS_MACOS_NATIVE=false
|
||||||
|
if [[ "$TARGET" == "native" && "$(uname -s)" == "Darwin" ]]; then
|
||||||
|
IS_MACOS_NATIVE=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if $IS_MACOS_NATIVE; then
|
||||||
|
echo "[fetch-libsodium] Building universal (arm64 + x86_64) for macOS..."
|
||||||
|
export MACOSX_DEPLOYMENT_TARGET="11.0"
|
||||||
|
|
||||||
|
INSTALL_ARM64="$PROJECT_DIR/libs/libsodium-arm64"
|
||||||
|
INSTALL_X86_64="$PROJECT_DIR/libs/libsodium-x86_64"
|
||||||
|
|
||||||
|
for ARCH in arm64 x86_64; do
|
||||||
|
echo "[fetch-libsodium] Building for $ARCH..."
|
||||||
|
cd "$SRC_DIR"
|
||||||
|
make clean 2>/dev/null || true
|
||||||
|
make distclean 2>/dev/null || true
|
||||||
|
|
||||||
|
if [[ "$ARCH" == "arm64" ]]; then
|
||||||
|
ARCH_INSTALL="$INSTALL_ARM64"
|
||||||
|
HOST_TRIPLE="aarch64-apple-darwin"
|
||||||
|
else
|
||||||
|
ARCH_INSTALL="$INSTALL_X86_64"
|
||||||
|
HOST_TRIPLE="x86_64-apple-darwin"
|
||||||
|
fi
|
||||||
|
|
||||||
|
ARCH_CFLAGS="-arch $ARCH -mmacosx-version-min=11.0"
|
||||||
|
|
||||||
|
./configure \
|
||||||
|
--prefix="$ARCH_INSTALL" \
|
||||||
|
--disable-shared \
|
||||||
|
--enable-static \
|
||||||
|
--with-pic \
|
||||||
|
--host="$HOST_TRIPLE" \
|
||||||
|
CFLAGS="$ARCH_CFLAGS" \
|
||||||
|
LDFLAGS="-arch $ARCH" \
|
||||||
|
> /dev/null
|
||||||
|
|
||||||
|
make -j"$(sysctl -n hw.ncpu 2>/dev/null || echo 4)" > /dev/null 2>&1
|
||||||
|
make install > /dev/null
|
||||||
|
done
|
||||||
|
|
||||||
|
# Merge with lipo
|
||||||
|
echo "[fetch-libsodium] Creating universal binary with lipo..."
|
||||||
|
mkdir -p "$INSTALL_DIR/lib" "$INSTALL_DIR/include"
|
||||||
|
lipo -create \
|
||||||
|
"$INSTALL_ARM64/lib/libsodium.a" \
|
||||||
|
"$INSTALL_X86_64/lib/libsodium.a" \
|
||||||
|
-output "$INSTALL_DIR/lib/libsodium.a"
|
||||||
|
cp -R "$INSTALL_ARM64/include/"* "$INSTALL_DIR/include/"
|
||||||
|
|
||||||
|
# Clean up per-arch builds
|
||||||
|
rm -rf "$INSTALL_ARM64" "$INSTALL_X86_64"
|
||||||
|
cd "$PROJECT_DIR"
|
||||||
|
rm -rf "$SRC_DIR"
|
||||||
|
rm -f "$TARBALL"
|
||||||
|
|
||||||
|
echo "[fetch-libsodium] Done (universal): $INSTALL_DIR/lib/libsodium.a"
|
||||||
|
lipo -info "$INSTALL_DIR/lib/libsodium.a"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
echo "[fetch-libsodium] Configuring for target: $TARGET ..."
|
echo "[fetch-libsodium] Configuring for target: $TARGET ..."
|
||||||
./configure "${CONFIGURE_ARGS[@]}" > /dev/null
|
./configure "${CONFIGURE_ARGS[@]}" > /dev/null
|
||||||
|
|
||||||
|
|||||||
36
scripts/fix_mojibake.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Fix mojibake en-dash (and other common patterns) in translation JSON files."""
|
||||||
|
import os
|
||||||
|
import glob
|
||||||
|
|
||||||
|
LANG_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'res', 'lang')
|
||||||
|
|
||||||
|
# Common mojibake patterns: UTF-8 bytes interpreted as Latin-1
|
||||||
|
MOJIBAKE_FIXES = {
|
||||||
|
'\u00e2\u0080\u0093': '\u2013', # en dash
|
||||||
|
'\u00e2\u0080\u0094': '\u2014', # em dash
|
||||||
|
'\u00e2\u0080\u0099': '\u2019', # right single quote
|
||||||
|
'\u00e2\u0080\u009c': '\u201c', # left double quote
|
||||||
|
'\u00e2\u0080\u009d': '\u201d', # right double quote
|
||||||
|
'\u00e2\u0080\u00a6': '\u2026', # ellipsis
|
||||||
|
}
|
||||||
|
|
||||||
|
total_fixed = 0
|
||||||
|
for path in sorted(glob.glob(os.path.join(LANG_DIR, '*.json'))):
|
||||||
|
with open(path, 'r', encoding='utf-8') as f:
|
||||||
|
raw = f.read()
|
||||||
|
|
||||||
|
original = raw
|
||||||
|
for bad, good in MOJIBAKE_FIXES.items():
|
||||||
|
if bad in raw:
|
||||||
|
count = raw.count(bad)
|
||||||
|
raw = raw.replace(bad, good)
|
||||||
|
lang = os.path.basename(path)
|
||||||
|
print(f" {lang}: fixed {count} x {repr(good)}")
|
||||||
|
total_fixed += count
|
||||||
|
|
||||||
|
if raw != original:
|
||||||
|
with open(path, 'w', encoding='utf-8') as f:
|
||||||
|
f.write(raw)
|
||||||
|
|
||||||
|
print(f"\nTotal fixes: {total_fixed}")
|
||||||
645
scripts/gen_de.py
Normal file
@@ -0,0 +1,645 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Generate German (de) translations for ObsidianDragon wallet."""
|
||||||
|
import json, os
|
||||||
|
|
||||||
|
translations = {
|
||||||
|
"24h_change": "24h Änderung",
|
||||||
|
"24h_volume": "24h Volumen",
|
||||||
|
"about": "Über",
|
||||||
|
"about_block_explorer": "Block-Explorer",
|
||||||
|
"about_block_height": "Blockhöhe:",
|
||||||
|
"about_build_date": "Erstellungsdatum:",
|
||||||
|
"about_build_type": "Build-Typ:",
|
||||||
|
"about_chain": "Chain:",
|
||||||
|
"about_connections": "Verbindungen:",
|
||||||
|
"about_credits": "Danksagungen",
|
||||||
|
"about_daemon": "Daemon:",
|
||||||
|
"about_debug": "Debug",
|
||||||
|
"about_dragonx": "Über ObsidianDragon",
|
||||||
|
"about_edition": "ImGui Edition",
|
||||||
|
"about_github": "GitHub",
|
||||||
|
"about_imgui": "ImGui:",
|
||||||
|
"about_license": "Lizenz",
|
||||||
|
"about_license_text": "Diese Software wird unter der GNU General Public License v3 (GPLv3) veröffentlicht. Sie dürfen diese Software gemäß den Lizenzbedingungen frei verwenden, modifizieren und verbreiten.",
|
||||||
|
"about_peers_count": "%zu Peers",
|
||||||
|
"about_release": "Release",
|
||||||
|
"about_title": "Über ObsidianDragon",
|
||||||
|
"about_version": "Version:",
|
||||||
|
"about_website": "Webseite",
|
||||||
|
"acrylic": "Acryl",
|
||||||
|
"add": "Hinzufügen",
|
||||||
|
"address": "Adresse",
|
||||||
|
"address_book_add": "Adresse hinzufügen",
|
||||||
|
"address_book_add_new": "Neue hinzufügen",
|
||||||
|
"address_book_added": "Adresse zum Buch hinzugefügt",
|
||||||
|
"address_book_count": "%zu Adressen gespeichert",
|
||||||
|
"address_book_deleted": "Eintrag gelöscht",
|
||||||
|
"address_book_edit": "Adresse bearbeiten",
|
||||||
|
"address_book_empty": "Keine gespeicherten Adressen. Klicken Sie auf 'Neue hinzufügen', um eine hinzuzufügen.",
|
||||||
|
"address_book_exists": "Adresse existiert bereits im Buch",
|
||||||
|
"address_book_title": "Adressbuch",
|
||||||
|
"address_book_update_failed": "Aktualisierung fehlgeschlagen - Adresse könnte doppelt sein",
|
||||||
|
"address_book_updated": "Adresse aktualisiert",
|
||||||
|
"address_copied": "Adresse in Zwischenablage kopiert",
|
||||||
|
"address_details": "Adressdetails",
|
||||||
|
"address_label": "Adresse:",
|
||||||
|
"address_upper": "ADRESSE",
|
||||||
|
"address_url": "Adress-URL",
|
||||||
|
"addresses_appear_here": "Ihre Empfangsadressen erscheinen hier, sobald Sie verbunden sind.",
|
||||||
|
"advanced": "ERWEITERT",
|
||||||
|
"all_filter": "Alle",
|
||||||
|
"allow_custom_fees": "Benutzerdefinierte Gebühren erlauben",
|
||||||
|
"amount": "Betrag",
|
||||||
|
"amount_details": "BETRAGSDETAILS",
|
||||||
|
"amount_exceeds_balance": "Betrag übersteigt Guthaben",
|
||||||
|
"amount_label": "Betrag:",
|
||||||
|
"appearance": "ERSCHEINUNGSBILD",
|
||||||
|
"auto_shield": "Mining automatisch abschirmen",
|
||||||
|
"available": "Verfügbar",
|
||||||
|
"backup_backing_up": "Sicherung läuft...",
|
||||||
|
"backup_create": "Sicherung erstellen",
|
||||||
|
"backup_created": "Wallet-Sicherung erstellt",
|
||||||
|
"backup_data": "SICHERUNG & DATEN",
|
||||||
|
"backup_description": "Erstellen Sie eine Sicherung Ihrer wallet.dat-Datei. Diese Datei enthält alle Ihre privaten Schlüssel und den Transaktionsverlauf. Bewahren Sie die Sicherung an einem sicheren Ort auf.",
|
||||||
|
"backup_destination": "Sicherungsziel:",
|
||||||
|
"backup_tip_external": "Speichern Sie Sicherungen auf externen Laufwerken oder Cloud-Speicher",
|
||||||
|
"backup_tip_multiple": "Erstellen Sie mehrere Sicherungen an verschiedenen Orten",
|
||||||
|
"backup_tip_test": "Testen Sie regelmäßig die Wiederherstellung aus der Sicherung",
|
||||||
|
"backup_tips": "Tipps:",
|
||||||
|
"backup_title": "Wallet sichern",
|
||||||
|
"backup_wallet": "Wallet sichern...",
|
||||||
|
"backup_wallet_not_found": "Warnung: wallet.dat nicht am erwarteten Speicherort gefunden",
|
||||||
|
"balance": "Guthaben",
|
||||||
|
"balance_layout": "Guthaben-Layout",
|
||||||
|
"ban": "Sperren",
|
||||||
|
"banned_peers": "Gesperrte Peers",
|
||||||
|
"block": "Block",
|
||||||
|
"block_bits": "Bits:",
|
||||||
|
"block_click_next": "Klicken für nächsten Block",
|
||||||
|
"block_click_prev": "Klicken für vorherigen Block",
|
||||||
|
"block_explorer": "Block-Explorer",
|
||||||
|
"block_get_info": "Block-Info abrufen",
|
||||||
|
"block_hash": "Block-Hash:",
|
||||||
|
"block_height": "Blockhöhe:",
|
||||||
|
"block_info_title": "Block-Informationen",
|
||||||
|
"block_merkle_root": "Merkle-Root:",
|
||||||
|
"block_nav_next": "Weiter >>",
|
||||||
|
"block_nav_prev": "<< Zurück",
|
||||||
|
"block_next": "Nächster Block:",
|
||||||
|
"block_previous": "Vorheriger Block:",
|
||||||
|
"block_size": "Größe:",
|
||||||
|
"block_timestamp": "Zeitstempel:",
|
||||||
|
"block_transactions": "Transaktionen:",
|
||||||
|
"blockchain_syncing": "Blockchain synchronisiert (%.1f%%)... Guthaben könnten ungenau sein.",
|
||||||
|
"cancel": "Abbrechen",
|
||||||
|
"characters": "Zeichen",
|
||||||
|
"clear": "Leeren",
|
||||||
|
"clear_all_bans": "Alle Sperren aufheben",
|
||||||
|
"clear_form_confirm": "Alle Formularfelder leeren?",
|
||||||
|
"clear_request": "Anfrage leeren",
|
||||||
|
"click_copy_address": "Klicken zum Kopieren der Adresse",
|
||||||
|
"click_copy_uri": "Klicken zum Kopieren der URI",
|
||||||
|
"close": "Schließen",
|
||||||
|
"conf_count": "%d Best.",
|
||||||
|
"confirm_and_send": "Bestätigen & Senden",
|
||||||
|
"confirm_send": "Senden bestätigen",
|
||||||
|
"confirm_transaction": "Transaktion bestätigen",
|
||||||
|
"confirmations": "Bestätigungen",
|
||||||
|
"confirmations_display": "%d Bestätigungen | %s",
|
||||||
|
"confirmed": "Bestätigt",
|
||||||
|
"connected": "Verbunden",
|
||||||
|
"connected_peers": "Verbundene Peers",
|
||||||
|
"connecting": "Verbinde...",
|
||||||
|
"console": "Konsole",
|
||||||
|
"console_auto_scroll": "Automatisch scrollen",
|
||||||
|
"console_available_commands": "Verfügbare Befehle:",
|
||||||
|
"console_capturing_output": "Erfasse Daemon-Ausgabe...",
|
||||||
|
"console_clear": "Leeren",
|
||||||
|
"console_clear_console": "Konsole leeren",
|
||||||
|
"console_cleared": "Konsole geleert",
|
||||||
|
"console_click_commands": "Befehle oben klicken zum Einfügen",
|
||||||
|
"console_click_insert": "Klicken zum Einfügen",
|
||||||
|
"console_click_insert_params": "Klicken zum Einfügen mit Parametern",
|
||||||
|
"console_close": "Schließen",
|
||||||
|
"console_commands": "Befehle",
|
||||||
|
"console_common_rpc": "Häufige RPC-Befehle:",
|
||||||
|
"console_completions": "Vervollständigungen:",
|
||||||
|
"console_connected": "Verbunden mit Daemon",
|
||||||
|
"console_copy_all": "Alles kopieren",
|
||||||
|
"console_copy_selected": "Kopieren",
|
||||||
|
"console_daemon": "Daemon",
|
||||||
|
"console_daemon_error": "Daemon-Fehler!",
|
||||||
|
"console_daemon_started": "Daemon gestartet",
|
||||||
|
"console_daemon_stopped": "Daemon gestoppt",
|
||||||
|
"console_disconnected": "Vom Daemon getrennt",
|
||||||
|
"console_errors": "Fehler",
|
||||||
|
"console_filter_hint": "Ausgabe filtern...",
|
||||||
|
"console_help_clear": " clear - Konsole leeren",
|
||||||
|
"console_help_getbalance": " getbalance - Transparentes Guthaben anzeigen",
|
||||||
|
"console_help_getblockcount": " getblockcount - Aktuelle Blockhöhe anzeigen",
|
||||||
|
"console_help_getinfo": " getinfo - Knoteninformationen anzeigen",
|
||||||
|
"console_help_getmininginfo": " getmininginfo - Mining-Status anzeigen",
|
||||||
|
"console_help_getpeerinfo": " getpeerinfo - Verbundene Peers anzeigen",
|
||||||
|
"console_help_gettotalbalance": " gettotalbalance - Gesamtguthaben anzeigen",
|
||||||
|
"console_help_help": " help - Diese Hilfe anzeigen",
|
||||||
|
"console_help_setgenerate": " setgenerate - Mining steuern",
|
||||||
|
"console_help_stop": " stop - Daemon stoppen",
|
||||||
|
"console_line_count": "%zu Zeilen",
|
||||||
|
"console_new_lines": "%d neue Zeilen",
|
||||||
|
"console_no_daemon": "Kein Daemon",
|
||||||
|
"console_not_connected": "Fehler: Nicht mit Daemon verbunden",
|
||||||
|
"console_rpc_reference": "RPC-Befehlsreferenz",
|
||||||
|
"console_scanline": "Konsolen-Scanline",
|
||||||
|
"console_search_commands": "Befehle suchen...",
|
||||||
|
"console_select_all": "Alles auswählen",
|
||||||
|
"console_show_daemon_output": "Daemon-Ausgabe anzeigen",
|
||||||
|
"console_show_errors_only": "Nur Fehler anzeigen",
|
||||||
|
"console_show_rpc_ref": "RPC-Befehlsreferenz anzeigen",
|
||||||
|
"console_showing_lines": "Zeige %zu von %zu Zeilen",
|
||||||
|
"console_starting_node": "Knoten wird gestartet...",
|
||||||
|
"console_status_error": "Fehler",
|
||||||
|
"console_status_running": "Läuft",
|
||||||
|
"console_status_starting": "Startet",
|
||||||
|
"console_status_stopped": "Gestoppt",
|
||||||
|
"console_status_stopping": "Stoppt",
|
||||||
|
"console_status_unknown": "Unbekannt",
|
||||||
|
"console_tab_completion": "Tab zur Vervollständigung",
|
||||||
|
"console_type_help": "Geben Sie 'help' ein für verfügbare Befehle",
|
||||||
|
"console_welcome": "Willkommen bei ObsidianDragon Konsole",
|
||||||
|
"console_zoom_in": "Vergrößern",
|
||||||
|
"console_zoom_out": "Verkleinern",
|
||||||
|
"copy": "Kopieren",
|
||||||
|
"copy_address": "Vollständige Adresse kopieren",
|
||||||
|
"copy_error": "Fehler kopieren",
|
||||||
|
"copy_to_clipboard": "In Zwischenablage kopieren",
|
||||||
|
"copy_txid": "TxID kopieren",
|
||||||
|
"copy_uri": "URI kopieren",
|
||||||
|
"current_price": "Aktueller Preis",
|
||||||
|
"custom_fees": "Benutzerdefinierte Gebühren",
|
||||||
|
"dark": "Dunkel",
|
||||||
|
"date": "Datum",
|
||||||
|
"date_label": "Datum:",
|
||||||
|
"delete": "Löschen",
|
||||||
|
"difficulty": "Schwierigkeit",
|
||||||
|
"disconnected": "Getrennt",
|
||||||
|
"dismiss": "Verwerfen",
|
||||||
|
"display": "Anzeige",
|
||||||
|
"dragonx_green": "DragonX (Grün)",
|
||||||
|
"edit": "Bearbeiten",
|
||||||
|
"error": "Fehler",
|
||||||
|
"est_time_to_block": "Gesch. Zeit bis Block",
|
||||||
|
"exit": "Beenden",
|
||||||
|
"explorer": "EXPLORER",
|
||||||
|
"export": "Exportieren",
|
||||||
|
"export_csv": "CSV exportieren",
|
||||||
|
"export_keys_btn": "Schlüssel exportieren",
|
||||||
|
"export_keys_danger": "ACHTUNG: Dies exportiert ALLE privaten Schlüssel aus Ihrer Wallet! Jeder mit Zugriff auf diese Datei kann Ihre Gelder stehlen. Sicher aufbewahren und nach Gebrauch löschen.",
|
||||||
|
"export_keys_include_t": "T-Adressen einschließen (transparent)",
|
||||||
|
"export_keys_include_z": "Z-Adressen einschließen (abgeschirmt)",
|
||||||
|
"export_keys_options": "Export-Optionen:",
|
||||||
|
"export_keys_success": "Schlüssel erfolgreich exportiert",
|
||||||
|
"export_keys_title": "Alle privaten Schlüssel exportieren",
|
||||||
|
"export_private_key": "Privaten Schlüssel exportieren",
|
||||||
|
"export_tx_count": "%zu Transaktionen als CSV exportieren.",
|
||||||
|
"export_tx_file_fail": "CSV-Datei konnte nicht erstellt werden",
|
||||||
|
"export_tx_none": "Keine Transaktionen zum Exportieren",
|
||||||
|
"export_tx_success": "Transaktionen erfolgreich exportiert",
|
||||||
|
"export_tx_title": "Transaktionen als CSV exportieren",
|
||||||
|
"export_viewing_key": "Betrachtungsschlüssel exportieren",
|
||||||
|
"failed_create_shielded": "Abgeschirmte Adresse konnte nicht erstellt werden",
|
||||||
|
"failed_create_transparent": "Transparente Adresse konnte nicht erstellt werden",
|
||||||
|
"fee": "Gebühr",
|
||||||
|
"fee_high": "Hoch",
|
||||||
|
"fee_label": "Gebühr:",
|
||||||
|
"fee_low": "Niedrig",
|
||||||
|
"fee_normal": "Normal",
|
||||||
|
"fetch_prices": "Preise abrufen",
|
||||||
|
"file": "Datei",
|
||||||
|
"file_save_location": "Datei wird gespeichert in: ~/.config/ObsidianDragon/",
|
||||||
|
"font_scale": "Schriftgröße",
|
||||||
|
"from": "Von",
|
||||||
|
"from_upper": "VON",
|
||||||
|
"full_details": "Alle Details",
|
||||||
|
"general": "Allgemein",
|
||||||
|
"go_to_receive": "Zum Empfangen",
|
||||||
|
"height": "Höhe",
|
||||||
|
"help": "Hilfe",
|
||||||
|
"hide": "Ausblenden",
|
||||||
|
"history": "Verlauf",
|
||||||
|
"immature_type": "Unreif",
|
||||||
|
"import": "Importieren",
|
||||||
|
"import_key_btn": "Schlüssel importieren",
|
||||||
|
"import_key_formats": "Unterstützte Schlüsselformate:",
|
||||||
|
"import_key_full_rescan": "(0 = vollständiger Rescan)",
|
||||||
|
"import_key_label": "Privater Schlüssel:",
|
||||||
|
"import_key_no_valid": "Keine gültigen Schlüssel in der Eingabe gefunden",
|
||||||
|
"import_key_rescan": "Blockchain nach Import neu scannen",
|
||||||
|
"import_key_start_height": "Starthöhe:",
|
||||||
|
"import_key_success": "Schlüssel erfolgreich importiert",
|
||||||
|
"import_key_t_format": "T-Adresse WIF private Schlüssel",
|
||||||
|
"import_key_title": "Privaten Schlüssel importieren",
|
||||||
|
"import_key_tooltip": "Geben Sie einen oder mehrere private Schlüssel ein, einen pro Zeile.\nUnterstützt sowohl z-Adresse als auch t-Adresse Schlüssel.\nZeilen die mit # beginnen werden als Kommentare behandelt.",
|
||||||
|
"import_key_warning": "Warnung: Teilen Sie niemals Ihre privaten Schlüssel! Das Importieren von Schlüsseln aus nicht vertrauenswürdigen Quellen kann Ihr Wallet gefährden.",
|
||||||
|
"import_key_z_format": "Z-Adresse Ausgabeschlüssel (secret-extended-key-...)",
|
||||||
|
"import_private_key": "Privaten Schlüssel importieren...",
|
||||||
|
"invalid_address": "Ungültiges Adressformat",
|
||||||
|
"ip_address": "IP-Adresse",
|
||||||
|
"keep": "Behalten",
|
||||||
|
"keep_daemon": "Daemon weiterlaufen lassen",
|
||||||
|
"key_export_fetching": "Schlüssel wird aus Wallet abgerufen...",
|
||||||
|
"key_export_private_key": "Privater Schlüssel:",
|
||||||
|
"key_export_private_warning": "Halten Sie diesen Schlüssel GEHEIM! Jeder mit diesem Schlüssel kann Ihre Gelder ausgeben. Teilen Sie ihn niemals online oder mit nicht vertrauenswürdigen Parteien.",
|
||||||
|
"key_export_reveal": "Schlüssel anzeigen",
|
||||||
|
"key_export_viewing_key": "Betrachtungsschlüssel:",
|
||||||
|
"key_export_viewing_warning": "Dieser Betrachtungsschlüssel ermöglicht es anderen, Ihre eingehenden Transaktionen und Ihr Guthaben zu sehen, aber NICHT Ihre Gelder auszugeben. Teilen Sie ihn nur mit vertrauenswürdigen Parteien.",
|
||||||
|
"label": "Bezeichnung:",
|
||||||
|
"language": "Sprache",
|
||||||
|
"light": "Hell",
|
||||||
|
"loading": "Laden...",
|
||||||
|
"loading_addresses": "Adressen werden geladen...",
|
||||||
|
"local_hashrate": "Lokale Hashrate",
|
||||||
|
"low_spec_mode": "Energiesparmodus",
|
||||||
|
"market": "Markt",
|
||||||
|
"market_12h": "12h",
|
||||||
|
"market_18h": "18h",
|
||||||
|
"market_24h": "24h",
|
||||||
|
"market_24h_volume": "24H VOLUMEN",
|
||||||
|
"market_6h": "6h",
|
||||||
|
"market_attribution": "Preisdaten von NonKYC",
|
||||||
|
"market_btc_price": "BTC PREIS",
|
||||||
|
"market_cap": "Marktkapitalisierung",
|
||||||
|
"market_no_history": "Kein Preisverlauf verfügbar",
|
||||||
|
"market_no_price": "Keine Preisdaten",
|
||||||
|
"market_now": "Jetzt",
|
||||||
|
"market_pct_shielded": "%.0f%% Abgeschirmt",
|
||||||
|
"market_portfolio": "PORTFOLIO",
|
||||||
|
"market_price_unavailable": "Preisdaten nicht verfügbar",
|
||||||
|
"market_refresh_price": "Preisdaten aktualisieren",
|
||||||
|
"market_trade_on": "Handeln auf %s",
|
||||||
|
"mature": "Reif",
|
||||||
|
"max": "Max",
|
||||||
|
"memo": "Memo (optional, verschlüsselt)",
|
||||||
|
"memo_label": "Memo:",
|
||||||
|
"memo_optional": "MEMO (OPTIONAL)",
|
||||||
|
"memo_upper": "MEMO",
|
||||||
|
"memo_z_only": "Hinweis: Memos sind nur beim Senden an abgeschirmte (z) Adressen verfügbar",
|
||||||
|
"merge_description": "Mehrere UTXOs zu einer einzelnen abgeschirmten Adresse zusammenführen. Dies kann die Wallet-Größe reduzieren und die Privatsphäre verbessern.",
|
||||||
|
"merge_funds": "Gelder zusammenführen",
|
||||||
|
"merge_started": "Zusammenführung gestartet",
|
||||||
|
"merge_title": "An Adresse zusammenführen",
|
||||||
|
"mine_when_idle": "Im Leerlauf minen",
|
||||||
|
"mined": "gemined",
|
||||||
|
"mined_filter": "Gemined",
|
||||||
|
"mined_type": "Gemined",
|
||||||
|
"mined_upper": "GEMINED",
|
||||||
|
"miner_fee": "Miner-Gebühr",
|
||||||
|
"mining": "Mining",
|
||||||
|
"mining_active": "Aktiv",
|
||||||
|
"mining_address_copied": "Mining-Adresse kopiert",
|
||||||
|
"mining_all_time": "Gesamt",
|
||||||
|
"mining_already_saved": "Pool-URL bereits gespeichert",
|
||||||
|
"mining_block_copied": "Block-Hash kopiert",
|
||||||
|
"mining_chart_1m_ago": "vor 1m",
|
||||||
|
"mining_chart_5m_ago": "vor 5m",
|
||||||
|
"mining_chart_now": "Jetzt",
|
||||||
|
"mining_click": "Klicken",
|
||||||
|
"mining_click_copy_address": "Klicken zum Kopieren der Adresse",
|
||||||
|
"mining_click_copy_block": "Klicken zum Kopieren des Block-Hash",
|
||||||
|
"mining_click_copy_difficulty": "Klicken zum Kopieren der Schwierigkeit",
|
||||||
|
"mining_connected": "Verbunden",
|
||||||
|
"mining_connecting": "Verbinde...",
|
||||||
|
"mining_control": "Mining-Steuerung",
|
||||||
|
"mining_difficulty_copied": "Schwierigkeit kopiert",
|
||||||
|
"mining_est_block": "Gesch. Block",
|
||||||
|
"mining_est_daily": "Gesch. täglich",
|
||||||
|
"mining_filter_all": "Alle",
|
||||||
|
"mining_filter_tip_all": "Alle Einnahmen anzeigen",
|
||||||
|
"mining_filter_tip_pool": "Nur Pool-Einnahmen anzeigen",
|
||||||
|
"mining_filter_tip_solo": "Nur Solo-Einnahmen anzeigen",
|
||||||
|
"mining_idle_off_tooltip": "Leerlauf-Mining aktivieren",
|
||||||
|
"mining_idle_on_tooltip": "Leerlauf-Mining deaktivieren",
|
||||||
|
"mining_local_hashrate": "Lokale Hashrate",
|
||||||
|
"mining_mine": "Minen",
|
||||||
|
"mining_mining_addr": "Mining-Adr.",
|
||||||
|
"mining_network": "Netzwerk",
|
||||||
|
"mining_no_blocks_yet": "Noch keine Blöcke gefunden",
|
||||||
|
"mining_no_payouts_yet": "Noch keine Pool-Auszahlungen",
|
||||||
|
"mining_no_saved_addresses": "Keine gespeicherten Adressen",
|
||||||
|
"mining_no_saved_pools": "Keine gespeicherten Pools",
|
||||||
|
"mining_off": "Mining ist AUS",
|
||||||
|
"mining_on": "Mining ist AN",
|
||||||
|
"mining_open_in_explorer": "Im Explorer öffnen",
|
||||||
|
"mining_payout_address": "Auszahlungsadresse",
|
||||||
|
"mining_payout_tooltip": "Adresse für Mining-Belohnungen",
|
||||||
|
"mining_pool": "Pool",
|
||||||
|
"mining_pool_hashrate": "Pool-Hashrate",
|
||||||
|
"mining_pool_url": "Pool-URL",
|
||||||
|
"mining_recent_blocks": "LETZTE BLÖCKE",
|
||||||
|
"mining_recent_payouts": "LETZTE POOL-AUSZAHLUNGEN",
|
||||||
|
"mining_remove": "Entfernen",
|
||||||
|
"mining_reset_defaults": "Standardwerte zurücksetzen",
|
||||||
|
"mining_save_payout_address": "Auszahlungsadresse speichern",
|
||||||
|
"mining_save_pool_url": "Pool-URL speichern",
|
||||||
|
"mining_saved_addresses": "Gespeicherte Adressen:",
|
||||||
|
"mining_saved_pools": "Gespeicherte Pools:",
|
||||||
|
"mining_shares": "Shares",
|
||||||
|
"mining_show_chart": "Diagramm",
|
||||||
|
"mining_show_log": "Protokoll",
|
||||||
|
"mining_solo": "Solo",
|
||||||
|
"mining_starting": "Startet...",
|
||||||
|
"mining_starting_tooltip": "Miner startet...",
|
||||||
|
"mining_statistics": "Mining-Statistiken",
|
||||||
|
"mining_stop": "Stopp",
|
||||||
|
"mining_stop_solo_for_pool": "Solo-Mining stoppen bevor Pool-Mining gestartet wird",
|
||||||
|
"mining_stop_solo_for_pool_settings": "Solo-Mining stoppen um Pool-Einstellungen zu ändern",
|
||||||
|
"mining_stopping": "Stoppt...",
|
||||||
|
"mining_stopping_tooltip": "Miner stoppt...",
|
||||||
|
"mining_syncing_tooltip": "Blockchain synchronisiert...",
|
||||||
|
"mining_threads": "Mining-Threads",
|
||||||
|
"mining_to_save": "zum Speichern",
|
||||||
|
"mining_today": "Heute",
|
||||||
|
"mining_uptime": "Laufzeit",
|
||||||
|
"mining_yesterday": "Gestern",
|
||||||
|
"network": "Netzwerk",
|
||||||
|
"network_fee": "NETZWERKGEBÜHR",
|
||||||
|
"network_hashrate": "Netzwerk-Hashrate",
|
||||||
|
"new": "+ Neu",
|
||||||
|
"new_shielded_created": "Neue abgeschirmte Adresse erstellt",
|
||||||
|
"new_t_address": "Neue T-Adresse",
|
||||||
|
"new_t_transparent": "Neue t-Adresse (Transparent)",
|
||||||
|
"new_transparent_created": "Neue transparente Adresse erstellt",
|
||||||
|
"new_z_address": "Neue Z-Adresse",
|
||||||
|
"new_z_shielded": "Neue z-Adresse (Abgeschirmt)",
|
||||||
|
"no_addresses": "Keine Adressen gefunden. Erstellen Sie eine mit den Schaltflächen oben.",
|
||||||
|
"no_addresses_available": "Keine Adressen verfügbar",
|
||||||
|
"no_addresses_match": "Keine Adressen passen zum Filter",
|
||||||
|
"no_addresses_with_balance": "Keine Adressen mit Guthaben",
|
||||||
|
"no_matching": "Keine passenden Transaktionen",
|
||||||
|
"no_recent_receives": "Keine kürzlichen Empfänge",
|
||||||
|
"no_recent_sends": "Keine kürzlichen Sendungen",
|
||||||
|
"no_transactions": "Keine Transaktionen gefunden",
|
||||||
|
"node": "KNOTEN",
|
||||||
|
"node_security": "KNOTEN & SICHERHEIT",
|
||||||
|
"noise": "Rauschen",
|
||||||
|
"not_connected": "Nicht mit Daemon verbunden...",
|
||||||
|
"not_connected_to_daemon": "Nicht mit Daemon verbunden",
|
||||||
|
"notes": "Notizen",
|
||||||
|
"notes_optional": "Notizen (optional):",
|
||||||
|
"output_filename": "Ausgabedateiname:",
|
||||||
|
"overview": "Übersicht",
|
||||||
|
"paste": "Einfügen",
|
||||||
|
"paste_from_clipboard": "Aus Zwischenablage einfügen",
|
||||||
|
"pay_from": "Zahlen von",
|
||||||
|
"payment_request": "ZAHLUNGSANFRAGE",
|
||||||
|
"payment_request_copied": "Zahlungsanfrage kopiert",
|
||||||
|
"payment_uri_copied": "Zahlungs-URI kopiert",
|
||||||
|
"peers": "Peers",
|
||||||
|
"peers_avg_ping": "Durchschn. Ping",
|
||||||
|
"peers_ban_24h": "Peer 24h sperren",
|
||||||
|
"peers_ban_score": "Sperr-Score: %d",
|
||||||
|
"peers_banned": "Gesperrt",
|
||||||
|
"peers_banned_count": "Gesperrt: %d",
|
||||||
|
"peers_best_block": "Bester Block",
|
||||||
|
"peers_blockchain": "BLOCKCHAIN",
|
||||||
|
"peers_blocks": "Blöcke",
|
||||||
|
"peers_blocks_left": "%d Blöcke übrig",
|
||||||
|
"peers_clear_all_bans": "Alle Sperren aufheben",
|
||||||
|
"peers_click_copy": "Klicken zum Kopieren",
|
||||||
|
"peers_connected": "Verbunden",
|
||||||
|
"peers_connected_count": "Verbunden: %d",
|
||||||
|
"peers_copy_ip": "IP kopieren",
|
||||||
|
"peers_dir_in": "Ein",
|
||||||
|
"peers_dir_out": "Aus",
|
||||||
|
"peers_hash_copied": "Hash kopiert",
|
||||||
|
"peers_hashrate": "Hashrate",
|
||||||
|
"peers_in_out": "Ein/Aus",
|
||||||
|
"peers_longest": "Längste",
|
||||||
|
"peers_longest_chain": "Längste Chain",
|
||||||
|
"peers_memory": "Speicher",
|
||||||
|
"peers_no_banned": "Keine gesperrten Peers",
|
||||||
|
"peers_no_connected": "Keine verbundenen Peers",
|
||||||
|
"peers_no_tls": "Kein TLS",
|
||||||
|
"peers_notarized": "Notarisiert",
|
||||||
|
"peers_p2p_port": "P2P-Port",
|
||||||
|
"peers_protocol": "Protokoll",
|
||||||
|
"peers_received": "Empfangen",
|
||||||
|
"peers_refresh": "Aktualisieren",
|
||||||
|
"peers_refresh_tooltip": "Peer-Liste aktualisieren",
|
||||||
|
"peers_refreshing": "Aktualisiere...",
|
||||||
|
"peers_sent": "Gesendet",
|
||||||
|
"peers_tt_id": "ID: %d",
|
||||||
|
"peers_tt_received": "Empfangen: %s",
|
||||||
|
"peers_tt_sent": "Gesendet: %s",
|
||||||
|
"peers_tt_services": "Dienste: %s",
|
||||||
|
"peers_tt_start_height": "Starthöhe: %d",
|
||||||
|
"peers_tt_synced": "Synchronisiert H/B: %d/%d",
|
||||||
|
"peers_tt_tls_cipher": "TLS: %s",
|
||||||
|
"peers_unban": "Entsperren",
|
||||||
|
"peers_upper": "PEERS",
|
||||||
|
"peers_version": "Version",
|
||||||
|
"pending": "Ausstehend",
|
||||||
|
"ping": "Ping",
|
||||||
|
"price_chart": "Preisdiagramm",
|
||||||
|
"qr_code": "QR-Code",
|
||||||
|
"qr_failed": "QR-Code konnte nicht generiert werden",
|
||||||
|
"qr_title": "QR-Code",
|
||||||
|
"qr_unavailable": "QR nicht verfügbar",
|
||||||
|
"receive": "Empfangen",
|
||||||
|
"received": "empfangen",
|
||||||
|
"received_filter": "Empfangen",
|
||||||
|
"received_label": "Empfangen",
|
||||||
|
"received_upper": "EMPFANGEN",
|
||||||
|
"receiving_addresses": "Ihre Empfangsadressen",
|
||||||
|
"recent_received": "KÜRZLICH EMPFANGEN",
|
||||||
|
"recent_sends": "KÜRZLICH GESENDET",
|
||||||
|
"recipient": "EMPFÄNGER",
|
||||||
|
"recv_type": "Empf.",
|
||||||
|
"refresh": "Aktualisieren",
|
||||||
|
"refresh_now": "Jetzt aktualisieren",
|
||||||
|
"report_bug": "Fehler melden",
|
||||||
|
"request_amount": "Betrag (optional):",
|
||||||
|
"request_copy_uri": "URI kopieren",
|
||||||
|
"request_description": "Erstellen Sie eine Zahlungsanfrage, die andere scannen oder kopieren können. Der QR-Code enthält Ihre Adresse und optionalen Betrag/Memo.",
|
||||||
|
"request_label": "Bezeichnung (optional):",
|
||||||
|
"request_memo": "Memo (optional):",
|
||||||
|
"request_payment": "Zahlung anfordern",
|
||||||
|
"request_payment_uri": "Zahlungs-URI:",
|
||||||
|
"request_receive_address": "Empfangsadresse:",
|
||||||
|
"request_select_address": "Adresse auswählen...",
|
||||||
|
"request_shielded_addrs": "-- Abgeschirmte Adressen --",
|
||||||
|
"request_title": "Zahlung anfordern",
|
||||||
|
"request_transparent_addrs": "-- Transparente Adressen --",
|
||||||
|
"request_uri_copied": "Zahlungs-URI in Zwischenablage kopiert",
|
||||||
|
"rescan": "Neu scannen",
|
||||||
|
"reset_to_defaults": "Standardwerte zurücksetzen",
|
||||||
|
"review_send": "Senden prüfen",
|
||||||
|
"rpc_host": "RPC-Host",
|
||||||
|
"rpc_pass": "Passwort",
|
||||||
|
"rpc_port": "Port",
|
||||||
|
"rpc_user": "Benutzername",
|
||||||
|
"save": "Speichern",
|
||||||
|
"save_settings": "Einstellungen speichern",
|
||||||
|
"save_z_transactions": "Z-Tx in Tx-Liste speichern",
|
||||||
|
"search_placeholder": "Suchen...",
|
||||||
|
"security": "SICHERHEIT",
|
||||||
|
"select_address": "Adresse auswählen...",
|
||||||
|
"select_receiving_address": "Empfangsadresse auswählen...",
|
||||||
|
"select_source_address": "Quelladresse auswählen...",
|
||||||
|
"send": "Senden",
|
||||||
|
"send_amount": "Betrag",
|
||||||
|
"send_amount_details": "BETRAGSDETAILS",
|
||||||
|
"send_amount_upper": "BETRAG",
|
||||||
|
"send_clear_fields": "Alle Formularfelder leeren?",
|
||||||
|
"send_copy_error": "Fehler kopieren",
|
||||||
|
"send_dismiss": "Verwerfen",
|
||||||
|
"send_error_copied": "Fehler in Zwischenablage kopiert",
|
||||||
|
"send_error_prefix": "Fehler: %s",
|
||||||
|
"send_exceeds_available": "Übersteigt verfügbar (%.8f)",
|
||||||
|
"send_fee": "Gebühr",
|
||||||
|
"send_fee_high": "Hoch",
|
||||||
|
"send_fee_low": "Niedrig",
|
||||||
|
"send_fee_normal": "Normal",
|
||||||
|
"send_form_restored": "Formular wiederhergestellt",
|
||||||
|
"send_from_this_address": "Von dieser Adresse senden",
|
||||||
|
"send_go_to_receive": "Zum Empfangen",
|
||||||
|
"send_keep": "Behalten",
|
||||||
|
"send_network_fee": "NETZWERKGEBÜHR",
|
||||||
|
"send_no_balance": "Kein Guthaben",
|
||||||
|
"send_no_recent": "Keine kürzlichen Sendungen",
|
||||||
|
"send_recent_sends": "KÜRZLICH GESENDET",
|
||||||
|
"send_recipient": "EMPFÄNGER",
|
||||||
|
"send_select_source": "Quelladresse auswählen...",
|
||||||
|
"send_sending_from": "SENDEN VON",
|
||||||
|
"send_submitting": "Transaktion wird übermittelt...",
|
||||||
|
"send_switch_to_receive": "Wechseln Sie zu Empfangen, um Ihre Adresse zu erhalten und Gelder zu empfangen.",
|
||||||
|
"send_to": "Senden an",
|
||||||
|
"send_tooltip_enter_amount": "Geben Sie einen Betrag zum Senden ein",
|
||||||
|
"send_tooltip_exceeds_balance": "Betrag übersteigt verfügbares Guthaben",
|
||||||
|
"send_tooltip_in_progress": "Transaktion bereits in Bearbeitung",
|
||||||
|
"send_tooltip_invalid_address": "Geben Sie eine gültige Empfängeradresse ein",
|
||||||
|
"send_tooltip_not_connected": "Nicht mit Daemon verbunden",
|
||||||
|
"send_tooltip_select_source": "Wählen Sie zuerst eine Quelladresse",
|
||||||
|
"send_tooltip_syncing": "Warten Sie auf die Blockchain-Synchronisierung",
|
||||||
|
"send_total": "Gesamt",
|
||||||
|
"send_transaction": "Transaktion senden",
|
||||||
|
"send_tx_failed": "Transaktion fehlgeschlagen",
|
||||||
|
"send_tx_sent": "Transaktion gesendet!",
|
||||||
|
"send_tx_success": "Transaktion erfolgreich gesendet!",
|
||||||
|
"send_txid_copied": "TxID in Zwischenablage kopiert",
|
||||||
|
"send_txid_label": "TxID: %s",
|
||||||
|
"send_valid_shielded": "Gültige abgeschirmte Adresse",
|
||||||
|
"send_valid_transparent": "Gültige transparente Adresse",
|
||||||
|
"send_wallet_empty": "Ihre Wallet ist leer",
|
||||||
|
"send_yes_clear": "Ja, leeren",
|
||||||
|
"sending": "Transaktion wird gesendet",
|
||||||
|
"sending_from": "SENDEN VON",
|
||||||
|
"sent": "gesendet",
|
||||||
|
"sent_filter": "Gesendet",
|
||||||
|
"sent_type": "Gesendet",
|
||||||
|
"sent_upper": "GESENDET",
|
||||||
|
"settings": "Einstellungen",
|
||||||
|
"setup_wizard": "Einrichtungsassistent",
|
||||||
|
"share": "Teilen",
|
||||||
|
"shield_check_status": "Status prüfen",
|
||||||
|
"shield_completed": "Vorgang erfolgreich abgeschlossen!",
|
||||||
|
"shield_description": "Schirmen Sie Ihre Mining-Belohnungen ab, indem Sie Coinbase-Ausgaben von transparenten Adressen an eine abgeschirmte Adresse senden. Dies verbessert die Privatsphäre, indem Ihre Mining-Einkünfte verborgen werden.",
|
||||||
|
"shield_from_address": "Von Adresse:",
|
||||||
|
"shield_funds": "Gelder abschirmen",
|
||||||
|
"shield_in_progress": "Vorgang läuft...",
|
||||||
|
"shield_max_utxos": "Max. UTXOs pro Vorgang",
|
||||||
|
"shield_merge_done": "Abschirmung/Zusammenführung abgeschlossen!",
|
||||||
|
"shield_select_z": "z-Adresse auswählen...",
|
||||||
|
"shield_started": "Abschirmvorgang gestartet",
|
||||||
|
"shield_title": "Coinbase-Belohnungen abschirmen",
|
||||||
|
"shield_to_address": "An Adresse (Abgeschirmt):",
|
||||||
|
"shield_utxo_limit": "UTXO-Limit:",
|
||||||
|
"shield_wildcard_hint": "Verwenden Sie '*' um von allen transparenten Adressen abzuschirmen",
|
||||||
|
"shielded": "Abgeschirmt",
|
||||||
|
"shielded_to": "ABGESCHIRMT AN",
|
||||||
|
"shielded_type": "Abgeschirmt",
|
||||||
|
"show": "Anzeigen",
|
||||||
|
"show_qr_code": "QR-Code anzeigen",
|
||||||
|
"showing_transactions": "Zeige %d\xe2\x80\x93%d von %d Transaktionen (gesamt: %zu)",
|
||||||
|
"simple_background": "Einfacher Hintergrund",
|
||||||
|
"start_mining": "Mining starten",
|
||||||
|
"status": "Status",
|
||||||
|
"stop_external": "Externen Daemon stoppen",
|
||||||
|
"stop_mining": "Mining stoppen",
|
||||||
|
"submitting_transaction": "Transaktion wird übermittelt...",
|
||||||
|
"success": "Erfolg",
|
||||||
|
"summary": "Zusammenfassung",
|
||||||
|
"syncing": "Synchronisiere...",
|
||||||
|
"t_addresses": "T-Adressen",
|
||||||
|
"test_connection": "Testen",
|
||||||
|
"theme": "Design",
|
||||||
|
"theme_effects": "Design-Effekte",
|
||||||
|
"time_days_ago": "vor %d Tagen",
|
||||||
|
"time_hours_ago": "vor %d Stunden",
|
||||||
|
"time_minutes_ago": "vor %d Minuten",
|
||||||
|
"time_seconds_ago": "vor %d Sekunden",
|
||||||
|
"to": "An",
|
||||||
|
"to_upper": "AN",
|
||||||
|
"tools": "WERKZEUGE",
|
||||||
|
"total": "Gesamt",
|
||||||
|
"transaction_id": "TRANSAKTIONS-ID",
|
||||||
|
"transaction_sent": "Transaktion erfolgreich gesendet",
|
||||||
|
"transaction_sent_msg": "Transaktion gesendet!",
|
||||||
|
"transaction_url": "Transaktions-URL",
|
||||||
|
"transactions": "Transaktionen",
|
||||||
|
"transactions_upper": "TRANSAKTIONEN",
|
||||||
|
"transparent": "Transparent",
|
||||||
|
"tx_confirmations": "%d Bestätigungen",
|
||||||
|
"tx_details_title": "Transaktionsdetails",
|
||||||
|
"tx_from_address": "Von Adresse:",
|
||||||
|
"tx_id_label": "Transaktions-ID:",
|
||||||
|
"tx_immature": "UNREIF",
|
||||||
|
"tx_mined": "GEMINED",
|
||||||
|
"tx_received": "EMPFANGEN",
|
||||||
|
"tx_sent": "GESENDET",
|
||||||
|
"tx_to_address": "An Adresse:",
|
||||||
|
"tx_view_explorer": "Im Explorer anzeigen",
|
||||||
|
"txs_count": "%d Txs",
|
||||||
|
"type": "Typ",
|
||||||
|
"ui_opacity": "UI-Transparenz",
|
||||||
|
"unban": "Entsperren",
|
||||||
|
"unconfirmed": "Unbestätigt",
|
||||||
|
"undo_clear": "Leeren rückgängig",
|
||||||
|
"unknown": "Unbekannt",
|
||||||
|
"use_embedded_daemon": "Eingebetteten dragonxd verwenden",
|
||||||
|
"use_tor": "Tor verwenden",
|
||||||
|
"validate_btn": "Validieren",
|
||||||
|
"validate_description": "Geben Sie eine DragonX-Adresse ein, um zu prüfen, ob sie gültig ist und ob sie zu dieser Wallet gehört.",
|
||||||
|
"validate_invalid": "UNGÜLTIG",
|
||||||
|
"validate_is_mine": "Diese Wallet besitzt diese Adresse",
|
||||||
|
"validate_not_mine": "Nicht im Besitz dieser Wallet",
|
||||||
|
"validate_ownership": "Eigentum:",
|
||||||
|
"validate_results": "Ergebnisse:",
|
||||||
|
"validate_shielded_type": "Abgeschirmt (z-Adresse)",
|
||||||
|
"validate_status": "Status:",
|
||||||
|
"validate_title": "Adresse validieren",
|
||||||
|
"validate_transparent_type": "Transparent (t-Adresse)",
|
||||||
|
"validate_type": "Typ:",
|
||||||
|
"validate_valid": "GÜLTIG",
|
||||||
|
"validating": "Validiere...",
|
||||||
|
"verbose_logging": "Ausführliches Logging",
|
||||||
|
"version": "Version",
|
||||||
|
"view": "Ansicht",
|
||||||
|
"view_details": "Details anzeigen",
|
||||||
|
"view_on_explorer": "Im Explorer anzeigen",
|
||||||
|
"waiting_for_daemon": "Warte auf Daemon-Verbindung...",
|
||||||
|
"wallet": "WALLET",
|
||||||
|
"wallet_empty": "Ihre Wallet ist leer",
|
||||||
|
"wallet_empty_hint": "Wechseln Sie zu Empfangen, um Ihre Adresse zu erhalten und Gelder zu empfangen.",
|
||||||
|
"warning": "Warnung",
|
||||||
|
"warning_upper": "WARNUNG!",
|
||||||
|
"website": "Webseite",
|
||||||
|
"window_opacity": "Fenster-Transparenz",
|
||||||
|
"yes_clear": "Ja, leeren",
|
||||||
|
"your_addresses": "Ihre Adressen",
|
||||||
|
"z_addresses": "Z-Adressen",
|
||||||
|
}
|
||||||
|
|
||||||
|
out = os.path.join(os.path.dirname(__file__), "..", "res", "lang", "de.json")
|
||||||
|
with open(out, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(translations, f, indent=4, ensure_ascii=False, sort_keys=True)
|
||||||
|
print(f"Wrote {len(translations)} German translations to {os.path.abspath(out)}")
|
||||||
665
scripts/gen_es.py
Normal file
@@ -0,0 +1,665 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Generate complete Spanish (es.json) translations for ObsidianDragon wallet."""
|
||||||
|
import json
|
||||||
|
|
||||||
|
es = {
|
||||||
|
# ---- Navigation & Tabs ----
|
||||||
|
"overview": "Resumen",
|
||||||
|
"balance": "Saldo",
|
||||||
|
"send": "Enviar",
|
||||||
|
"receive": "Recibir",
|
||||||
|
"transactions": "Transacciones",
|
||||||
|
"history": "Historial",
|
||||||
|
"mining": "Minería",
|
||||||
|
"peers": "Nodos",
|
||||||
|
"market": "Mercado",
|
||||||
|
"settings": "Configuración",
|
||||||
|
"console": "Consola",
|
||||||
|
"tools": "HERRAMIENTAS",
|
||||||
|
"advanced": "AVANZADO",
|
||||||
|
"network": "Red",
|
||||||
|
|
||||||
|
# ---- Settings sections ----
|
||||||
|
"appearance": "APARIENCIA",
|
||||||
|
"wallet": "CARTERA",
|
||||||
|
"node_security": "NODO Y SEGURIDAD",
|
||||||
|
"node": "NODO",
|
||||||
|
"security": "SEGURIDAD",
|
||||||
|
"explorer": "EXPLORADOR",
|
||||||
|
"about": "Acerca de",
|
||||||
|
"backup_data": "RESPALDO Y DATOS",
|
||||||
|
"general": "General",
|
||||||
|
|
||||||
|
# ---- Settings options ----
|
||||||
|
"balance_layout": "Diseño de Saldo",
|
||||||
|
"low_spec_mode": "Modo bajo rendimiento",
|
||||||
|
"simple_background": "Fondo simple",
|
||||||
|
"console_scanline": "Líneas de consola",
|
||||||
|
"theme_effects": "Efectos de tema",
|
||||||
|
"language": "Idioma",
|
||||||
|
"save_z_transactions": "Guardar Z-tx en lista",
|
||||||
|
"allow_custom_fees": "Permitir comisiones personalizadas",
|
||||||
|
"custom_fees": "Comisiones personalizadas",
|
||||||
|
"auto_shield": "Auto-proteger minería",
|
||||||
|
"fetch_prices": "Obtener precios",
|
||||||
|
"use_tor": "Usar Tor",
|
||||||
|
"font_scale": "Escala de fuente",
|
||||||
|
"keep_daemon": "Mantener daemon activo",
|
||||||
|
"stop_external": "Detener daemon externo",
|
||||||
|
"mine_when_idle": "Minar en reposo",
|
||||||
|
"verbose_logging": "Registro detallado",
|
||||||
|
"acrylic": "Acrílico",
|
||||||
|
"noise": "Ruido",
|
||||||
|
"ui_opacity": "Opacidad de UI",
|
||||||
|
"window_opacity": "Opacidad de ventana",
|
||||||
|
|
||||||
|
# ---- Settings buttons ----
|
||||||
|
"save_settings": "Guardar Configuración",
|
||||||
|
"reset_to_defaults": "Restablecer Valores",
|
||||||
|
"report_bug": "Reportar Error",
|
||||||
|
"website": "Sitio Web",
|
||||||
|
"setup_wizard": "Asistente de Configuración",
|
||||||
|
"rescan": "Re-escanear",
|
||||||
|
"test_connection": "Probar",
|
||||||
|
|
||||||
|
# ---- Settings fields ----
|
||||||
|
"rpc_host": "Host RPC",
|
||||||
|
"rpc_port": "Puerto",
|
||||||
|
"rpc_user": "Usuario",
|
||||||
|
"rpc_pass": "Contraseña",
|
||||||
|
"transaction_url": "URL de Transacción",
|
||||||
|
"address_url": "URL de Dirección",
|
||||||
|
"block_explorer": "Explorador de Bloques",
|
||||||
|
|
||||||
|
# ---- Common actions ----
|
||||||
|
"add": "Agregar",
|
||||||
|
"edit": "Editar",
|
||||||
|
"delete": "Eliminar",
|
||||||
|
"cancel": "Cancelar",
|
||||||
|
"close": "Cerrar",
|
||||||
|
"clear": "Limpiar",
|
||||||
|
"copy": "Copiar",
|
||||||
|
"paste": "Pegar",
|
||||||
|
"save": "Guardar",
|
||||||
|
"refresh": "Actualizar",
|
||||||
|
"export": "Exportar",
|
||||||
|
"import": "Importar",
|
||||||
|
"show": "Mostrar",
|
||||||
|
"hide": "Ocultar",
|
||||||
|
"share": "Compartir",
|
||||||
|
"confirm_and_send": "Confirmar y Enviar",
|
||||||
|
"confirm_send": "Confirmar Envío",
|
||||||
|
"confirm_transaction": "Confirmar Transacción",
|
||||||
|
"review_send": "Revisar Envío",
|
||||||
|
"copy_address": "Copiar Dirección Completa",
|
||||||
|
"copy_to_clipboard": "Copiar al Portapapeles",
|
||||||
|
"paste_from_clipboard": "Pegar del Portapapeles",
|
||||||
|
"copy_txid": "Copiar TxID",
|
||||||
|
"copy_uri": "Copiar URI",
|
||||||
|
"copy_error": "Copiar Error",
|
||||||
|
"search_placeholder": "Buscar...",
|
||||||
|
"exit": "Salir",
|
||||||
|
"help": "Ayuda",
|
||||||
|
"file": "Archivo",
|
||||||
|
"display": "Pantalla",
|
||||||
|
"new": "+ Nuevo",
|
||||||
|
"dismiss": "Descartar",
|
||||||
|
"keep": "Mantener",
|
||||||
|
"yes_clear": "Sí, Limpiar",
|
||||||
|
"undo_clear": "Deshacer Limpieza",
|
||||||
|
|
||||||
|
# ---- Common labels ----
|
||||||
|
"address": "Dirección",
|
||||||
|
"address_label": "Dirección:",
|
||||||
|
"amount": "Cantidad",
|
||||||
|
"amount_label": "Cantidad:",
|
||||||
|
"date": "Fecha",
|
||||||
|
"date_label": "Fecha:",
|
||||||
|
"fee": "Comisión",
|
||||||
|
"fee_label": "Comisión:",
|
||||||
|
"label": "Etiqueta:",
|
||||||
|
"memo": "Memo (opcional, encriptado)",
|
||||||
|
"memo_label": "Memo:",
|
||||||
|
"notes": "Notas",
|
||||||
|
"notes_optional": "Notas (opcional):",
|
||||||
|
"total": "Total",
|
||||||
|
"from": "Desde",
|
||||||
|
"to_upper": "PARA",
|
||||||
|
"from_upper": "DESDE",
|
||||||
|
"max": "Máximo",
|
||||||
|
"characters": "caracteres",
|
||||||
|
"ping": "Ping",
|
||||||
|
"height": "Altura",
|
||||||
|
"block": "Bloque",
|
||||||
|
"available": "Disponible",
|
||||||
|
"unknown": "Desconocido",
|
||||||
|
"loading": "Cargando...",
|
||||||
|
"validating": "Validando...",
|
||||||
|
"warning_upper": "¡ADVERTENCIA!",
|
||||||
|
"output_filename": "Nombre del archivo:",
|
||||||
|
"file_save_location": "El archivo se guardará en: ~/.config/ObsidianDragon/",
|
||||||
|
"light": "Claro",
|
||||||
|
"dark": "Oscuro",
|
||||||
|
|
||||||
|
# ---- Status ----
|
||||||
|
"connected": "Conectado",
|
||||||
|
"disconnected": "Desconectado",
|
||||||
|
"connecting": "Conectando...",
|
||||||
|
"confirmed": "Confirmada",
|
||||||
|
"confirmations": "Confirmaciones",
|
||||||
|
"not_connected_to_daemon": "No conectado al daemon",
|
||||||
|
"waiting_for_daemon": "Esperando conexión al daemon...",
|
||||||
|
"blockchain_syncing": "Sincronizando blockchain (%.1f%%)... Los saldos pueden ser inexactos.",
|
||||||
|
"error": "Error",
|
||||||
|
"success": "Éxito",
|
||||||
|
"warning": "Advertencia",
|
||||||
|
|
||||||
|
# ---- Time ----
|
||||||
|
"time_days_ago": "hace %d días",
|
||||||
|
"time_hours_ago": "hace %d horas",
|
||||||
|
"time_minutes_ago": "hace %d minutos",
|
||||||
|
"time_seconds_ago": "hace %d segundos",
|
||||||
|
|
||||||
|
# ---- Transaction types/filters ----
|
||||||
|
"sent_type": "Enviado",
|
||||||
|
"sent_filter": "Enviado",
|
||||||
|
"sent_upper": "ENVIADO",
|
||||||
|
"received_label": "Recibido",
|
||||||
|
"received_filter": "Recibido",
|
||||||
|
"received_upper": "RECIBIDO",
|
||||||
|
"mined_type": "Minado",
|
||||||
|
"mined_filter": "Minado",
|
||||||
|
"mined_upper": "MINADO",
|
||||||
|
"immature_type": "Inmaduro",
|
||||||
|
"mature": "Maduro",
|
||||||
|
"recv_type": "Recibido",
|
||||||
|
"all_filter": "Todos",
|
||||||
|
"shielded_type": "Protegido",
|
||||||
|
|
||||||
|
# ---- Balance / Overview ----
|
||||||
|
"address_upper": "DIRECCIÓN",
|
||||||
|
"address_details": "Detalles de Dirección",
|
||||||
|
"amount_details": "DETALLES DE CANTIDAD",
|
||||||
|
"transactions_upper": "TRANSACCIONES",
|
||||||
|
"addresses_appear_here": "Tus direcciones de recepción aparecerán aquí una vez conectado.",
|
||||||
|
"wallet_empty": "Tu cartera está vacía",
|
||||||
|
"wallet_empty_hint": "Cambia a Recibir para obtener tu dirección y empezar a recibir fondos.",
|
||||||
|
"loading_addresses": "Cargando direcciones...",
|
||||||
|
"no_addresses_match": "No hay direcciones que coincidan con el filtro",
|
||||||
|
"no_addresses_with_balance": "No hay direcciones con saldo",
|
||||||
|
"click_copy_address": "Clic para copiar dirección",
|
||||||
|
"click_copy_uri": "Clic para copiar URI",
|
||||||
|
"address_copied": "Dirección copiada al portapapeles",
|
||||||
|
"about_dragonx": "Acerca de DragonX",
|
||||||
|
"dragonx_green": "DragonX (Verde)",
|
||||||
|
|
||||||
|
# ---- Transactions tab ----
|
||||||
|
"no_transactions": "No se encontraron transacciones",
|
||||||
|
"no_matching": "No hay transacciones coincidentes",
|
||||||
|
"showing_transactions": "Mostrando %d\u2013%d de %d transacciones (total: %zu)",
|
||||||
|
"conf_count": "%d conf",
|
||||||
|
"confirmations_display": "%d confirmaciones | %s",
|
||||||
|
"txs_count": "%d txs",
|
||||||
|
"view_details": "Ver Detalles",
|
||||||
|
"full_details": "Detalles Completos",
|
||||||
|
"export_csv": "Exportar CSV",
|
||||||
|
"transaction_id": "ID DE TRANSACCIÓN",
|
||||||
|
|
||||||
|
# ---- Receive tab ----
|
||||||
|
"select_receiving_address": "Seleccionar dirección de recepción...",
|
||||||
|
"payment_request": "SOLICITUD DE PAGO",
|
||||||
|
"recent_received": "RECIBIDOS RECIENTES",
|
||||||
|
"no_recent_receives": "No hay recepciones recientes",
|
||||||
|
"qr_unavailable": "QR no disponible",
|
||||||
|
"clear_request": "Limpiar Solicitud",
|
||||||
|
"clear_form_confirm": "¿Limpiar todos los campos del formulario?",
|
||||||
|
"payment_request_copied": "Solicitud de pago copiada",
|
||||||
|
"payment_uri_copied": "URI de pago copiada",
|
||||||
|
"failed_create_shielded": "Error al crear dirección protegida",
|
||||||
|
"failed_create_transparent": "Error al crear dirección transparente",
|
||||||
|
"new_shielded_created": "Nueva dirección protegida creada",
|
||||||
|
"new_transparent_created": "Nueva dirección transparente creada",
|
||||||
|
|
||||||
|
# ---- Send tab ----
|
||||||
|
"send_sending_from": "ENVIANDO DESDE",
|
||||||
|
"send_select_source": "Seleccionar dirección de origen...",
|
||||||
|
"send_no_balance": "Sin saldo",
|
||||||
|
"send_recipient": "DESTINATARIO",
|
||||||
|
"send_amount_upper": "CANTIDAD",
|
||||||
|
"send_amount": "Cantidad",
|
||||||
|
"send_amount_details": "DETALLES DE CANTIDAD",
|
||||||
|
"send_fee": "Comisión",
|
||||||
|
"send_fee_low": "Baja",
|
||||||
|
"send_fee_normal": "Normal",
|
||||||
|
"send_fee_high": "Alta",
|
||||||
|
"send_network_fee": "COMISIÓN DE RED",
|
||||||
|
"send_total": "Total",
|
||||||
|
"send_recent_sends": "ENVÍOS RECIENTES",
|
||||||
|
"send_no_recent": "No hay envíos recientes",
|
||||||
|
"send_clear_fields": "¿Limpiar todos los campos del formulario?",
|
||||||
|
"send_yes_clear": "Sí, Limpiar",
|
||||||
|
"send_keep": "Mantener",
|
||||||
|
"send_form_restored": "Formulario restaurado",
|
||||||
|
"send_valid_shielded": "Dirección protegida válida",
|
||||||
|
"send_valid_transparent": "Dirección transparente válida",
|
||||||
|
"send_exceeds_available": "Excede disponible (%.8f)",
|
||||||
|
"send_submitting": "Enviando transacción...",
|
||||||
|
"send_tx_sent": "¡Transacción enviada!",
|
||||||
|
"send_tx_success": "¡Transacción enviada exitosamente!",
|
||||||
|
"send_tx_failed": "Error en la transacción",
|
||||||
|
"send_error_prefix": "Error: %s",
|
||||||
|
"send_error_copied": "Error copiado al portapapeles",
|
||||||
|
"send_copy_error": "Copiar Error",
|
||||||
|
"send_dismiss": "Descartar",
|
||||||
|
"send_txid_copied": "TxID copiado al portapapeles",
|
||||||
|
"send_txid_label": "TxID: %s",
|
||||||
|
"send_go_to_receive": "Ir a Recibir",
|
||||||
|
"send_wallet_empty": "Tu cartera está vacía",
|
||||||
|
"send_switch_to_receive": "Cambia a Recibir para obtener tu dirección y empezar a recibir fondos.",
|
||||||
|
"send_tooltip_select_source": "Selecciona una dirección de origen primero",
|
||||||
|
"send_tooltip_invalid_address": "Ingresa una dirección de destinatario válida",
|
||||||
|
"send_tooltip_enter_amount": "Ingresa una cantidad a enviar",
|
||||||
|
"send_tooltip_exceeds_balance": "La cantidad excede el saldo disponible",
|
||||||
|
"send_tooltip_not_connected": "No conectado al daemon",
|
||||||
|
"send_tooltip_syncing": "Espera a que se sincronice el blockchain",
|
||||||
|
"send_tooltip_in_progress": "Transacción ya en progreso",
|
||||||
|
"sending_from": "ENVIANDO DESDE",
|
||||||
|
"select_source_address": "Seleccionar dirección de origen...",
|
||||||
|
"recipient": "DESTINATARIO",
|
||||||
|
"memo_optional": "MEMO (OPCIONAL)",
|
||||||
|
"memo_upper": "MEMO",
|
||||||
|
"network_fee": "COMISIÓN DE RED",
|
||||||
|
"fee_low": "Baja",
|
||||||
|
"fee_normal": "Normal",
|
||||||
|
"fee_high": "Alta",
|
||||||
|
"recent_sends": "ENVÍOS RECIENTES",
|
||||||
|
"no_recent_sends": "No hay envíos recientes",
|
||||||
|
"shielded_to": "PROTEGIDA PARA",
|
||||||
|
"submitting_transaction": "Enviando transacción...",
|
||||||
|
"transaction_sent_msg": "¡Transacción enviada!",
|
||||||
|
"amount_exceeds_balance": "La cantidad excede el saldo",
|
||||||
|
|
||||||
|
# ---- Mining tab ----
|
||||||
|
"mining_solo": "Solo",
|
||||||
|
"mining_pool": "Pool",
|
||||||
|
"mining_pool_url": "URL del Pool",
|
||||||
|
"mining_pool_hashrate": "Hashrate del Pool",
|
||||||
|
"mining_local_hashrate": "Hashrate Local",
|
||||||
|
"mining_payout_address": "Dirección de Pago",
|
||||||
|
"mining_payout_tooltip": "Dirección para recibir recompensas de minería",
|
||||||
|
"mining_mine": "Minar",
|
||||||
|
"mining_stop": "Detener",
|
||||||
|
"mining_starting": "Iniciando...",
|
||||||
|
"mining_stopping": "Deteniendo...",
|
||||||
|
"mining_active": "Activo",
|
||||||
|
"mining_connected": "Conectado",
|
||||||
|
"mining_connecting": "Conectando...",
|
||||||
|
"mining_network": "Red",
|
||||||
|
"mining_shares": "Shares",
|
||||||
|
"mining_uptime": "Tiempo activo",
|
||||||
|
"mining_mining_addr": "Dir. Minería",
|
||||||
|
"mining_est_block": "Bloque Est.",
|
||||||
|
"mining_est_daily": "Diario Est.",
|
||||||
|
"mining_today": "Hoy",
|
||||||
|
"mining_yesterday": "Ayer",
|
||||||
|
"mining_all_time": "Todo el Tiempo",
|
||||||
|
"mining_recent_blocks": "BLOQUES RECIENTES",
|
||||||
|
"mining_recent_payouts": "PAGOS DE POOL RECIENTES",
|
||||||
|
"mining_no_blocks_yet": "Aún no se han encontrado bloques",
|
||||||
|
"mining_no_payouts_yet": "Aún no hay pagos del pool",
|
||||||
|
"mining_show_chart": "Gráfico",
|
||||||
|
"mining_show_log": "Registro",
|
||||||
|
"mining_chart_now": "Ahora",
|
||||||
|
"mining_chart_start": "Inicio",
|
||||||
|
"mining_chart_1m_ago": "hace 1m",
|
||||||
|
"mining_chart_5m_ago": "hace 5m",
|
||||||
|
"mining_save_pool_url": "Guardar URL del pool",
|
||||||
|
"mining_save_payout_address": "Guardar dirección de pago",
|
||||||
|
"mining_saved_pools": "Pools Guardados:",
|
||||||
|
"mining_saved_addresses": "Direcciones Guardadas:",
|
||||||
|
"mining_no_saved_pools": "No hay pools guardados",
|
||||||
|
"mining_no_saved_addresses": "No hay direcciones guardadas",
|
||||||
|
"mining_already_saved": "URL del pool ya guardada",
|
||||||
|
"mining_remove": "Eliminar",
|
||||||
|
"mining_reset_defaults": "Restablecer Valores",
|
||||||
|
"mining_click": "Clic",
|
||||||
|
"mining_to_save": "para guardar",
|
||||||
|
"mining_click_copy_address": "Clic para copiar dirección",
|
||||||
|
"mining_click_copy_block": "Clic para copiar hash de bloque",
|
||||||
|
"mining_click_copy_difficulty": "Clic para copiar dificultad",
|
||||||
|
"mining_address_copied": "Dirección de minería copiada",
|
||||||
|
"mining_block_copied": "Hash de bloque copiado",
|
||||||
|
"mining_difficulty_copied": "Dificultad copiada",
|
||||||
|
"mining_open_in_explorer": "Abrir en explorador",
|
||||||
|
"mining_starting_tooltip": "El minero está iniciando...",
|
||||||
|
"mining_stopping_tooltip": "El minero está deteniéndose...",
|
||||||
|
"mining_syncing_tooltip": "El blockchain está sincronizando...",
|
||||||
|
"mining_idle_on_tooltip": "Desactivar minería en reposo",
|
||||||
|
"mining_idle_off_tooltip": "Activar minería en reposo",
|
||||||
|
"mining_stop_solo_for_pool": "Detener minería solo antes de iniciar minería en pool",
|
||||||
|
"mining_stop_solo_for_pool_settings": "Detener minería solo para cambiar configuración del pool",
|
||||||
|
"mining_filter_all": "Todos",
|
||||||
|
"mining_filter_tip_all": "Mostrar todas las ganancias",
|
||||||
|
"mining_filter_tip_solo": "Mostrar solo ganancias solo",
|
||||||
|
"mining_filter_tip_pool": "Mostrar solo ganancias del pool",
|
||||||
|
"local_hashrate": "Tasa Hash Local",
|
||||||
|
"est_time_to_block": "Tiempo Est. al Bloque",
|
||||||
|
"difficulty": "Dificultad",
|
||||||
|
"current_price": "Precio Actual",
|
||||||
|
"market_cap": "Cap. de Mercado",
|
||||||
|
|
||||||
|
# ---- Peers tab ----
|
||||||
|
"peers_blockchain": "BLOCKCHAIN",
|
||||||
|
"peers_blocks": "Bloques",
|
||||||
|
"peers_connected": "Conectados",
|
||||||
|
"peers_connected_count": "Conectados: %d",
|
||||||
|
"peers_banned": "Bloqueados",
|
||||||
|
"peers_banned_count": "Bloqueados: %d",
|
||||||
|
"peers_upper": "NODOS",
|
||||||
|
"peers_avg_ping": "Ping Prom.",
|
||||||
|
"peers_best_block": "Mejor Bloque",
|
||||||
|
"peers_hashrate": "Hashrate",
|
||||||
|
"peers_longest": "Más Larga",
|
||||||
|
"peers_longest_chain": "Cadena Más Larga",
|
||||||
|
"peers_memory": "Memoria",
|
||||||
|
"peers_notarized": "Notarizado",
|
||||||
|
"peers_p2p_port": "Puerto P2P",
|
||||||
|
"peers_protocol": "Protocolo",
|
||||||
|
"peers_version": "Versión",
|
||||||
|
"peers_in_out": "Ent/Sal",
|
||||||
|
"peers_dir_in": "Ent",
|
||||||
|
"peers_dir_out": "Sal",
|
||||||
|
"peers_received": "Recibido",
|
||||||
|
"peers_sent": "Enviado",
|
||||||
|
"peers_refresh": "Actualizar",
|
||||||
|
"peers_refresh_tooltip": "Actualizar lista de nodos",
|
||||||
|
"peers_refreshing": "Actualizando...",
|
||||||
|
"peers_no_connected": "No hay nodos conectados",
|
||||||
|
"peers_no_banned": "No hay nodos bloqueados",
|
||||||
|
"peers_ban_24h": "Bloquear Nodo 24h",
|
||||||
|
"peers_unban": "Desbloquear",
|
||||||
|
"peers_clear_all_bans": "Limpiar Todos los Bloqueos",
|
||||||
|
"peers_copy_ip": "Copiar IP",
|
||||||
|
"peers_click_copy": "Clic para copiar",
|
||||||
|
"peers_hash_copied": "Hash copiado",
|
||||||
|
"peers_no_tls": "Sin TLS",
|
||||||
|
"peers_blocks_left": "%d bloques restantes",
|
||||||
|
"peers_ban_score": "Puntuación: %d",
|
||||||
|
"peers_tt_id": "ID: %d",
|
||||||
|
"peers_tt_sent": "Enviado: %s",
|
||||||
|
"peers_tt_received": "Recibido: %s",
|
||||||
|
"peers_tt_services": "Servicios: %s",
|
||||||
|
"peers_tt_start_height": "Altura Inicial: %d",
|
||||||
|
"peers_tt_synced": "Sinc H/B: %d/%d",
|
||||||
|
"peers_tt_tls_cipher": "TLS: %s",
|
||||||
|
"connected_peers": "Nodos Conectados",
|
||||||
|
"banned_peers": "Nodos Bloqueados",
|
||||||
|
"ban": "Bloquear",
|
||||||
|
"clear_all_bans": "Limpiar Todos los Bloqueos",
|
||||||
|
"ip_address": "Dirección IP",
|
||||||
|
|
||||||
|
# ---- Market tab ----
|
||||||
|
"market_btc_price": "PRECIO BTC",
|
||||||
|
"market_24h_volume": "VOLUMEN 24H",
|
||||||
|
"market_portfolio": "PORTAFOLIO",
|
||||||
|
"market_pct_shielded": "%.0f%% Protegido",
|
||||||
|
"market_attribution": "Datos de precios de NonKYC",
|
||||||
|
"market_no_price": "Sin datos de precio",
|
||||||
|
"market_no_history": "No hay historial de precios disponible",
|
||||||
|
"market_price_unavailable": "Datos de precio no disponibles",
|
||||||
|
"market_refresh_price": "Actualizar datos de precio",
|
||||||
|
"market_trade_on": "Operar en %s",
|
||||||
|
"market_now": "Ahora",
|
||||||
|
"market_6h": "6h",
|
||||||
|
"market_12h": "12h",
|
||||||
|
"market_18h": "18h",
|
||||||
|
"market_24h": "24h",
|
||||||
|
"24h_change": "Cambio 24h",
|
||||||
|
"24h_volume": "Volumen 24h",
|
||||||
|
|
||||||
|
# ---- Console tab ----
|
||||||
|
"console_welcome": "Bienvenido a la Consola de ObsidianDragon",
|
||||||
|
"console_type_help": "Escribe 'help' para ver los comandos disponibles",
|
||||||
|
"console_available_commands": "Comandos disponibles:",
|
||||||
|
"console_common_rpc": "Comandos RPC comunes:",
|
||||||
|
"console_rpc_reference": "Referencia de Comandos RPC",
|
||||||
|
"console_auto_scroll": "Auto-desplazamiento",
|
||||||
|
"console_clear": "Limpiar",
|
||||||
|
"console_clear_console": "Limpiar Consola",
|
||||||
|
"console_cleared": "Consola limpiada",
|
||||||
|
"console_commands": "Comandos",
|
||||||
|
"console_completions": "Completaciones:",
|
||||||
|
"console_tab_completion": "Tab para completar",
|
||||||
|
"console_connected": "Conectado al daemon",
|
||||||
|
"console_disconnected": "Desconectado del daemon",
|
||||||
|
"console_not_connected": "Error: No conectado al daemon",
|
||||||
|
"console_no_daemon": "Sin daemon",
|
||||||
|
"console_daemon": "Daemon",
|
||||||
|
"console_daemon_error": "¡Error del daemon!",
|
||||||
|
"console_daemon_started": "Daemon iniciado",
|
||||||
|
"console_daemon_stopped": "Daemon detenido",
|
||||||
|
"console_errors": "Errores",
|
||||||
|
"console_filter_hint": "Filtrar salida...",
|
||||||
|
"console_search_commands": "Buscar comandos...",
|
||||||
|
"console_copy_all": "Copiar Todo",
|
||||||
|
"console_copy_selected": "Copiar",
|
||||||
|
"console_select_all": "Seleccionar Todo",
|
||||||
|
"console_zoom_in": "Acercar",
|
||||||
|
"console_zoom_out": "Alejar",
|
||||||
|
"console_show_daemon_output": "Mostrar salida del daemon",
|
||||||
|
"console_show_errors_only": "Mostrar solo errores",
|
||||||
|
"console_show_rpc_ref": "Mostrar referencia de comandos RPC",
|
||||||
|
"console_capturing_output": "Capturando salida del daemon...",
|
||||||
|
"console_starting_node": "Iniciando nodo...",
|
||||||
|
"console_line_count": "%zu líneas",
|
||||||
|
"console_new_lines": "%d nuevas líneas",
|
||||||
|
"console_showing_lines": "Mostrando %zu de %zu líneas",
|
||||||
|
"console_click_commands": "Clic en los comandos de arriba para insertarlos",
|
||||||
|
"console_click_insert": "Clic para insertar",
|
||||||
|
"console_click_insert_params": "Clic para insertar con parámetros",
|
||||||
|
"console_close": "Cerrar",
|
||||||
|
"console_status_running": "Ejecutando",
|
||||||
|
"console_status_stopped": "Detenido",
|
||||||
|
"console_status_starting": "Iniciando",
|
||||||
|
"console_status_stopping": "Deteniendo",
|
||||||
|
"console_status_error": "Error",
|
||||||
|
"console_status_unknown": "Desconocido",
|
||||||
|
"console_help_help": " help - Mostrar este mensaje de ayuda",
|
||||||
|
"console_help_getinfo": " getinfo - Mostrar información del nodo",
|
||||||
|
"console_help_getblockcount": " getblockcount - Mostrar altura actual del bloque",
|
||||||
|
"console_help_getbalance": " getbalance - Mostrar saldo transparente",
|
||||||
|
"console_help_gettotalbalance": " gettotalbalance - Mostrar saldo total",
|
||||||
|
"console_help_getmininginfo": " getmininginfo - Mostrar estado de minería",
|
||||||
|
"console_help_getpeerinfo": " getpeerinfo - Mostrar nodos conectados",
|
||||||
|
"console_help_setgenerate": " setgenerate - Controlar minería",
|
||||||
|
"console_help_stop": " stop - Detener el daemon",
|
||||||
|
"console_help_clear": " clear - Limpiar la consola",
|
||||||
|
|
||||||
|
# ---- About dialog ----
|
||||||
|
"about_title": "Acerca de ObsidianDragon",
|
||||||
|
"about_edition": "Edición ImGui",
|
||||||
|
"about_version": "Versión:",
|
||||||
|
"about_imgui": "ImGui:",
|
||||||
|
"about_build_date": "Fecha de Compilación:",
|
||||||
|
"about_build_type": "Tipo de Compilación:",
|
||||||
|
"about_debug": "Depuración",
|
||||||
|
"about_release": "Producción",
|
||||||
|
"about_daemon": "Daemon:",
|
||||||
|
"about_chain": "Cadena:",
|
||||||
|
"about_block_height": "Altura de Bloque:",
|
||||||
|
"about_connections": "Conexiones:",
|
||||||
|
"about_peers_count": "%zu nodos",
|
||||||
|
"about_credits": "Créditos",
|
||||||
|
"about_license": "Licencia",
|
||||||
|
"about_license_text": "Este software se distribuye bajo la Licencia Pública General de GNU v3 (GPLv3). Usted es libre de usar, modificar y distribuir este software bajo los términos de la licencia.",
|
||||||
|
"about_website": "Sitio Web",
|
||||||
|
"about_github": "GitHub",
|
||||||
|
"about_block_explorer": "Explorador de Bloques",
|
||||||
|
|
||||||
|
# ---- Address Book dialog ----
|
||||||
|
"address_book_title": "Libreta de Direcciones",
|
||||||
|
"address_book_add_new": "Agregar Nueva",
|
||||||
|
"address_book_add": "Agregar Dirección",
|
||||||
|
"address_book_edit": "Editar Dirección",
|
||||||
|
"address_book_empty": "No hay direcciones guardadas. Haz clic en 'Agregar Nueva' para añadir una.",
|
||||||
|
"address_book_count": "%zu direcciones guardadas",
|
||||||
|
"address_book_deleted": "Entrada eliminada",
|
||||||
|
"address_book_added": "Dirección agregada a la libreta",
|
||||||
|
"address_book_exists": "La dirección ya existe en la libreta",
|
||||||
|
"address_book_updated": "Dirección actualizada",
|
||||||
|
"address_book_update_failed": "Error al actualizar - la dirección puede estar duplicada",
|
||||||
|
|
||||||
|
# ---- Backup dialog ----
|
||||||
|
"backup_title": "Respaldar Cartera",
|
||||||
|
"backup_description": "Crea un respaldo de tu archivo wallet.dat. Este archivo contiene todas tus claves privadas e historial de transacciones. Guarda el respaldo en un lugar seguro.",
|
||||||
|
"backup_destination": "Destino del respaldo:",
|
||||||
|
"backup_wallet_not_found": "Advertencia: wallet.dat no encontrado en la ubicación esperada",
|
||||||
|
"backup_create": "Crear Respaldo",
|
||||||
|
"backup_created": "Respaldo de cartera creado",
|
||||||
|
"backup_backing_up": "Respaldando...",
|
||||||
|
"backup_tips": "Consejos:",
|
||||||
|
"backup_tip_external": "Guarda respaldos en unidades externas o almacenamiento en la nube",
|
||||||
|
"backup_tip_multiple": "Crea múltiples respaldos en diferentes ubicaciones",
|
||||||
|
"backup_tip_test": "Prueba restaurar desde el respaldo periódicamente",
|
||||||
|
"backup_wallet": "Respaldar Cartera...",
|
||||||
|
|
||||||
|
# ---- Block Info dialog ----
|
||||||
|
"block_info_title": "Información del Bloque",
|
||||||
|
"block_height": "Altura del Bloque:",
|
||||||
|
"block_get_info": "Obtener Info del Bloque",
|
||||||
|
"block_hash": "Hash del Bloque:",
|
||||||
|
"block_timestamp": "Fecha y Hora:",
|
||||||
|
"block_transactions": "Transacciones:",
|
||||||
|
"block_size": "Tamaño:",
|
||||||
|
"block_bits": "Bits:",
|
||||||
|
"block_merkle_root": "Raíz Merkle:",
|
||||||
|
"block_previous": "Bloque Anterior:",
|
||||||
|
"block_next": "Bloque Siguiente:",
|
||||||
|
"block_click_prev": "Clic para ver bloque anterior",
|
||||||
|
"block_click_next": "Clic para ver bloque siguiente",
|
||||||
|
"block_nav_prev": "<< Anterior",
|
||||||
|
"block_nav_next": "Siguiente >>",
|
||||||
|
|
||||||
|
# ---- Export Keys dialog ----
|
||||||
|
"export_keys_title": "Exportar Todas las Claves Privadas",
|
||||||
|
"export_keys_danger": "PELIGRO: ¡Esto exportará TODAS las claves privadas de tu cartera! Cualquiera con acceso a este archivo puede robar tus fondos. Guárdalo de forma segura y elimínalo después de usar.",
|
||||||
|
"export_keys_options": "Opciones de exportación:",
|
||||||
|
"export_keys_include_z": "Incluir direcciones Z (protegidas)",
|
||||||
|
"export_keys_include_t": "Incluir direcciones T (transparentes)",
|
||||||
|
"export_keys_btn": "Exportar Claves",
|
||||||
|
"export_keys_success": "Claves exportadas exitosamente",
|
||||||
|
"export_private_key": "Exportar Clave Privada",
|
||||||
|
"export_viewing_key": "Exportar Clave de Vista",
|
||||||
|
|
||||||
|
# ---- Export Transactions dialog ----
|
||||||
|
"export_tx_title": "Exportar Transacciones a CSV",
|
||||||
|
"export_tx_count": "Exportar %zu transacciones a archivo CSV.",
|
||||||
|
"export_tx_none": "No hay transacciones para exportar",
|
||||||
|
"export_tx_file_fail": "Error al crear archivo CSV",
|
||||||
|
"export_tx_success": "Transacciones exportadas exitosamente",
|
||||||
|
|
||||||
|
# ---- Import Key dialog ----
|
||||||
|
"import_key_title": "Importar Clave Privada",
|
||||||
|
"import_key_warning": "Advertencia: ¡Nunca compartas tus claves privadas! Importar claves de fuentes no confiables puede comprometer tu cartera.",
|
||||||
|
"import_key_label": "Clave(s) Privada(s):",
|
||||||
|
"import_key_tooltip": "Ingresa una o más claves privadas, una por línea.\nSoporta claves de direcciones z y t.\nLas líneas que empiezan con # se tratan como comentarios.",
|
||||||
|
"import_key_btn": "Importar Clave(s)",
|
||||||
|
"import_key_no_valid": "No se encontraron claves válidas en la entrada",
|
||||||
|
"import_key_success": "Claves importadas exitosamente",
|
||||||
|
"import_key_rescan": "Re-escanear blockchain después de importar",
|
||||||
|
"import_key_start_height": "Altura inicial:",
|
||||||
|
"import_key_full_rescan": "(0 = re-escaneo completo)",
|
||||||
|
"import_key_formats": "Formatos de clave soportados:",
|
||||||
|
"import_key_z_format": "Claves de gasto de direcciones Z (secret-extended-key-...)",
|
||||||
|
"import_key_t_format": "Claves privadas WIF de direcciones T",
|
||||||
|
"import_private_key": "Importar Clave Privada...",
|
||||||
|
"invalid_address": "Formato de dirección inválido",
|
||||||
|
|
||||||
|
# ---- Key Export dialog ----
|
||||||
|
"key_export_private_key": "Clave Privada:",
|
||||||
|
"key_export_viewing_key": "Clave de Vista:",
|
||||||
|
"key_export_private_warning": "¡Mantén esta clave en SECRETO! Cualquiera con esta clave puede gastar tus fondos. Nunca la compartas en línea ni con personas no confiables.",
|
||||||
|
"key_export_viewing_warning": "Esta clave de vista permite a otros ver tus transacciones entrantes y saldo, pero NO gastar tus fondos. Comparte solo con personas de confianza.",
|
||||||
|
"key_export_fetching": "Obteniendo clave de la cartera...",
|
||||||
|
"key_export_reveal": "Revelar Clave",
|
||||||
|
|
||||||
|
# ---- QR dialog ----
|
||||||
|
"qr_title": "Código QR",
|
||||||
|
"qr_failed": "Error al generar código QR",
|
||||||
|
|
||||||
|
# ---- Request Payment dialog ----
|
||||||
|
"request_title": "Solicitar Pago",
|
||||||
|
"request_description": "Genera una solicitud de pago que otros pueden escanear o copiar. El código QR contiene tu dirección y cantidad/memo opcionales.",
|
||||||
|
"request_receive_address": "Dirección de Recepción:",
|
||||||
|
"request_select_address": "Seleccionar dirección...",
|
||||||
|
"request_shielded_addrs": "-- Direcciones Protegidas --",
|
||||||
|
"request_transparent_addrs": "-- Direcciones Transparentes --",
|
||||||
|
"request_amount": "Cantidad (opcional):",
|
||||||
|
"request_label": "Etiqueta (opcional):",
|
||||||
|
"request_memo": "Memo (opcional):",
|
||||||
|
"request_payment_uri": "URI de Pago:",
|
||||||
|
"request_copy_uri": "Copiar URI",
|
||||||
|
"request_uri_copied": "URI de pago copiada al portapapeles",
|
||||||
|
|
||||||
|
# ---- Shield dialog ----
|
||||||
|
"shield_title": "Proteger Recompensas de Coinbase",
|
||||||
|
"shield_description": "Protege tus recompensas de minería enviando salidas coinbase de direcciones transparentes a una dirección protegida. Esto mejora la privacidad ocultando tus ingresos de minería.",
|
||||||
|
"shield_from_address": "Dirección de Origen:",
|
||||||
|
"shield_wildcard_hint": "Usa '*' para proteger desde todas las direcciones transparentes",
|
||||||
|
"shield_to_address": "Dirección Destino (Protegida):",
|
||||||
|
"shield_select_z": "Seleccionar dirección z...",
|
||||||
|
"shield_utxo_limit": "Límite UTXO:",
|
||||||
|
"shield_max_utxos": "UTXOs máximos por operación",
|
||||||
|
"shield_funds": "Proteger Fondos",
|
||||||
|
"shield_started": "Operación de protección iniciada",
|
||||||
|
"shield_check_status": "Verificar Estado",
|
||||||
|
"shield_completed": "¡Operación completada exitosamente!",
|
||||||
|
"shield_merge_done": "¡Protección/fusión completada!",
|
||||||
|
"shield_in_progress": "Operación en progreso...",
|
||||||
|
"merge_title": "Fusionar a Dirección",
|
||||||
|
"merge_description": "Fusiona múltiples UTXOs en una sola dirección protegida. Esto puede ayudar a reducir el tamaño de la cartera y mejorar la privacidad.",
|
||||||
|
"merge_funds": "Fusionar Fondos",
|
||||||
|
"merge_started": "Operación de fusión iniciada",
|
||||||
|
|
||||||
|
# ---- Transaction Details dialog ----
|
||||||
|
"tx_details_title": "Detalles de Transacción",
|
||||||
|
"tx_received": "RECIBIDO",
|
||||||
|
"tx_sent": "ENVIADO",
|
||||||
|
"tx_mined": "MINADO",
|
||||||
|
"tx_immature": "INMADURO",
|
||||||
|
"tx_confirmations": "%d confirmaciones",
|
||||||
|
"tx_id_label": "ID de Transacción:",
|
||||||
|
"tx_to_address": "Dirección Destino:",
|
||||||
|
"tx_from_address": "Dirección Origen:",
|
||||||
|
"tx_view_explorer": "Ver en Explorador",
|
||||||
|
"pending": "Pendiente",
|
||||||
|
|
||||||
|
# ---- Validate Address dialog ----
|
||||||
|
"validate_title": "Validar Dirección",
|
||||||
|
"validate_description": "Ingresa una dirección DragonX para verificar si es válida y si pertenece a esta cartera.",
|
||||||
|
"validate_btn": "Validar",
|
||||||
|
"validate_results": "Resultados:",
|
||||||
|
"validate_status": "Estado:",
|
||||||
|
"validate_valid": "VÁLIDA",
|
||||||
|
"validate_invalid": "INVÁLIDA",
|
||||||
|
"validate_type": "Tipo:",
|
||||||
|
"validate_ownership": "Propiedad:",
|
||||||
|
"validate_is_mine": "Esta cartera es dueña de esta dirección",
|
||||||
|
"validate_not_mine": "No es propiedad de esta cartera",
|
||||||
|
"validate_shielded_type": "Protegida (dirección z)",
|
||||||
|
"validate_transparent_type": "Transparente (dirección t)",
|
||||||
|
|
||||||
|
# ---- Misc ----
|
||||||
|
"transaction_sent": "Transacción enviada exitosamente",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Load existing to preserve anything we might have missed
|
||||||
|
import os
|
||||||
|
existing_path = os.path.join(os.path.dirname(__file__), '..', 'res', 'lang', 'es.json')
|
||||||
|
out_path = existing_path
|
||||||
|
|
||||||
|
with open(out_path, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(dict(sorted(es.items())), f, indent=4, ensure_ascii=False)
|
||||||
|
f.write('\n')
|
||||||
|
|
||||||
|
print(f"Wrote {len(es)} Spanish translations to {out_path}")
|
||||||
646
scripts/gen_fr.py
Normal file
@@ -0,0 +1,646 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Generate French (fr) translations for ObsidianDragon wallet."""
|
||||||
|
import json, os
|
||||||
|
|
||||||
|
translations = {
|
||||||
|
"24h_change": "Variation 24h",
|
||||||
|
"24h_volume": "Volume 24h",
|
||||||
|
"about": "À propos",
|
||||||
|
"about_block_explorer": "Explorateur de blocs",
|
||||||
|
"about_block_height": "Hauteur de bloc :",
|
||||||
|
"about_build_date": "Date de compilation :",
|
||||||
|
"about_build_type": "Type de build :",
|
||||||
|
"about_chain": "Chaîne :",
|
||||||
|
"about_connections": "Connexions :",
|
||||||
|
"about_credits": "Crédits",
|
||||||
|
"about_daemon": "Daemon :",
|
||||||
|
"about_debug": "Débogage",
|
||||||
|
"about_dragonx": "À propos d'ObsidianDragon",
|
||||||
|
"about_edition": "Édition ImGui",
|
||||||
|
"about_github": "GitHub",
|
||||||
|
"about_imgui": "ImGui :",
|
||||||
|
"about_license": "Licence",
|
||||||
|
"about_license_text": "Ce logiciel est publié sous la licence publique générale GNU v3 (GPLv3). Vous êtes libre d'utiliser, de modifier et de distribuer ce logiciel selon les termes de la licence.",
|
||||||
|
"about_peers_count": "%zu pairs",
|
||||||
|
"about_release": "Version",
|
||||||
|
"about_title": "À propos d'ObsidianDragon",
|
||||||
|
"about_version": "Version :",
|
||||||
|
"about_website": "Site web",
|
||||||
|
"acrylic": "Acrylique",
|
||||||
|
"add": "Ajouter",
|
||||||
|
"address": "Adresse",
|
||||||
|
"address_book_add": "Ajouter une adresse",
|
||||||
|
"address_book_add_new": "Ajouter",
|
||||||
|
"address_book_added": "Adresse ajoutée au carnet",
|
||||||
|
"address_book_count": "%zu adresses enregistrées",
|
||||||
|
"address_book_deleted": "Entrée supprimée",
|
||||||
|
"address_book_edit": "Modifier l'adresse",
|
||||||
|
"address_book_empty": "Aucune adresse enregistrée. Cliquez sur 'Ajouter' pour en créer une.",
|
||||||
|
"address_book_exists": "L'adresse existe déjà dans le carnet",
|
||||||
|
"address_book_title": "Carnet d'adresses",
|
||||||
|
"address_book_update_failed": "Échec de la mise à jour - l'adresse est peut-être en double",
|
||||||
|
"address_book_updated": "Adresse mise à jour",
|
||||||
|
"address_copied": "Adresse copiée dans le presse-papiers",
|
||||||
|
"address_details": "Détails de l'adresse",
|
||||||
|
"address_label": "Adresse :",
|
||||||
|
"address_upper": "ADRESSE",
|
||||||
|
"address_url": "URL de l'adresse",
|
||||||
|
"addresses_appear_here": "Vos adresses de réception apparaîtront ici une fois connecté.",
|
||||||
|
"advanced": "AVANCÉ",
|
||||||
|
"all_filter": "Tout",
|
||||||
|
"allow_custom_fees": "Autoriser les frais personnalisés",
|
||||||
|
"amount": "Montant",
|
||||||
|
"amount_details": "DÉTAILS DU MONTANT",
|
||||||
|
"amount_exceeds_balance": "Le montant dépasse le solde",
|
||||||
|
"amount_label": "Montant :",
|
||||||
|
"appearance": "APPARENCE",
|
||||||
|
"auto_shield": "Auto-blindage du minage",
|
||||||
|
"available": "Disponible",
|
||||||
|
"backup_backing_up": "Sauvegarde en cours...",
|
||||||
|
"backup_create": "Créer une sauvegarde",
|
||||||
|
"backup_created": "Sauvegarde du portefeuille créée",
|
||||||
|
"backup_data": "SAUVEGARDE & DONNÉES",
|
||||||
|
"backup_description": "Créez une sauvegarde de votre fichier wallet.dat. Ce fichier contient toutes vos clés privées et l'historique des transactions. Conservez la sauvegarde dans un endroit sûr.",
|
||||||
|
"backup_destination": "Destination de sauvegarde :",
|
||||||
|
"backup_tip_external": "Stockez les sauvegardes sur des disques externes ou un stockage cloud",
|
||||||
|
"backup_tip_multiple": "Créez plusieurs sauvegardes à différents endroits",
|
||||||
|
"backup_tip_test": "Testez périodiquement la restauration à partir de la sauvegarde",
|
||||||
|
"backup_tips": "Conseils :",
|
||||||
|
"backup_title": "Sauvegarder le portefeuille",
|
||||||
|
"backup_wallet": "Sauvegarder le portefeuille...",
|
||||||
|
"backup_wallet_not_found": "Attention : wallet.dat introuvable à l'emplacement prévu",
|
||||||
|
"balance": "Solde",
|
||||||
|
"balance_layout": "Disposition du solde",
|
||||||
|
"ban": "Bannir",
|
||||||
|
"banned_peers": "Pairs bannis",
|
||||||
|
"block": "Bloc",
|
||||||
|
"block_bits": "Bits :",
|
||||||
|
"block_click_next": "Cliquez pour voir le bloc suivant",
|
||||||
|
"block_click_prev": "Cliquez pour voir le bloc précédent",
|
||||||
|
"block_explorer": "Explorateur de blocs",
|
||||||
|
"block_get_info": "Obtenir les infos du bloc",
|
||||||
|
"block_hash": "Hash du bloc :",
|
||||||
|
"block_height": "Hauteur du bloc :",
|
||||||
|
"block_info_title": "Informations sur le bloc",
|
||||||
|
"block_merkle_root": "Racine de Merkle :",
|
||||||
|
"block_nav_next": "Suivant >>",
|
||||||
|
"block_nav_prev": "<< Précédent",
|
||||||
|
"block_next": "Bloc suivant :",
|
||||||
|
"block_previous": "Bloc précédent :",
|
||||||
|
"block_size": "Taille :",
|
||||||
|
"block_timestamp": "Horodatage :",
|
||||||
|
"block_transactions": "Transactions :",
|
||||||
|
"blockchain_syncing": "Synchronisation de la blockchain (%.1f%%)... Les soldes peuvent être inexacts.",
|
||||||
|
"cancel": "Annuler",
|
||||||
|
"characters": "caractères",
|
||||||
|
"clear": "Effacer",
|
||||||
|
"clear_all_bans": "Lever tous les bannissements",
|
||||||
|
"clear_form_confirm": "Effacer tous les champs du formulaire ?",
|
||||||
|
"clear_request": "Effacer la demande",
|
||||||
|
"click_copy_address": "Cliquez pour copier l'adresse",
|
||||||
|
"click_copy_uri": "Cliquez pour copier l'URI",
|
||||||
|
"close": "Fermer",
|
||||||
|
"conf_count": "%d conf.",
|
||||||
|
"confirm_and_send": "Confirmer & Envoyer",
|
||||||
|
"confirm_send": "Confirmer l'envoi",
|
||||||
|
"confirm_transaction": "Confirmer la transaction",
|
||||||
|
"confirmations": "Confirmations",
|
||||||
|
"confirmations_display": "%d confirmations | %s",
|
||||||
|
"confirmed": "Confirmé",
|
||||||
|
"connected": "Connecté",
|
||||||
|
"connected_peers": "Pairs connectés",
|
||||||
|
"connecting": "Connexion...",
|
||||||
|
"console": "Console",
|
||||||
|
"console_auto_scroll": "Défilement auto",
|
||||||
|
"console_available_commands": "Commandes disponibles :",
|
||||||
|
"console_capturing_output": "Capture de la sortie du daemon...",
|
||||||
|
"console_clear": "Effacer",
|
||||||
|
"console_clear_console": "Effacer la console",
|
||||||
|
"console_cleared": "Console effacée",
|
||||||
|
"console_click_commands": "Cliquez sur les commandes ci-dessus pour les insérer",
|
||||||
|
"console_click_insert": "Cliquez pour insérer",
|
||||||
|
"console_click_insert_params": "Cliquez pour insérer avec paramètres",
|
||||||
|
"console_close": "Fermer",
|
||||||
|
"console_commands": "Commandes",
|
||||||
|
"console_common_rpc": "Commandes RPC courantes :",
|
||||||
|
"console_completions": "Complétions :",
|
||||||
|
"console_connected": "Connecté au daemon",
|
||||||
|
"console_copy_all": "Tout copier",
|
||||||
|
"console_copy_selected": "Copier",
|
||||||
|
"console_daemon": "Daemon",
|
||||||
|
"console_daemon_error": "Erreur du daemon !",
|
||||||
|
"console_daemon_started": "Daemon démarré",
|
||||||
|
"console_daemon_stopped": "Daemon arrêté",
|
||||||
|
"console_disconnected": "Déconnecté du daemon",
|
||||||
|
"console_errors": "Erreurs",
|
||||||
|
"console_filter_hint": "Filtrer la sortie...",
|
||||||
|
"console_help_clear": " clear - Effacer la console",
|
||||||
|
"console_help_getbalance": " getbalance - Afficher le solde transparent",
|
||||||
|
"console_help_getblockcount": " getblockcount - Afficher la hauteur de bloc actuelle",
|
||||||
|
"console_help_getinfo": " getinfo - Afficher les infos du nœud",
|
||||||
|
"console_help_getmininginfo": " getmininginfo - Afficher le statut du minage",
|
||||||
|
"console_help_getpeerinfo": " getpeerinfo - Afficher les pairs connectés",
|
||||||
|
"console_help_gettotalbalance": " gettotalbalance - Afficher le solde total",
|
||||||
|
"console_help_help": " help - Afficher ce message d'aide",
|
||||||
|
"console_help_setgenerate": " setgenerate - Contrôler le minage",
|
||||||
|
"console_help_stop": " stop - Arrêter le daemon",
|
||||||
|
"console_line_count": "%zu lignes",
|
||||||
|
"console_new_lines": "%d nouvelles lignes",
|
||||||
|
"console_no_daemon": "Pas de daemon",
|
||||||
|
"console_not_connected": "Erreur : Non connecté au daemon",
|
||||||
|
"console_rpc_reference": "Référence des commandes RPC",
|
||||||
|
"console_scanline": "Scanline de la console",
|
||||||
|
"console_search_commands": "Rechercher des commandes...",
|
||||||
|
"console_select_all": "Tout sélectionner",
|
||||||
|
"console_show_daemon_output": "Afficher la sortie du daemon",
|
||||||
|
"console_show_errors_only": "Afficher uniquement les erreurs",
|
||||||
|
"console_show_rpc_ref": "Afficher la référence des commandes RPC",
|
||||||
|
"console_showing_lines": "Affichage de %zu sur %zu lignes",
|
||||||
|
"console_starting_node": "Démarrage du nœud...",
|
||||||
|
"console_status_error": "Erreur",
|
||||||
|
"console_status_running": "En cours",
|
||||||
|
"console_status_starting": "Démarrage",
|
||||||
|
"console_status_stopped": "Arrêté",
|
||||||
|
"console_status_stopping": "Arrêt",
|
||||||
|
"console_status_unknown": "Inconnu",
|
||||||
|
"console_tab_completion": "Tab pour compléter",
|
||||||
|
"console_type_help": "Tapez 'help' pour les commandes disponibles",
|
||||||
|
"console_welcome": "Bienvenue dans la console ObsidianDragon",
|
||||||
|
"console_zoom_in": "Agrandir",
|
||||||
|
"console_zoom_out": "Réduire",
|
||||||
|
"copy": "Copier",
|
||||||
|
"copy_address": "Copier l'adresse complète",
|
||||||
|
"copy_error": "Copier l'erreur",
|
||||||
|
"copy_to_clipboard": "Copier dans le presse-papiers",
|
||||||
|
"copy_txid": "Copier le TxID",
|
||||||
|
"copy_uri": "Copier l'URI",
|
||||||
|
"current_price": "Prix actuel",
|
||||||
|
"custom_fees": "Frais personnalisés",
|
||||||
|
"dark": "Sombre",
|
||||||
|
"date": "Date",
|
||||||
|
"date_label": "Date :",
|
||||||
|
"delete": "Supprimer",
|
||||||
|
"difficulty": "Difficulté",
|
||||||
|
"disconnected": "Déconnecté",
|
||||||
|
"dismiss": "Ignorer",
|
||||||
|
"display": "Affichage",
|
||||||
|
"dragonx_green": "DragonX (Vert)",
|
||||||
|
"edit": "Modifier",
|
||||||
|
"error": "Erreur",
|
||||||
|
"est_time_to_block": "Temps est. par bloc",
|
||||||
|
"exit": "Quitter",
|
||||||
|
"explorer": "EXPLORATEUR",
|
||||||
|
"export": "Exporter",
|
||||||
|
"export_csv": "Exporter en CSV",
|
||||||
|
"export_keys_btn": "Exporter les clés",
|
||||||
|
"export_keys_danger": "DANGER : Ceci exportera TOUTES les clés privées de votre portefeuille ! Toute personne ayant accès à ce fichier peut voler vos fonds. Conservez-le en sécurité et supprimez-le après utilisation.",
|
||||||
|
"export_keys_include_t": "Inclure les adresses T (transparentes)",
|
||||||
|
"export_keys_include_z": "Inclure les adresses Z (blindées)",
|
||||||
|
"export_keys_options": "Options d'exportation :",
|
||||||
|
"export_keys_success": "Clés exportées avec succès",
|
||||||
|
"export_keys_title": "Exporter toutes les clés privées",
|
||||||
|
"export_private_key": "Exporter la clé privée",
|
||||||
|
"export_tx_count": "Exporter %zu transactions en fichier CSV.",
|
||||||
|
"export_tx_file_fail": "Impossible de créer le fichier CSV",
|
||||||
|
"export_tx_none": "Aucune transaction à exporter",
|
||||||
|
"export_tx_success": "Transactions exportées avec succès",
|
||||||
|
"export_tx_title": "Exporter les transactions en CSV",
|
||||||
|
"export_viewing_key": "Exporter la clé de visualisation",
|
||||||
|
"failed_create_shielded": "Échec de la création de l'adresse blindée",
|
||||||
|
"failed_create_transparent": "Échec de la création de l'adresse transparente",
|
||||||
|
"fee": "Frais",
|
||||||
|
"fee_high": "Élevés",
|
||||||
|
"fee_label": "Frais :",
|
||||||
|
"fee_low": "Faibles",
|
||||||
|
"fee_normal": "Normal",
|
||||||
|
"fetch_prices": "Récupérer les prix",
|
||||||
|
"file": "Fichier",
|
||||||
|
"file_save_location": "Le fichier sera enregistré dans : ~/.config/ObsidianDragon/",
|
||||||
|
"font_scale": "Taille de police",
|
||||||
|
"from": "De",
|
||||||
|
"from_upper": "DE",
|
||||||
|
"full_details": "Tous les détails",
|
||||||
|
"general": "Général",
|
||||||
|
"go_to_receive": "Aller à Recevoir",
|
||||||
|
"height": "Hauteur",
|
||||||
|
"help": "Aide",
|
||||||
|
"hide": "Masquer",
|
||||||
|
"history": "Historique",
|
||||||
|
"immature_type": "Immature",
|
||||||
|
"import": "Importer",
|
||||||
|
"import_key_btn": "Importer clé(s)",
|
||||||
|
"import_key_formats": "Formats de clés pris en charge :",
|
||||||
|
"import_key_full_rescan": "(0 = rescan complet)",
|
||||||
|
"import_key_label": "Clé(s) privée(s) :",
|
||||||
|
"import_key_no_valid": "Aucune clé valide trouvée dans l'entrée",
|
||||||
|
"import_key_rescan": "Re-scanner la blockchain après l'importation",
|
||||||
|
"import_key_start_height": "Hauteur de départ :",
|
||||||
|
"import_key_success": "Clés importées avec succès",
|
||||||
|
"import_key_t_format": "Clés privées WIF d'adresses T",
|
||||||
|
"import_key_title": "Importer une clé privée",
|
||||||
|
"import_key_tooltip": "Entrez une ou plusieurs clés privées, une par ligne.\nPrend en charge les clés z-adresse et t-adresse.\nLes lignes commençant par # sont traitées comme des commentaires.",
|
||||||
|
"import_key_warning": "Attention : Ne partagez jamais vos clés privées ! L'importation de clés provenant de sources non fiables peut compromettre votre portefeuille.",
|
||||||
|
"import_key_z_format": "Clés de dépenses z-adresse (secret-extended-key-...)",
|
||||||
|
"import_private_key": "Importer une clé privée...",
|
||||||
|
"invalid_address": "Format d'adresse invalide",
|
||||||
|
"ip_address": "Adresse IP",
|
||||||
|
"keep": "Conserver",
|
||||||
|
"keep_daemon": "Garder le daemon en marche",
|
||||||
|
"key_export_fetching": "Récupération de la clé depuis le portefeuille...",
|
||||||
|
"key_export_private_key": "Clé privée :",
|
||||||
|
"key_export_private_warning": "Gardez cette clé SECRÈTE ! Toute personne possédant cette clé peut dépenser vos fonds. Ne la partagez jamais en ligne ou avec des tiers non fiables.",
|
||||||
|
"key_export_reveal": "Révéler la clé",
|
||||||
|
"key_export_viewing_key": "Clé de visualisation :",
|
||||||
|
"key_export_viewing_warning": "Cette clé de visualisation permet à d'autres de voir vos transactions entrantes et votre solde, mais PAS de dépenser vos fonds. Ne la partagez qu'avec des personnes de confiance.",
|
||||||
|
"label": "Libellé :",
|
||||||
|
"language": "Langue",
|
||||||
|
"light": "Clair",
|
||||||
|
"loading": "Chargement...",
|
||||||
|
"loading_addresses": "Chargement des adresses...",
|
||||||
|
"local_hashrate": "Hashrate local",
|
||||||
|
"low_spec_mode": "Mode économie",
|
||||||
|
"market": "Marché",
|
||||||
|
"market_12h": "12h",
|
||||||
|
"market_18h": "18h",
|
||||||
|
"market_24h": "24h",
|
||||||
|
"market_24h_volume": "VOLUME 24H",
|
||||||
|
"market_6h": "6h",
|
||||||
|
"market_attribution": "Données de prix de NonKYC",
|
||||||
|
"market_btc_price": "PRIX BTC",
|
||||||
|
"market_cap": "Capitalisation",
|
||||||
|
"market_no_history": "Aucun historique de prix disponible",
|
||||||
|
"market_no_price": "Pas de données de prix",
|
||||||
|
"market_now": "Maintenant",
|
||||||
|
"market_pct_shielded": "%.0f%% Blindé",
|
||||||
|
"market_portfolio": "PORTEFEUILLE",
|
||||||
|
"market_price_unavailable": "Données de prix indisponibles",
|
||||||
|
"market_refresh_price": "Actualiser les données de prix",
|
||||||
|
"market_trade_on": "Échanger sur %s",
|
||||||
|
"mature": "Mature",
|
||||||
|
"max": "Max",
|
||||||
|
"memo": "Mémo (optionnel, chiffré)",
|
||||||
|
"memo_label": "Mémo :",
|
||||||
|
"memo_optional": "MÉMO (OPTIONNEL)",
|
||||||
|
"memo_upper": "MÉMO",
|
||||||
|
"memo_z_only": "Note : Les mémos ne sont disponibles que lors de l'envoi vers des adresses blindées (z)",
|
||||||
|
"merge_description": "Fusionnez plusieurs UTXOs en une seule adresse blindée. Cela peut réduire la taille du portefeuille et améliorer la confidentialité.",
|
||||||
|
"merge_funds": "Fusionner les fonds",
|
||||||
|
"merge_started": "Opération de fusion démarrée",
|
||||||
|
"merge_title": "Fusionner vers une adresse",
|
||||||
|
"mine_when_idle": "Miner au repos",
|
||||||
|
"mined": "miné",
|
||||||
|
"mined_filter": "Miné",
|
||||||
|
"mined_type": "Miné",
|
||||||
|
"mined_upper": "MINÉ",
|
||||||
|
"miner_fee": "Frais de mineur",
|
||||||
|
"mining": "Minage",
|
||||||
|
"mining_active": "Actif",
|
||||||
|
"mining_address_copied": "Adresse de minage copiée",
|
||||||
|
"mining_all_time": "Tout le temps",
|
||||||
|
"mining_already_saved": "URL du pool déjà enregistrée",
|
||||||
|
"mining_block_copied": "Hash du bloc copié",
|
||||||
|
"mining_chart_1m_ago": "il y a 1m",
|
||||||
|
"mining_chart_5m_ago": "il y a 5m",
|
||||||
|
"mining_chart_now": "Maintenant",
|
||||||
|
"mining_chart_start": "Début",
|
||||||
|
"mining_click": "Cliquer",
|
||||||
|
"mining_click_copy_address": "Cliquez pour copier l'adresse",
|
||||||
|
"mining_click_copy_block": "Cliquez pour copier le hash du bloc",
|
||||||
|
"mining_click_copy_difficulty": "Cliquez pour copier la difficulté",
|
||||||
|
"mining_connected": "Connecté",
|
||||||
|
"mining_connecting": "Connexion...",
|
||||||
|
"mining_control": "Contrôle du minage",
|
||||||
|
"mining_difficulty_copied": "Difficulté copiée",
|
||||||
|
"mining_est_block": "Bloc est.",
|
||||||
|
"mining_est_daily": "Est. quotidien",
|
||||||
|
"mining_filter_all": "Tout",
|
||||||
|
"mining_filter_tip_all": "Afficher tous les gains",
|
||||||
|
"mining_filter_tip_pool": "Afficher uniquement les gains du pool",
|
||||||
|
"mining_filter_tip_solo": "Afficher uniquement les gains solo",
|
||||||
|
"mining_idle_off_tooltip": "Activer le minage au repos",
|
||||||
|
"mining_idle_on_tooltip": "Désactiver le minage au repos",
|
||||||
|
"mining_local_hashrate": "Hashrate local",
|
||||||
|
"mining_mine": "Miner",
|
||||||
|
"mining_mining_addr": "Adr. minage",
|
||||||
|
"mining_network": "Réseau",
|
||||||
|
"mining_no_blocks_yet": "Aucun bloc trouvé pour l'instant",
|
||||||
|
"mining_no_payouts_yet": "Aucun paiement de pool pour l'instant",
|
||||||
|
"mining_no_saved_addresses": "Aucune adresse enregistrée",
|
||||||
|
"mining_no_saved_pools": "Aucun pool enregistré",
|
||||||
|
"mining_off": "Le minage est DÉSACTIVÉ",
|
||||||
|
"mining_on": "Le minage est ACTIVÉ",
|
||||||
|
"mining_open_in_explorer": "Ouvrir dans l'explorateur",
|
||||||
|
"mining_payout_address": "Adresse de paiement",
|
||||||
|
"mining_payout_tooltip": "Adresse pour recevoir les récompenses de minage",
|
||||||
|
"mining_pool": "Pool",
|
||||||
|
"mining_pool_hashrate": "Hashrate du pool",
|
||||||
|
"mining_pool_url": "URL du pool",
|
||||||
|
"mining_recent_blocks": "BLOCS RÉCENTS",
|
||||||
|
"mining_recent_payouts": "PAIEMENTS DE POOL RÉCENTS",
|
||||||
|
"mining_remove": "Supprimer",
|
||||||
|
"mining_reset_defaults": "Réinitialiser les paramètres",
|
||||||
|
"mining_save_payout_address": "Enregistrer l'adresse de paiement",
|
||||||
|
"mining_save_pool_url": "Enregistrer l'URL du pool",
|
||||||
|
"mining_saved_addresses": "Adresses enregistrées :",
|
||||||
|
"mining_saved_pools": "Pools enregistrés :",
|
||||||
|
"mining_shares": "Parts",
|
||||||
|
"mining_show_chart": "Graphique",
|
||||||
|
"mining_show_log": "Journal",
|
||||||
|
"mining_solo": "Solo",
|
||||||
|
"mining_starting": "Démarrage...",
|
||||||
|
"mining_starting_tooltip": "Le mineur démarre...",
|
||||||
|
"mining_statistics": "Statistiques de minage",
|
||||||
|
"mining_stop": "Arrêter",
|
||||||
|
"mining_stop_solo_for_pool": "Arrêtez le minage solo avant de démarrer le minage en pool",
|
||||||
|
"mining_stop_solo_for_pool_settings": "Arrêtez le minage solo pour modifier les paramètres du pool",
|
||||||
|
"mining_stopping": "Arrêt...",
|
||||||
|
"mining_stopping_tooltip": "Le mineur s'arrête...",
|
||||||
|
"mining_syncing_tooltip": "La blockchain se synchronise...",
|
||||||
|
"mining_threads": "Threads de minage",
|
||||||
|
"mining_to_save": "pour enregistrer",
|
||||||
|
"mining_today": "Aujourd'hui",
|
||||||
|
"mining_uptime": "Temps de fonctionnement",
|
||||||
|
"mining_yesterday": "Hier",
|
||||||
|
"network": "Réseau",
|
||||||
|
"network_fee": "FRAIS RÉSEAU",
|
||||||
|
"network_hashrate": "Hashrate du réseau",
|
||||||
|
"new": "+ Nouveau",
|
||||||
|
"new_shielded_created": "Nouvelle adresse blindée créée",
|
||||||
|
"new_t_address": "Nouvelle adresse T",
|
||||||
|
"new_t_transparent": "Nouvelle adresse t (Transparente)",
|
||||||
|
"new_transparent_created": "Nouvelle adresse transparente créée",
|
||||||
|
"new_z_address": "Nouvelle adresse Z",
|
||||||
|
"new_z_shielded": "Nouvelle adresse z (Blindée)",
|
||||||
|
"no_addresses": "Aucune adresse trouvée. Créez-en une avec les boutons ci-dessus.",
|
||||||
|
"no_addresses_available": "Aucune adresse disponible",
|
||||||
|
"no_addresses_match": "Aucune adresse ne correspond au filtre",
|
||||||
|
"no_addresses_with_balance": "Aucune adresse avec solde",
|
||||||
|
"no_matching": "Aucune transaction correspondante",
|
||||||
|
"no_recent_receives": "Aucune réception récente",
|
||||||
|
"no_recent_sends": "Aucun envoi récent",
|
||||||
|
"no_transactions": "Aucune transaction trouvée",
|
||||||
|
"node": "NŒUD",
|
||||||
|
"node_security": "NŒUD & SÉCURITÉ",
|
||||||
|
"noise": "Bruit",
|
||||||
|
"not_connected": "Non connecté au daemon...",
|
||||||
|
"not_connected_to_daemon": "Non connecté au daemon",
|
||||||
|
"notes": "Notes",
|
||||||
|
"notes_optional": "Notes (optionnel) :",
|
||||||
|
"output_filename": "Nom du fichier de sortie :",
|
||||||
|
"overview": "Aperçu",
|
||||||
|
"paste": "Coller",
|
||||||
|
"paste_from_clipboard": "Coller depuis le presse-papiers",
|
||||||
|
"pay_from": "Payer depuis",
|
||||||
|
"payment_request": "DEMANDE DE PAIEMENT",
|
||||||
|
"payment_request_copied": "Demande de paiement copiée",
|
||||||
|
"payment_uri_copied": "URI de paiement copiée",
|
||||||
|
"peers": "Pairs",
|
||||||
|
"peers_avg_ping": "Ping moyen",
|
||||||
|
"peers_ban_24h": "Bannir le pair 24h",
|
||||||
|
"peers_ban_score": "Score de ban : %d",
|
||||||
|
"peers_banned": "Bannis",
|
||||||
|
"peers_banned_count": "Bannis : %d",
|
||||||
|
"peers_best_block": "Meilleur bloc",
|
||||||
|
"peers_blockchain": "BLOCKCHAIN",
|
||||||
|
"peers_blocks": "Blocs",
|
||||||
|
"peers_blocks_left": "%d blocs restants",
|
||||||
|
"peers_clear_all_bans": "Lever tous les bannissements",
|
||||||
|
"peers_click_copy": "Cliquez pour copier",
|
||||||
|
"peers_connected": "Connectés",
|
||||||
|
"peers_connected_count": "Connectés : %d",
|
||||||
|
"peers_copy_ip": "Copier l'IP",
|
||||||
|
"peers_dir_in": "Ent.",
|
||||||
|
"peers_dir_out": "Sort.",
|
||||||
|
"peers_hash_copied": "Hash copié",
|
||||||
|
"peers_hashrate": "Hashrate",
|
||||||
|
"peers_in_out": "Ent./Sort.",
|
||||||
|
"peers_longest": "Plus longue",
|
||||||
|
"peers_longest_chain": "Plus longue chaîne",
|
||||||
|
"peers_memory": "Mémoire",
|
||||||
|
"peers_no_banned": "Aucun pair banni",
|
||||||
|
"peers_no_connected": "Aucun pair connecté",
|
||||||
|
"peers_no_tls": "Pas de TLS",
|
||||||
|
"peers_notarized": "Notarisé",
|
||||||
|
"peers_p2p_port": "Port P2P",
|
||||||
|
"peers_protocol": "Protocole",
|
||||||
|
"peers_received": "Reçu",
|
||||||
|
"peers_refresh": "Actualiser",
|
||||||
|
"peers_refresh_tooltip": "Actualiser la liste des pairs",
|
||||||
|
"peers_refreshing": "Actualisation...",
|
||||||
|
"peers_sent": "Envoyé",
|
||||||
|
"peers_tt_id": "ID : %d",
|
||||||
|
"peers_tt_received": "Reçu : %s",
|
||||||
|
"peers_tt_sent": "Envoyé : %s",
|
||||||
|
"peers_tt_services": "Services : %s",
|
||||||
|
"peers_tt_start_height": "Hauteur de départ : %d",
|
||||||
|
"peers_tt_synced": "Synchronisé H/B : %d/%d",
|
||||||
|
"peers_tt_tls_cipher": "TLS : %s",
|
||||||
|
"peers_unban": "Débannir",
|
||||||
|
"peers_upper": "PAIRS",
|
||||||
|
"peers_version": "Version",
|
||||||
|
"pending": "En attente",
|
||||||
|
"ping": "Ping",
|
||||||
|
"price_chart": "Graphique des prix",
|
||||||
|
"qr_code": "Code QR",
|
||||||
|
"qr_failed": "Échec de la génération du code QR",
|
||||||
|
"qr_title": "Code QR",
|
||||||
|
"qr_unavailable": "QR indisponible",
|
||||||
|
"receive": "Recevoir",
|
||||||
|
"received": "reçu",
|
||||||
|
"received_filter": "Reçu",
|
||||||
|
"received_label": "Reçu",
|
||||||
|
"received_upper": "REÇU",
|
||||||
|
"receiving_addresses": "Vos adresses de réception",
|
||||||
|
"recent_received": "REÇUS RÉCENTS",
|
||||||
|
"recent_sends": "ENVOIS RÉCENTS",
|
||||||
|
"recipient": "DESTINATAIRE",
|
||||||
|
"recv_type": "Reçu",
|
||||||
|
"refresh": "Actualiser",
|
||||||
|
"refresh_now": "Actualiser maintenant",
|
||||||
|
"report_bug": "Signaler un bug",
|
||||||
|
"request_amount": "Montant (optionnel) :",
|
||||||
|
"request_copy_uri": "Copier l'URI",
|
||||||
|
"request_description": "Générez une demande de paiement que d'autres peuvent scanner ou copier. Le code QR contient votre adresse et un montant/mémo optionnel.",
|
||||||
|
"request_label": "Libellé (optionnel) :",
|
||||||
|
"request_memo": "Mémo (optionnel) :",
|
||||||
|
"request_payment": "Demander un paiement",
|
||||||
|
"request_payment_uri": "URI de paiement :",
|
||||||
|
"request_receive_address": "Adresse de réception :",
|
||||||
|
"request_select_address": "Sélectionner une adresse...",
|
||||||
|
"request_shielded_addrs": "-- Adresses blindées --",
|
||||||
|
"request_title": "Demander un paiement",
|
||||||
|
"request_transparent_addrs": "-- Adresses transparentes --",
|
||||||
|
"request_uri_copied": "URI de paiement copiée dans le presse-papiers",
|
||||||
|
"rescan": "Re-scanner",
|
||||||
|
"reset_to_defaults": "Réinitialiser les paramètres",
|
||||||
|
"review_send": "Vérifier l'envoi",
|
||||||
|
"rpc_host": "Hôte RPC",
|
||||||
|
"rpc_pass": "Mot de passe",
|
||||||
|
"rpc_port": "Port",
|
||||||
|
"rpc_user": "Nom d'utilisateur",
|
||||||
|
"save": "Enregistrer",
|
||||||
|
"save_settings": "Enregistrer les paramètres",
|
||||||
|
"save_z_transactions": "Enregistrer les Z-tx dans la liste",
|
||||||
|
"search_placeholder": "Rechercher...",
|
||||||
|
"security": "SÉCURITÉ",
|
||||||
|
"select_address": "Sélectionner une adresse...",
|
||||||
|
"select_receiving_address": "Sélectionner une adresse de réception...",
|
||||||
|
"select_source_address": "Sélectionner une adresse source...",
|
||||||
|
"send": "Envoyer",
|
||||||
|
"send_amount": "Montant",
|
||||||
|
"send_amount_details": "DÉTAILS DU MONTANT",
|
||||||
|
"send_amount_upper": "MONTANT",
|
||||||
|
"send_clear_fields": "Effacer tous les champs du formulaire ?",
|
||||||
|
"send_copy_error": "Copier l'erreur",
|
||||||
|
"send_dismiss": "Ignorer",
|
||||||
|
"send_error_copied": "Erreur copiée dans le presse-papiers",
|
||||||
|
"send_error_prefix": "Erreur : %s",
|
||||||
|
"send_exceeds_available": "Dépasse le disponible (%.8f)",
|
||||||
|
"send_fee": "Frais",
|
||||||
|
"send_fee_high": "Élevés",
|
||||||
|
"send_fee_low": "Faibles",
|
||||||
|
"send_fee_normal": "Normal",
|
||||||
|
"send_form_restored": "Formulaire restauré",
|
||||||
|
"send_from_this_address": "Envoyer depuis cette adresse",
|
||||||
|
"send_go_to_receive": "Aller à Recevoir",
|
||||||
|
"send_keep": "Conserver",
|
||||||
|
"send_network_fee": "FRAIS RÉSEAU",
|
||||||
|
"send_no_balance": "Pas de solde",
|
||||||
|
"send_no_recent": "Aucun envoi récent",
|
||||||
|
"send_recent_sends": "ENVOIS RÉCENTS",
|
||||||
|
"send_recipient": "DESTINATAIRE",
|
||||||
|
"send_select_source": "Sélectionner une adresse source...",
|
||||||
|
"send_sending_from": "ENVOI DEPUIS",
|
||||||
|
"send_submitting": "Soumission de la transaction...",
|
||||||
|
"send_switch_to_receive": "Passez à Recevoir pour obtenir votre adresse et commencer à recevoir des fonds.",
|
||||||
|
"send_to": "Envoyer à",
|
||||||
|
"send_tooltip_enter_amount": "Entrez un montant à envoyer",
|
||||||
|
"send_tooltip_exceeds_balance": "Le montant dépasse le solde disponible",
|
||||||
|
"send_tooltip_in_progress": "Transaction déjà en cours",
|
||||||
|
"send_tooltip_invalid_address": "Entrez une adresse de destinataire valide",
|
||||||
|
"send_tooltip_not_connected": "Non connecté au daemon",
|
||||||
|
"send_tooltip_select_source": "Sélectionnez d'abord une adresse source",
|
||||||
|
"send_tooltip_syncing": "Attendez la synchronisation de la blockchain",
|
||||||
|
"send_total": "Total",
|
||||||
|
"send_transaction": "Envoyer la transaction",
|
||||||
|
"send_tx_failed": "Transaction échouée",
|
||||||
|
"send_tx_sent": "Transaction envoyée !",
|
||||||
|
"send_tx_success": "Transaction envoyée avec succès !",
|
||||||
|
"send_txid_copied": "TxID copié dans le presse-papiers",
|
||||||
|
"send_txid_label": "TxID : %s",
|
||||||
|
"send_valid_shielded": "Adresse blindée valide",
|
||||||
|
"send_valid_transparent": "Adresse transparente valide",
|
||||||
|
"send_wallet_empty": "Votre portefeuille est vide",
|
||||||
|
"send_yes_clear": "Oui, effacer",
|
||||||
|
"sending": "Envoi de la transaction",
|
||||||
|
"sending_from": "ENVOI DEPUIS",
|
||||||
|
"sent": "envoyé",
|
||||||
|
"sent_filter": "Envoyé",
|
||||||
|
"sent_type": "Envoyé",
|
||||||
|
"sent_upper": "ENVOYÉ",
|
||||||
|
"settings": "Paramètres",
|
||||||
|
"setup_wizard": "Assistant de configuration",
|
||||||
|
"share": "Partager",
|
||||||
|
"shield_check_status": "Vérifier le statut",
|
||||||
|
"shield_completed": "Opération terminée avec succès !",
|
||||||
|
"shield_description": "Blindez vos récompenses de minage en envoyant les sorties coinbase des adresses transparentes vers une adresse blindée. Cela améliore la confidentialité en masquant vos revenus de minage.",
|
||||||
|
"shield_from_address": "Depuis l'adresse :",
|
||||||
|
"shield_funds": "Blinder les fonds",
|
||||||
|
"shield_in_progress": "Opération en cours...",
|
||||||
|
"shield_max_utxos": "UTXOs max par opération",
|
||||||
|
"shield_merge_done": "Blindage/fusion terminé !",
|
||||||
|
"shield_select_z": "Sélectionner une z-adresse...",
|
||||||
|
"shield_started": "Opération de blindage démarrée",
|
||||||
|
"shield_title": "Blinder les récompenses coinbase",
|
||||||
|
"shield_to_address": "Vers l'adresse (blindée) :",
|
||||||
|
"shield_utxo_limit": "Limite UTXO :",
|
||||||
|
"shield_wildcard_hint": "Utilisez '*' pour blinder depuis toutes les adresses transparentes",
|
||||||
|
"shielded": "Blindé",
|
||||||
|
"shielded_to": "BLINDÉ VERS",
|
||||||
|
"shielded_type": "Blindé",
|
||||||
|
"show": "Afficher",
|
||||||
|
"show_qr_code": "Afficher le code QR",
|
||||||
|
"showing_transactions": "Affichage %d\xe2\x80\x93%d sur %d transactions (total : %zu)",
|
||||||
|
"simple_background": "Arrière-plan simple",
|
||||||
|
"start_mining": "Démarrer le minage",
|
||||||
|
"status": "Statut",
|
||||||
|
"stop_external": "Arrêter le daemon externe",
|
||||||
|
"stop_mining": "Arrêter le minage",
|
||||||
|
"submitting_transaction": "Soumission de la transaction...",
|
||||||
|
"success": "Succès",
|
||||||
|
"summary": "Résumé",
|
||||||
|
"syncing": "Synchronisation...",
|
||||||
|
"t_addresses": "Adresses T",
|
||||||
|
"test_connection": "Tester",
|
||||||
|
"theme": "Thème",
|
||||||
|
"theme_effects": "Effets de thème",
|
||||||
|
"time_days_ago": "il y a %d jours",
|
||||||
|
"time_hours_ago": "il y a %d heures",
|
||||||
|
"time_minutes_ago": "il y a %d minutes",
|
||||||
|
"time_seconds_ago": "il y a %d secondes",
|
||||||
|
"to": "À",
|
||||||
|
"to_upper": "À",
|
||||||
|
"tools": "OUTILS",
|
||||||
|
"total": "Total",
|
||||||
|
"transaction_id": "ID DE TRANSACTION",
|
||||||
|
"transaction_sent": "Transaction envoyée avec succès",
|
||||||
|
"transaction_sent_msg": "Transaction envoyée !",
|
||||||
|
"transaction_url": "URL de transaction",
|
||||||
|
"transactions": "Transactions",
|
||||||
|
"transactions_upper": "TRANSACTIONS",
|
||||||
|
"transparent": "Transparent",
|
||||||
|
"tx_confirmations": "%d confirmations",
|
||||||
|
"tx_details_title": "Détails de la transaction",
|
||||||
|
"tx_from_address": "Adresse d'origine :",
|
||||||
|
"tx_id_label": "ID de transaction :",
|
||||||
|
"tx_immature": "IMMATURE",
|
||||||
|
"tx_mined": "MINÉ",
|
||||||
|
"tx_received": "REÇU",
|
||||||
|
"tx_sent": "ENVOYÉ",
|
||||||
|
"tx_to_address": "Adresse de destination :",
|
||||||
|
"tx_view_explorer": "Voir dans l'explorateur",
|
||||||
|
"txs_count": "%d txs",
|
||||||
|
"type": "Type",
|
||||||
|
"ui_opacity": "Opacité de l'interface",
|
||||||
|
"unban": "Débannir",
|
||||||
|
"unconfirmed": "Non confirmé",
|
||||||
|
"undo_clear": "Annuler l'effacement",
|
||||||
|
"unknown": "Inconnu",
|
||||||
|
"use_embedded_daemon": "Utiliser le dragonxd intégré",
|
||||||
|
"use_tor": "Utiliser Tor",
|
||||||
|
"validate_btn": "Valider",
|
||||||
|
"validate_description": "Entrez une adresse DragonX pour vérifier si elle est valide et si elle appartient à ce portefeuille.",
|
||||||
|
"validate_invalid": "INVALIDE",
|
||||||
|
"validate_is_mine": "Ce portefeuille possède cette adresse",
|
||||||
|
"validate_not_mine": "N'appartient pas à ce portefeuille",
|
||||||
|
"validate_ownership": "Propriété :",
|
||||||
|
"validate_results": "Résultats :",
|
||||||
|
"validate_shielded_type": "Blindée (z-adresse)",
|
||||||
|
"validate_status": "Statut :",
|
||||||
|
"validate_title": "Valider l'adresse",
|
||||||
|
"validate_transparent_type": "Transparente (t-adresse)",
|
||||||
|
"validate_type": "Type :",
|
||||||
|
"validate_valid": "VALIDE",
|
||||||
|
"validating": "Validation...",
|
||||||
|
"verbose_logging": "Journalisation détaillée",
|
||||||
|
"version": "Version",
|
||||||
|
"view": "Afficher",
|
||||||
|
"view_details": "Voir les détails",
|
||||||
|
"view_on_explorer": "Voir dans l'explorateur",
|
||||||
|
"waiting_for_daemon": "En attente de la connexion au daemon...",
|
||||||
|
"wallet": "PORTEFEUILLE",
|
||||||
|
"wallet_empty": "Votre portefeuille est vide",
|
||||||
|
"wallet_empty_hint": "Passez à Recevoir pour obtenir votre adresse et commencer à recevoir des fonds.",
|
||||||
|
"warning": "Attention",
|
||||||
|
"warning_upper": "ATTENTION !",
|
||||||
|
"website": "Site web",
|
||||||
|
"window_opacity": "Opacité de la fenêtre",
|
||||||
|
"yes_clear": "Oui, effacer",
|
||||||
|
"your_addresses": "Vos adresses",
|
||||||
|
"z_addresses": "Adresses Z",
|
||||||
|
}
|
||||||
|
|
||||||
|
out = os.path.join(os.path.dirname(__file__), "..", "res", "lang", "fr.json")
|
||||||
|
with open(out, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(translations, f, indent=4, ensure_ascii=False, sort_keys=True)
|
||||||
|
print(f"Wrote {len(translations)} French translations to {os.path.abspath(out)}")
|
||||||
646
scripts/gen_ja.py
Normal file
@@ -0,0 +1,646 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Generate Japanese (ja) translations for ObsidianDragon wallet."""
|
||||||
|
import json, os
|
||||||
|
|
||||||
|
translations = {
|
||||||
|
"24h_change": "24時間変動",
|
||||||
|
"24h_volume": "24時間出来高",
|
||||||
|
"about": "概要",
|
||||||
|
"about_block_explorer": "ブロックエクスプローラー",
|
||||||
|
"about_block_height": "ブロック高:",
|
||||||
|
"about_build_date": "ビルド日:",
|
||||||
|
"about_build_type": "ビルドタイプ:",
|
||||||
|
"about_chain": "チェーン:",
|
||||||
|
"about_connections": "接続数:",
|
||||||
|
"about_credits": "クレジット",
|
||||||
|
"about_daemon": "デーモン:",
|
||||||
|
"about_debug": "デバッグ",
|
||||||
|
"about_dragonx": "ObsidianDragonについて",
|
||||||
|
"about_edition": "ImGui エディション",
|
||||||
|
"about_github": "GitHub",
|
||||||
|
"about_imgui": "ImGui:",
|
||||||
|
"about_license": "ライセンス",
|
||||||
|
"about_license_text": "本ソフトウェアはGNU General Public License v3 (GPLv3)の下で公開されています。ライセンス条項に従い、自由に使用、変更、配布できます。",
|
||||||
|
"about_peers_count": "%zu ピア",
|
||||||
|
"about_release": "リリース",
|
||||||
|
"about_title": "ObsidianDragonについて",
|
||||||
|
"about_version": "バージョン:",
|
||||||
|
"about_website": "ウェブサイト",
|
||||||
|
"acrylic": "アクリル",
|
||||||
|
"add": "追加",
|
||||||
|
"address": "アドレス",
|
||||||
|
"address_book_add": "アドレスを追加",
|
||||||
|
"address_book_add_new": "新規追加",
|
||||||
|
"address_book_added": "アドレスをアドレス帳に追加しました",
|
||||||
|
"address_book_count": "%zu 件のアドレスを保存済み",
|
||||||
|
"address_book_deleted": "エントリを削除しました",
|
||||||
|
"address_book_edit": "アドレスを編集",
|
||||||
|
"address_book_empty": "保存されたアドレスがありません。「新規追加」をクリックして追加してください。",
|
||||||
|
"address_book_exists": "アドレスは既にアドレス帳に存在します",
|
||||||
|
"address_book_title": "アドレス帳",
|
||||||
|
"address_book_update_failed": "更新に失敗しました — アドレスが重複している可能性があります",
|
||||||
|
"address_book_updated": "アドレスを更新しました",
|
||||||
|
"address_copied": "アドレスをクリップボードにコピーしました",
|
||||||
|
"address_details": "アドレス詳細",
|
||||||
|
"address_label": "アドレス:",
|
||||||
|
"address_upper": "アドレス",
|
||||||
|
"address_url": "アドレスURL",
|
||||||
|
"addresses_appear_here": "接続後、受信アドレスがここに表示されます。",
|
||||||
|
"advanced": "詳細設定",
|
||||||
|
"all_filter": "すべて",
|
||||||
|
"allow_custom_fees": "カスタム手数料を許可",
|
||||||
|
"amount": "金額",
|
||||||
|
"amount_details": "金額の詳細",
|
||||||
|
"amount_exceeds_balance": "金額が残高を超えています",
|
||||||
|
"amount_label": "金額:",
|
||||||
|
"appearance": "外観",
|
||||||
|
"auto_shield": "マイニング自動シールド",
|
||||||
|
"available": "利用可能",
|
||||||
|
"backup_backing_up": "バックアップ中...",
|
||||||
|
"backup_create": "バックアップを作成",
|
||||||
|
"backup_created": "ウォレットのバックアップを作成しました",
|
||||||
|
"backup_data": "バックアップとデータ",
|
||||||
|
"backup_description": "wallet.datファイルのバックアップを作成します。このファイルにはすべての秘密鍵と取引履歴が含まれています。バックアップは安全な場所に保管してください。",
|
||||||
|
"backup_destination": "バックアップ先:",
|
||||||
|
"backup_tip_external": "外部ドライブまたはクラウドストレージにバックアップを保存",
|
||||||
|
"backup_tip_multiple": "異なる場所に複数のバックアップを作成",
|
||||||
|
"backup_tip_test": "定期的にバックアップからの復元をテスト",
|
||||||
|
"backup_tips": "ヒント:",
|
||||||
|
"backup_title": "ウォレットのバックアップ",
|
||||||
|
"backup_wallet": "ウォレットをバックアップ...",
|
||||||
|
"backup_wallet_not_found": "警告:予想される場所にwallet.datが見つかりません",
|
||||||
|
"balance": "残高",
|
||||||
|
"balance_layout": "残高レイアウト",
|
||||||
|
"ban": "ブロック",
|
||||||
|
"banned_peers": "ブロック済みピア",
|
||||||
|
"block": "ブロック",
|
||||||
|
"block_bits": "ビット:",
|
||||||
|
"block_click_next": "クリックして次のブロックを表示",
|
||||||
|
"block_click_prev": "クリックして前のブロックを表示",
|
||||||
|
"block_explorer": "ブロックエクスプローラー",
|
||||||
|
"block_get_info": "ブロック情報を取得",
|
||||||
|
"block_hash": "ブロックハッシュ:",
|
||||||
|
"block_height": "ブロック高:",
|
||||||
|
"block_info_title": "ブロック情報",
|
||||||
|
"block_merkle_root": "マークルルート:",
|
||||||
|
"block_nav_next": "次へ >>",
|
||||||
|
"block_nav_prev": "<< 前へ",
|
||||||
|
"block_next": "次のブロック:",
|
||||||
|
"block_previous": "前のブロック:",
|
||||||
|
"block_size": "サイズ:",
|
||||||
|
"block_timestamp": "タイムスタンプ:",
|
||||||
|
"block_transactions": "トランザクション:",
|
||||||
|
"blockchain_syncing": "ブロックチェーン同期中 (%.1f%%)... 残高が不正確な場合があります。",
|
||||||
|
"cancel": "キャンセル",
|
||||||
|
"characters": "文字",
|
||||||
|
"clear": "クリア",
|
||||||
|
"clear_all_bans": "すべてのブロックを解除",
|
||||||
|
"clear_form_confirm": "すべてのフォームフィールドをクリアしますか?",
|
||||||
|
"clear_request": "リクエストをクリア",
|
||||||
|
"click_copy_address": "クリックしてアドレスをコピー",
|
||||||
|
"click_copy_uri": "クリックしてURIをコピー",
|
||||||
|
"close": "閉じる",
|
||||||
|
"conf_count": "%d 確認",
|
||||||
|
"confirm_and_send": "確認して送金",
|
||||||
|
"confirm_send": "送金を確認",
|
||||||
|
"confirm_transaction": "取引を確認",
|
||||||
|
"confirmations": "確認数",
|
||||||
|
"confirmations_display": "%d 確認 | %s",
|
||||||
|
"confirmed": "確認済み",
|
||||||
|
"connected": "接続済み",
|
||||||
|
"connected_peers": "接続中のピア",
|
||||||
|
"connecting": "接続中...",
|
||||||
|
"console": "コンソール",
|
||||||
|
"console_auto_scroll": "自動スクロール",
|
||||||
|
"console_available_commands": "利用可能なコマンド:",
|
||||||
|
"console_capturing_output": "デーモン出力をキャプチャ中...",
|
||||||
|
"console_clear": "クリア",
|
||||||
|
"console_clear_console": "コンソールをクリア",
|
||||||
|
"console_cleared": "コンソールをクリアしました",
|
||||||
|
"console_click_commands": "上のコマンドをクリックして挿入",
|
||||||
|
"console_click_insert": "クリックして挿入",
|
||||||
|
"console_click_insert_params": "クリックしてパラメータ付きで挿入",
|
||||||
|
"console_close": "閉じる",
|
||||||
|
"console_commands": "コマンド",
|
||||||
|
"console_common_rpc": "一般的なRPCコマンド:",
|
||||||
|
"console_completions": "補完:",
|
||||||
|
"console_connected": "デーモンに接続済み",
|
||||||
|
"console_copy_all": "すべてコピー",
|
||||||
|
"console_copy_selected": "コピー",
|
||||||
|
"console_daemon": "デーモン",
|
||||||
|
"console_daemon_error": "デーモンエラー!",
|
||||||
|
"console_daemon_started": "デーモンが起動しました",
|
||||||
|
"console_daemon_stopped": "デーモンが停止しました",
|
||||||
|
"console_disconnected": "デーモンから切断されました",
|
||||||
|
"console_errors": "エラー",
|
||||||
|
"console_filter_hint": "出力をフィルタ...",
|
||||||
|
"console_help_clear": " clear - コンソールをクリア",
|
||||||
|
"console_help_getbalance": " getbalance - 透明残高を表示",
|
||||||
|
"console_help_getblockcount": " getblockcount - 現在のブロック高を表示",
|
||||||
|
"console_help_getinfo": " getinfo - ノード情報を表示",
|
||||||
|
"console_help_getmininginfo": " getmininginfo - マイニング状況を表示",
|
||||||
|
"console_help_getpeerinfo": " getpeerinfo - 接続中のピアを表示",
|
||||||
|
"console_help_gettotalbalance": " gettotalbalance - 合計残高を表示",
|
||||||
|
"console_help_help": " help - このヘルプを表示",
|
||||||
|
"console_help_setgenerate": " setgenerate - マイニングを制御",
|
||||||
|
"console_help_stop": " stop - デーモンを停止",
|
||||||
|
"console_line_count": "%zu 行",
|
||||||
|
"console_new_lines": "%d 新しい行",
|
||||||
|
"console_no_daemon": "デーモンなし",
|
||||||
|
"console_not_connected": "エラー:デーモンに接続されていません",
|
||||||
|
"console_rpc_reference": "RPCコマンドリファレンス",
|
||||||
|
"console_scanline": "コンソールスキャンライン",
|
||||||
|
"console_search_commands": "コマンドを検索...",
|
||||||
|
"console_select_all": "すべて選択",
|
||||||
|
"console_show_daemon_output": "デーモン出力を表示",
|
||||||
|
"console_show_errors_only": "エラーのみ表示",
|
||||||
|
"console_show_rpc_ref": "RPCコマンドリファレンスを表示",
|
||||||
|
"console_showing_lines": "%zu / %zu 行を表示中",
|
||||||
|
"console_starting_node": "ノードを起動中...",
|
||||||
|
"console_status_error": "エラー",
|
||||||
|
"console_status_running": "実行中",
|
||||||
|
"console_status_starting": "起動中",
|
||||||
|
"console_status_stopped": "停止済み",
|
||||||
|
"console_status_stopping": "停止中",
|
||||||
|
"console_status_unknown": "不明",
|
||||||
|
"console_tab_completion": "Tabで補完",
|
||||||
|
"console_type_help": "'help'と入力して利用可能なコマンドを表示",
|
||||||
|
"console_welcome": "ObsidianDragonコンソールへようこそ",
|
||||||
|
"console_zoom_in": "拡大",
|
||||||
|
"console_zoom_out": "縮小",
|
||||||
|
"copy": "コピー",
|
||||||
|
"copy_address": "完全なアドレスをコピー",
|
||||||
|
"copy_error": "エラーをコピー",
|
||||||
|
"copy_to_clipboard": "クリップボードにコピー",
|
||||||
|
"copy_txid": "TxIDをコピー",
|
||||||
|
"copy_uri": "URIをコピー",
|
||||||
|
"current_price": "現在の価格",
|
||||||
|
"custom_fees": "カスタム手数料",
|
||||||
|
"dark": "ダーク",
|
||||||
|
"date": "日付",
|
||||||
|
"date_label": "日付:",
|
||||||
|
"delete": "削除",
|
||||||
|
"difficulty": "難易度",
|
||||||
|
"disconnected": "切断済み",
|
||||||
|
"dismiss": "閉じる",
|
||||||
|
"display": "表示",
|
||||||
|
"dragonx_green": "DragonX(グリーン)",
|
||||||
|
"edit": "編集",
|
||||||
|
"error": "エラー",
|
||||||
|
"est_time_to_block": "予測ブロック時間",
|
||||||
|
"exit": "終了",
|
||||||
|
"explorer": "エクスプローラー",
|
||||||
|
"export": "エクスポート",
|
||||||
|
"export_csv": "CSVエクスポート",
|
||||||
|
"export_keys_btn": "鍵をエクスポート",
|
||||||
|
"export_keys_danger": "危険:ウォレットからすべての秘密鍵がエクスポートされます!このファイルにアクセスできる人は誰でもあなたの資金を盗めます。安全に保管し、使用後は削除してください。",
|
||||||
|
"export_keys_include_t": "Tアドレスを含める(透明)",
|
||||||
|
"export_keys_include_z": "Zアドレスを含める(シールド)",
|
||||||
|
"export_keys_options": "エクスポートオプション:",
|
||||||
|
"export_keys_success": "鍵のエクスポートに成功しました",
|
||||||
|
"export_keys_title": "すべての秘密鍵をエクスポート",
|
||||||
|
"export_private_key": "秘密鍵をエクスポート",
|
||||||
|
"export_tx_count": "%zu件の取引をCSVファイルにエクスポート。",
|
||||||
|
"export_tx_file_fail": "CSVファイルの作成に失敗しました",
|
||||||
|
"export_tx_none": "エクスポートする取引がありません",
|
||||||
|
"export_tx_success": "取引のエクスポートに成功しました",
|
||||||
|
"export_tx_title": "取引をCSVにエクスポート",
|
||||||
|
"export_viewing_key": "閲覧鍵をエクスポート",
|
||||||
|
"failed_create_shielded": "シールドアドレスの作成に失敗しました",
|
||||||
|
"failed_create_transparent": "透明アドレスの作成に失敗しました",
|
||||||
|
"fee": "手数料",
|
||||||
|
"fee_high": "高い",
|
||||||
|
"fee_label": "手数料:",
|
||||||
|
"fee_low": "低い",
|
||||||
|
"fee_normal": "通常",
|
||||||
|
"fetch_prices": "価格を取得",
|
||||||
|
"file": "ファイル",
|
||||||
|
"file_save_location": "ファイルの保存先:~/.config/ObsidianDragon/",
|
||||||
|
"font_scale": "フォントサイズ",
|
||||||
|
"from": "送信元",
|
||||||
|
"from_upper": "送信元",
|
||||||
|
"full_details": "詳細情報",
|
||||||
|
"general": "一般",
|
||||||
|
"go_to_receive": "受信へ移動",
|
||||||
|
"height": "高さ",
|
||||||
|
"help": "ヘルプ",
|
||||||
|
"hide": "非表示",
|
||||||
|
"history": "履歴",
|
||||||
|
"immature_type": "未成熟",
|
||||||
|
"import": "インポート",
|
||||||
|
"import_key_btn": "鍵をインポート",
|
||||||
|
"import_key_formats": "サポートされる鍵形式:",
|
||||||
|
"import_key_full_rescan": "(0 = 完全再スキャン)",
|
||||||
|
"import_key_label": "秘密鍵:",
|
||||||
|
"import_key_no_valid": "入力に有効な鍵が見つかりません",
|
||||||
|
"import_key_rescan": "インポート後にブロックチェーンを再スキャン",
|
||||||
|
"import_key_start_height": "開始高:",
|
||||||
|
"import_key_success": "鍵のインポートに成功しました",
|
||||||
|
"import_key_t_format": "TアドレスWIF秘密鍵",
|
||||||
|
"import_key_title": "秘密鍵をインポート",
|
||||||
|
"import_key_tooltip": "1行に1つずつ秘密鍵を入力してください。\nzアドレスとtアドレスの鍵の両方に対応しています。\n#で始まる行はコメントとして扱われます。",
|
||||||
|
"import_key_warning": "警告:秘密鍵を決して共有しないでください!信頼できないソースからの鍵のインポートはウォレットを危険にさらす可能性があります。",
|
||||||
|
"import_key_z_format": "Zアドレス支出鍵 (secret-extended-key-...)",
|
||||||
|
"import_private_key": "秘密鍵をインポート...",
|
||||||
|
"invalid_address": "無効なアドレス形式",
|
||||||
|
"ip_address": "IPアドレス",
|
||||||
|
"keep": "保持",
|
||||||
|
"keep_daemon": "デーモンを実行し続ける",
|
||||||
|
"key_export_fetching": "ウォレットから鍵を取得中...",
|
||||||
|
"key_export_private_key": "秘密鍵:",
|
||||||
|
"key_export_private_warning": "この鍵は秘密にしてください!この鍵を持つ人は誰でもあなたの資金を使えます。オンラインや信頼できない相手と共有しないでください。",
|
||||||
|
"key_export_reveal": "鍵を表示",
|
||||||
|
"key_export_viewing_key": "閲覧鍵:",
|
||||||
|
"key_export_viewing_warning": "この閲覧鍵を使うと、他者があなたの受信取引と残高を見ることができますが、資金を使うことはできません。信頼できる相手とのみ共有してください。",
|
||||||
|
"label": "ラベル:",
|
||||||
|
"language": "言語",
|
||||||
|
"light": "ライト",
|
||||||
|
"loading": "読み込み中...",
|
||||||
|
"loading_addresses": "アドレスを読み込み中...",
|
||||||
|
"local_hashrate": "ローカルハッシュレート",
|
||||||
|
"low_spec_mode": "省電力モード",
|
||||||
|
"market": "市場",
|
||||||
|
"market_12h": "12時間",
|
||||||
|
"market_18h": "18時間",
|
||||||
|
"market_24h": "24時間",
|
||||||
|
"market_24h_volume": "24時間出来高",
|
||||||
|
"market_6h": "6時間",
|
||||||
|
"market_attribution": "価格データ:NonKYC提供",
|
||||||
|
"market_btc_price": "BTC価格",
|
||||||
|
"market_cap": "時価総額",
|
||||||
|
"market_no_history": "価格履歴がありません",
|
||||||
|
"market_no_price": "価格データなし",
|
||||||
|
"market_now": "現在",
|
||||||
|
"market_pct_shielded": "%.0f%% シールド済み",
|
||||||
|
"market_portfolio": "ポートフォリオ",
|
||||||
|
"market_price_unavailable": "価格データが利用できません",
|
||||||
|
"market_refresh_price": "価格データを更新",
|
||||||
|
"market_trade_on": "%s で取引",
|
||||||
|
"mature": "成熟済み",
|
||||||
|
"max": "最大",
|
||||||
|
"memo": "メモ(任意、暗号化)",
|
||||||
|
"memo_label": "メモ:",
|
||||||
|
"memo_optional": "メモ(任意)",
|
||||||
|
"memo_upper": "メモ",
|
||||||
|
"memo_z_only": "注:メモはシールド (z) アドレスへの送金時のみ利用可能です",
|
||||||
|
"merge_description": "複数のUTXOを単一のシールドアドレスに統合します。ウォレットサイズの縮小とプライバシーの向上に役立ちます。",
|
||||||
|
"merge_funds": "資金を統合",
|
||||||
|
"merge_started": "統合操作を開始しました",
|
||||||
|
"merge_title": "アドレスに統合",
|
||||||
|
"mine_when_idle": "アイドル時にマイニング",
|
||||||
|
"mined": "採掘済み",
|
||||||
|
"mined_filter": "採掘済み",
|
||||||
|
"mined_type": "採掘済み",
|
||||||
|
"mined_upper": "採掘済み",
|
||||||
|
"miner_fee": "マイナー手数料",
|
||||||
|
"mining": "マイニング",
|
||||||
|
"mining_active": "アクティブ",
|
||||||
|
"mining_address_copied": "マイニングアドレスをコピーしました",
|
||||||
|
"mining_all_time": "全期間",
|
||||||
|
"mining_already_saved": "プールURLは既に保存済みです",
|
||||||
|
"mining_block_copied": "ブロックハッシュをコピーしました",
|
||||||
|
"mining_chart_1m_ago": "1分前",
|
||||||
|
"mining_chart_5m_ago": "5分前",
|
||||||
|
"mining_chart_now": "現在",
|
||||||
|
"mining_chart_start": "開始",
|
||||||
|
"mining_click": "クリック",
|
||||||
|
"mining_click_copy_address": "クリックしてアドレスをコピー",
|
||||||
|
"mining_click_copy_block": "クリックしてブロックハッシュをコピー",
|
||||||
|
"mining_click_copy_difficulty": "クリックして難易度をコピー",
|
||||||
|
"mining_connected": "接続済み",
|
||||||
|
"mining_connecting": "接続中...",
|
||||||
|
"mining_control": "マイニング制御",
|
||||||
|
"mining_difficulty_copied": "難易度をコピーしました",
|
||||||
|
"mining_est_block": "予測ブロック",
|
||||||
|
"mining_est_daily": "予測日収",
|
||||||
|
"mining_filter_all": "すべて",
|
||||||
|
"mining_filter_tip_all": "すべての収益を表示",
|
||||||
|
"mining_filter_tip_pool": "プール収益のみ表示",
|
||||||
|
"mining_filter_tip_solo": "ソロ収益のみ表示",
|
||||||
|
"mining_idle_off_tooltip": "アイドルマイニングを有効にする",
|
||||||
|
"mining_idle_on_tooltip": "アイドルマイニングを無効にする",
|
||||||
|
"mining_local_hashrate": "ローカルハッシュレート",
|
||||||
|
"mining_mine": "マイニング",
|
||||||
|
"mining_mining_addr": "マイニングアドレス",
|
||||||
|
"mining_network": "ネットワーク",
|
||||||
|
"mining_no_blocks_yet": "まだブロックが見つかっていません",
|
||||||
|
"mining_no_payouts_yet": "まだプール支払いがありません",
|
||||||
|
"mining_no_saved_addresses": "保存されたアドレスがありません",
|
||||||
|
"mining_no_saved_pools": "保存されたプールがありません",
|
||||||
|
"mining_off": "マイニングはオフです",
|
||||||
|
"mining_on": "マイニングはオンです",
|
||||||
|
"mining_open_in_explorer": "エクスプローラーで開く",
|
||||||
|
"mining_payout_address": "支払いアドレス",
|
||||||
|
"mining_payout_tooltip": "マイニング報酬の受取アドレス",
|
||||||
|
"mining_pool": "プール",
|
||||||
|
"mining_pool_hashrate": "プールハッシュレート",
|
||||||
|
"mining_pool_url": "プールURL",
|
||||||
|
"mining_recent_blocks": "最近のブロック",
|
||||||
|
"mining_recent_payouts": "最近のプール支払い",
|
||||||
|
"mining_remove": "削除",
|
||||||
|
"mining_reset_defaults": "デフォルトにリセット",
|
||||||
|
"mining_save_payout_address": "支払いアドレスを保存",
|
||||||
|
"mining_save_pool_url": "プールURLを保存",
|
||||||
|
"mining_saved_addresses": "保存済みアドレス:",
|
||||||
|
"mining_saved_pools": "保存済みプール:",
|
||||||
|
"mining_shares": "シェア",
|
||||||
|
"mining_show_chart": "チャート",
|
||||||
|
"mining_show_log": "ログ",
|
||||||
|
"mining_solo": "ソロ",
|
||||||
|
"mining_starting": "起動中...",
|
||||||
|
"mining_starting_tooltip": "マイナーを起動中...",
|
||||||
|
"mining_statistics": "マイニング統計",
|
||||||
|
"mining_stop": "停止",
|
||||||
|
"mining_stop_solo_for_pool": "プールマイニングを開始する前にソロマイニングを停止してください",
|
||||||
|
"mining_stop_solo_for_pool_settings": "プール設定を変更するにはソロマイニングを停止してください",
|
||||||
|
"mining_stopping": "停止中...",
|
||||||
|
"mining_stopping_tooltip": "マイナーを停止中...",
|
||||||
|
"mining_syncing_tooltip": "ブロックチェーン同期中...",
|
||||||
|
"mining_threads": "マイニングスレッド",
|
||||||
|
"mining_to_save": "保存する",
|
||||||
|
"mining_today": "今日",
|
||||||
|
"mining_uptime": "稼働時間",
|
||||||
|
"mining_yesterday": "昨日",
|
||||||
|
"network": "ネットワーク",
|
||||||
|
"network_fee": "ネットワーク手数料",
|
||||||
|
"network_hashrate": "ネットワークハッシュレート",
|
||||||
|
"new": "+ 新規",
|
||||||
|
"new_shielded_created": "新しいシールドアドレスを作成しました",
|
||||||
|
"new_t_address": "新しいTアドレス",
|
||||||
|
"new_t_transparent": "新しいtアドレス(透明)",
|
||||||
|
"new_transparent_created": "新しい透明アドレスを作成しました",
|
||||||
|
"new_z_address": "新しいZアドレス",
|
||||||
|
"new_z_shielded": "新しいzアドレス(シールド)",
|
||||||
|
"no_addresses": "アドレスが見つかりません。上のボタンを使用して作成してください。",
|
||||||
|
"no_addresses_available": "利用可能なアドレスがありません",
|
||||||
|
"no_addresses_match": "フィルタに一致するアドレスがありません",
|
||||||
|
"no_addresses_with_balance": "残高のあるアドレスがありません",
|
||||||
|
"no_matching": "一致する取引がありません",
|
||||||
|
"no_recent_receives": "最近の受信がありません",
|
||||||
|
"no_recent_sends": "最近の送信がありません",
|
||||||
|
"no_transactions": "取引が見つかりません",
|
||||||
|
"node": "ノード",
|
||||||
|
"node_security": "ノードとセキュリティ",
|
||||||
|
"noise": "ノイズ",
|
||||||
|
"not_connected": "デーモンに未接続...",
|
||||||
|
"not_connected_to_daemon": "デーモンに未接続",
|
||||||
|
"notes": "メモ",
|
||||||
|
"notes_optional": "メモ(任意):",
|
||||||
|
"output_filename": "出力ファイル名:",
|
||||||
|
"overview": "概要",
|
||||||
|
"paste": "貼り付け",
|
||||||
|
"paste_from_clipboard": "クリップボードから貼り付け",
|
||||||
|
"pay_from": "支払い元",
|
||||||
|
"payment_request": "支払い請求",
|
||||||
|
"payment_request_copied": "支払い請求をコピーしました",
|
||||||
|
"payment_uri_copied": "支払いURIをコピーしました",
|
||||||
|
"peers": "ピア",
|
||||||
|
"peers_avg_ping": "平均Ping",
|
||||||
|
"peers_ban_24h": "ピアを24時間ブロック",
|
||||||
|
"peers_ban_score": "ブロックスコア:%d",
|
||||||
|
"peers_banned": "ブロック済み",
|
||||||
|
"peers_banned_count": "ブロック済み:%d",
|
||||||
|
"peers_best_block": "最良ブロック",
|
||||||
|
"peers_blockchain": "ブロックチェーン",
|
||||||
|
"peers_blocks": "ブロック",
|
||||||
|
"peers_blocks_left": "残り %d ブロック",
|
||||||
|
"peers_clear_all_bans": "すべてのブロックを解除",
|
||||||
|
"peers_click_copy": "クリックしてコピー",
|
||||||
|
"peers_connected": "接続済み",
|
||||||
|
"peers_connected_count": "接続済み:%d",
|
||||||
|
"peers_copy_ip": "IPをコピー",
|
||||||
|
"peers_dir_in": "入",
|
||||||
|
"peers_dir_out": "出",
|
||||||
|
"peers_hash_copied": "ハッシュをコピーしました",
|
||||||
|
"peers_hashrate": "ハッシュレート",
|
||||||
|
"peers_in_out": "入/出",
|
||||||
|
"peers_longest": "最長",
|
||||||
|
"peers_longest_chain": "最長チェーン",
|
||||||
|
"peers_memory": "メモリ",
|
||||||
|
"peers_no_banned": "ブロック済みピアなし",
|
||||||
|
"peers_no_connected": "接続済みピアなし",
|
||||||
|
"peers_no_tls": "TLSなし",
|
||||||
|
"peers_notarized": "公証済み",
|
||||||
|
"peers_p2p_port": "P2Pポート",
|
||||||
|
"peers_protocol": "プロトコル",
|
||||||
|
"peers_received": "受信",
|
||||||
|
"peers_refresh": "更新",
|
||||||
|
"peers_refresh_tooltip": "ピアリストを更新",
|
||||||
|
"peers_refreshing": "更新中...",
|
||||||
|
"peers_sent": "送信",
|
||||||
|
"peers_tt_id": "ID:%d",
|
||||||
|
"peers_tt_received": "受信:%s",
|
||||||
|
"peers_tt_sent": "送信:%s",
|
||||||
|
"peers_tt_services": "サービス:%s",
|
||||||
|
"peers_tt_start_height": "開始高:%d",
|
||||||
|
"peers_tt_synced": "同期済み H/B:%d/%d",
|
||||||
|
"peers_tt_tls_cipher": "TLS:%s",
|
||||||
|
"peers_unban": "ブロック解除",
|
||||||
|
"peers_upper": "ピア",
|
||||||
|
"peers_version": "バージョン",
|
||||||
|
"pending": "保留中",
|
||||||
|
"ping": "Ping",
|
||||||
|
"price_chart": "価格チャート",
|
||||||
|
"qr_code": "QRコード",
|
||||||
|
"qr_failed": "QRコードの生成に失敗しました",
|
||||||
|
"qr_title": "QRコード",
|
||||||
|
"qr_unavailable": "QR利用不可",
|
||||||
|
"receive": "受信",
|
||||||
|
"received": "受信済み",
|
||||||
|
"received_filter": "受信済み",
|
||||||
|
"received_label": "受信済み",
|
||||||
|
"received_upper": "受信済み",
|
||||||
|
"receiving_addresses": "あなたの受信アドレス",
|
||||||
|
"recent_received": "最近の受信",
|
||||||
|
"recent_sends": "最近の送信",
|
||||||
|
"recipient": "受取人",
|
||||||
|
"recv_type": "受信",
|
||||||
|
"refresh": "更新",
|
||||||
|
"refresh_now": "今すぐ更新",
|
||||||
|
"report_bug": "バグを報告",
|
||||||
|
"request_amount": "金額(任意):",
|
||||||
|
"request_copy_uri": "URIをコピー",
|
||||||
|
"request_description": "他の人がスキャンまたはコピーできる支払い請求を生成します。QRコードにはアドレスとオプションの金額/メモが含まれます。",
|
||||||
|
"request_label": "ラベル(任意):",
|
||||||
|
"request_memo": "メモ(任意):",
|
||||||
|
"request_payment": "支払いを請求",
|
||||||
|
"request_payment_uri": "支払いURI:",
|
||||||
|
"request_receive_address": "受信アドレス:",
|
||||||
|
"request_select_address": "アドレスを選択...",
|
||||||
|
"request_shielded_addrs": "-- シールドアドレス --",
|
||||||
|
"request_title": "支払いを請求",
|
||||||
|
"request_transparent_addrs": "-- 透明アドレス --",
|
||||||
|
"request_uri_copied": "支払いURIをクリップボードにコピーしました",
|
||||||
|
"rescan": "再スキャン",
|
||||||
|
"reset_to_defaults": "デフォルトにリセット",
|
||||||
|
"review_send": "送金を確認",
|
||||||
|
"rpc_host": "RPCホスト",
|
||||||
|
"rpc_pass": "パスワード",
|
||||||
|
"rpc_port": "ポート",
|
||||||
|
"rpc_user": "ユーザー名",
|
||||||
|
"save": "保存",
|
||||||
|
"save_settings": "設定を保存",
|
||||||
|
"save_z_transactions": "Z取引を取引リストに保存",
|
||||||
|
"search_placeholder": "検索...",
|
||||||
|
"security": "セキュリティ",
|
||||||
|
"select_address": "アドレスを選択...",
|
||||||
|
"select_receiving_address": "受信アドレスを選択...",
|
||||||
|
"select_source_address": "送信元アドレスを選択...",
|
||||||
|
"send": "送金",
|
||||||
|
"send_amount": "金額",
|
||||||
|
"send_amount_details": "金額の詳細",
|
||||||
|
"send_amount_upper": "金額",
|
||||||
|
"send_clear_fields": "すべてのフォームフィールドをクリアしますか?",
|
||||||
|
"send_copy_error": "エラーをコピー",
|
||||||
|
"send_dismiss": "閉じる",
|
||||||
|
"send_error_copied": "エラーをクリップボードにコピーしました",
|
||||||
|
"send_error_prefix": "エラー:%s",
|
||||||
|
"send_exceeds_available": "利用可能額を超過 (%.8f)",
|
||||||
|
"send_fee": "手数料",
|
||||||
|
"send_fee_high": "高い",
|
||||||
|
"send_fee_low": "低い",
|
||||||
|
"send_fee_normal": "通常",
|
||||||
|
"send_form_restored": "フォームが復元されました",
|
||||||
|
"send_from_this_address": "このアドレスから送金",
|
||||||
|
"send_go_to_receive": "受信へ移動",
|
||||||
|
"send_keep": "保持",
|
||||||
|
"send_network_fee": "ネットワーク手数料",
|
||||||
|
"send_no_balance": "残高なし",
|
||||||
|
"send_no_recent": "最近の送信なし",
|
||||||
|
"send_recent_sends": "最近の送信",
|
||||||
|
"send_recipient": "受取人",
|
||||||
|
"send_select_source": "送信元アドレスを選択...",
|
||||||
|
"send_sending_from": "送信元",
|
||||||
|
"send_submitting": "取引を送信中...",
|
||||||
|
"send_switch_to_receive": "受信に切り替えてアドレスを取得し、資金の受け取りを開始してください。",
|
||||||
|
"send_to": "送金先",
|
||||||
|
"send_tooltip_enter_amount": "送金額を入力してください",
|
||||||
|
"send_tooltip_exceeds_balance": "金額が利用可能残高を超えています",
|
||||||
|
"send_tooltip_in_progress": "取引は既に進行中です",
|
||||||
|
"send_tooltip_invalid_address": "有効な受取人アドレスを入力してください",
|
||||||
|
"send_tooltip_not_connected": "デーモンに未接続",
|
||||||
|
"send_tooltip_select_source": "まず送信元アドレスを選択してください",
|
||||||
|
"send_tooltip_syncing": "ブロックチェーンの同期をお待ちください",
|
||||||
|
"send_total": "合計",
|
||||||
|
"send_transaction": "取引を送信",
|
||||||
|
"send_tx_failed": "取引に失敗しました",
|
||||||
|
"send_tx_sent": "取引を送信しました!",
|
||||||
|
"send_tx_success": "取引の送信に成功しました!",
|
||||||
|
"send_txid_copied": "TxIDをクリップボードにコピーしました",
|
||||||
|
"send_txid_label": "TxID:%s",
|
||||||
|
"send_valid_shielded": "有効なシールドアドレス",
|
||||||
|
"send_valid_transparent": "有効な透明アドレス",
|
||||||
|
"send_wallet_empty": "ウォレットは空です",
|
||||||
|
"send_yes_clear": "はい、クリア",
|
||||||
|
"sending": "取引を送信中",
|
||||||
|
"sending_from": "送信元",
|
||||||
|
"sent": "送信済み",
|
||||||
|
"sent_filter": "送信済み",
|
||||||
|
"sent_type": "送信済み",
|
||||||
|
"sent_upper": "送信済み",
|
||||||
|
"settings": "設定",
|
||||||
|
"setup_wizard": "セットアップウィザード",
|
||||||
|
"share": "共有",
|
||||||
|
"shield_check_status": "ステータスを確認",
|
||||||
|
"shield_completed": "操作が正常に完了しました!",
|
||||||
|
"shield_description": "透明アドレスのcoinbase出力をシールドアドレスに送信して、マイニング報酬をシールドします。マイニング収入を隠すことでプライバシーが向上します。",
|
||||||
|
"shield_from_address": "送信元アドレス:",
|
||||||
|
"shield_funds": "資金をシールド",
|
||||||
|
"shield_in_progress": "操作進行中...",
|
||||||
|
"shield_max_utxos": "1回の操作あたりの最大UTXO数",
|
||||||
|
"shield_merge_done": "シールド/統合が完了しました!",
|
||||||
|
"shield_select_z": "zアドレスを選択...",
|
||||||
|
"shield_started": "シールド操作を開始しました",
|
||||||
|
"shield_title": "Coinbase報酬をシールド",
|
||||||
|
"shield_to_address": "送信先アドレス(シールド):",
|
||||||
|
"shield_utxo_limit": "UTXO制限:",
|
||||||
|
"shield_wildcard_hint": "'*' を使用してすべての透明アドレスからシールド",
|
||||||
|
"shielded": "シールド",
|
||||||
|
"shielded_to": "シールド先",
|
||||||
|
"shielded_type": "シールド",
|
||||||
|
"show": "表示",
|
||||||
|
"show_qr_code": "QRコードを表示",
|
||||||
|
"showing_transactions": "%d\xe2\x80\x93%d / %d 件の取引を表示中(合計:%zu)",
|
||||||
|
"simple_background": "シンプル背景",
|
||||||
|
"start_mining": "マイニング開始",
|
||||||
|
"status": "ステータス",
|
||||||
|
"stop_external": "外部デーモンを停止",
|
||||||
|
"stop_mining": "マイニング停止",
|
||||||
|
"submitting_transaction": "取引を送信中...",
|
||||||
|
"success": "成功",
|
||||||
|
"summary": "概要",
|
||||||
|
"syncing": "同期中...",
|
||||||
|
"t_addresses": "Tアドレス",
|
||||||
|
"test_connection": "テスト",
|
||||||
|
"theme": "テーマ",
|
||||||
|
"theme_effects": "テーマ効果",
|
||||||
|
"time_days_ago": "%d日前",
|
||||||
|
"time_hours_ago": "%d時間前",
|
||||||
|
"time_minutes_ago": "%d分前",
|
||||||
|
"time_seconds_ago": "%d秒前",
|
||||||
|
"to": "宛先",
|
||||||
|
"to_upper": "宛先",
|
||||||
|
"tools": "ツール",
|
||||||
|
"total": "合計",
|
||||||
|
"transaction_id": "取引ID",
|
||||||
|
"transaction_sent": "取引の送信に成功しました",
|
||||||
|
"transaction_sent_msg": "取引を送信しました!",
|
||||||
|
"transaction_url": "取引URL",
|
||||||
|
"transactions": "取引",
|
||||||
|
"transactions_upper": "取引",
|
||||||
|
"transparent": "透明",
|
||||||
|
"tx_confirmations": "%d 確認",
|
||||||
|
"tx_details_title": "取引の詳細",
|
||||||
|
"tx_from_address": "送信元アドレス:",
|
||||||
|
"tx_id_label": "取引ID:",
|
||||||
|
"tx_immature": "未成熟",
|
||||||
|
"tx_mined": "採掘済み",
|
||||||
|
"tx_received": "受信済み",
|
||||||
|
"tx_sent": "送信済み",
|
||||||
|
"tx_to_address": "送信先アドレス:",
|
||||||
|
"tx_view_explorer": "エクスプローラーで表示",
|
||||||
|
"txs_count": "%d 件",
|
||||||
|
"type": "タイプ",
|
||||||
|
"ui_opacity": "UI透明度",
|
||||||
|
"unban": "ブロック解除",
|
||||||
|
"unconfirmed": "未確認",
|
||||||
|
"undo_clear": "クリアを元に戻す",
|
||||||
|
"unknown": "不明",
|
||||||
|
"use_embedded_daemon": "内蔵dragonxdを使用",
|
||||||
|
"use_tor": "Torを使用",
|
||||||
|
"validate_btn": "検証",
|
||||||
|
"validate_description": "DragonXアドレスを入力して、有効かどうか、そしてこのウォレットに属しているかどうかを確認します。",
|
||||||
|
"validate_invalid": "無効",
|
||||||
|
"validate_is_mine": "このウォレットがこのアドレスを所有しています",
|
||||||
|
"validate_not_mine": "このウォレットに属していません",
|
||||||
|
"validate_ownership": "所有者:",
|
||||||
|
"validate_results": "結果:",
|
||||||
|
"validate_shielded_type": "シールド(zアドレス)",
|
||||||
|
"validate_status": "ステータス:",
|
||||||
|
"validate_title": "アドレスを検証",
|
||||||
|
"validate_transparent_type": "透明(tアドレス)",
|
||||||
|
"validate_type": "タイプ:",
|
||||||
|
"validate_valid": "有効",
|
||||||
|
"validating": "検証中...",
|
||||||
|
"verbose_logging": "詳細ログ",
|
||||||
|
"version": "バージョン",
|
||||||
|
"view": "表示",
|
||||||
|
"view_details": "詳細を表示",
|
||||||
|
"view_on_explorer": "エクスプローラーで表示",
|
||||||
|
"waiting_for_daemon": "デーモン接続を待機中...",
|
||||||
|
"wallet": "ウォレット",
|
||||||
|
"wallet_empty": "ウォレットは空です",
|
||||||
|
"wallet_empty_hint": "受信に切り替えてアドレスを取得し、資金の受け取りを開始してください。",
|
||||||
|
"warning": "警告",
|
||||||
|
"warning_upper": "警告!",
|
||||||
|
"website": "ウェブサイト",
|
||||||
|
"window_opacity": "ウィンドウ透明度",
|
||||||
|
"yes_clear": "はい、クリア",
|
||||||
|
"your_addresses": "あなたのアドレス",
|
||||||
|
"z_addresses": "Zアドレス",
|
||||||
|
}
|
||||||
|
|
||||||
|
out = os.path.join(os.path.dirname(__file__), "..", "res", "lang", "ja.json")
|
||||||
|
with open(out, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(translations, f, indent=4, ensure_ascii=False, sort_keys=True)
|
||||||
|
print(f"Wrote {len(translations)} Japanese translations to {os.path.abspath(out)}")
|
||||||
646
scripts/gen_ko.py
Normal file
@@ -0,0 +1,646 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Generate Korean (ko) translations for ObsidianDragon wallet."""
|
||||||
|
import json, os
|
||||||
|
|
||||||
|
translations = {
|
||||||
|
"24h_change": "24시간 변동",
|
||||||
|
"24h_volume": "24시간 거래량",
|
||||||
|
"about": "정보",
|
||||||
|
"about_block_explorer": "블록 탐색기",
|
||||||
|
"about_block_height": "블록 높이:",
|
||||||
|
"about_build_date": "빌드 날짜:",
|
||||||
|
"about_build_type": "빌드 유형:",
|
||||||
|
"about_chain": "체인:",
|
||||||
|
"about_connections": "연결:",
|
||||||
|
"about_credits": "크레딧",
|
||||||
|
"about_daemon": "데몬:",
|
||||||
|
"about_debug": "디버그",
|
||||||
|
"about_dragonx": "ObsidianDragon 정보",
|
||||||
|
"about_edition": "ImGui 에디션",
|
||||||
|
"about_github": "GitHub",
|
||||||
|
"about_imgui": "ImGui:",
|
||||||
|
"about_license": "라이선스",
|
||||||
|
"about_license_text": "본 소프트웨어는 GNU General Public License v3 (GPLv3) 하에 배포됩니다. 라이선스 조건에 따라 자유롭게 사용, 수정 및 배포할 수 있습니다.",
|
||||||
|
"about_peers_count": "%zu 피어",
|
||||||
|
"about_release": "릴리스",
|
||||||
|
"about_title": "ObsidianDragon 정보",
|
||||||
|
"about_version": "버전:",
|
||||||
|
"about_website": "웹사이트",
|
||||||
|
"acrylic": "아크릴",
|
||||||
|
"add": "추가",
|
||||||
|
"address": "주소",
|
||||||
|
"address_book_add": "주소 추가",
|
||||||
|
"address_book_add_new": "새로 추가",
|
||||||
|
"address_book_added": "주소록에 주소를 추가했습니다",
|
||||||
|
"address_book_count": "저장된 주소 %zu개",
|
||||||
|
"address_book_deleted": "항목이 삭제되었습니다",
|
||||||
|
"address_book_edit": "주소 편집",
|
||||||
|
"address_book_empty": "저장된 주소가 없습니다. '새로 추가'를 클릭하여 추가하세요.",
|
||||||
|
"address_book_exists": "주소가 이미 주소록에 있습니다",
|
||||||
|
"address_book_title": "주소록",
|
||||||
|
"address_book_update_failed": "업데이트 실패 — 주소가 중복될 수 있습니다",
|
||||||
|
"address_book_updated": "주소가 업데이트되었습니다",
|
||||||
|
"address_copied": "주소가 클립보드에 복사되었습니다",
|
||||||
|
"address_details": "주소 상세",
|
||||||
|
"address_label": "주소:",
|
||||||
|
"address_upper": "주소",
|
||||||
|
"address_url": "주소 URL",
|
||||||
|
"addresses_appear_here": "연결 후 수신 주소가 여기에 표시됩니다.",
|
||||||
|
"advanced": "고급 설정",
|
||||||
|
"all_filter": "전체",
|
||||||
|
"allow_custom_fees": "사용자 정의 수수료 허용",
|
||||||
|
"amount": "금액",
|
||||||
|
"amount_details": "금액 상세",
|
||||||
|
"amount_exceeds_balance": "금액이 잔액을 초과합니다",
|
||||||
|
"amount_label": "금액:",
|
||||||
|
"appearance": "외관",
|
||||||
|
"auto_shield": "채굴 자동 차폐",
|
||||||
|
"available": "사용 가능",
|
||||||
|
"backup_backing_up": "백업 중...",
|
||||||
|
"backup_create": "백업 생성",
|
||||||
|
"backup_created": "지갑 백업이 생성되었습니다",
|
||||||
|
"backup_data": "백업 및 데이터",
|
||||||
|
"backup_description": "wallet.dat 파일의 백업을 생성합니다. 이 파일에는 모든 개인 키와 거래 내역이 포함되어 있습니다. 백업을 안전한 곳에 보관하세요.",
|
||||||
|
"backup_destination": "백업 위치:",
|
||||||
|
"backup_tip_external": "외장 드라이브 또는 클라우드 스토리지에 백업 저장",
|
||||||
|
"backup_tip_multiple": "서로 다른 위치에 여러 백업 생성",
|
||||||
|
"backup_tip_test": "정기적으로 백업 복원 테스트",
|
||||||
|
"backup_tips": "팁:",
|
||||||
|
"backup_title": "지갑 백업",
|
||||||
|
"backup_wallet": "지갑 백업...",
|
||||||
|
"backup_wallet_not_found": "경고: 예상 위치에서 wallet.dat를 찾을 수 없습니다",
|
||||||
|
"balance": "잔액",
|
||||||
|
"balance_layout": "잔액 레이아웃",
|
||||||
|
"ban": "차단",
|
||||||
|
"banned_peers": "차단된 피어",
|
||||||
|
"block": "블록",
|
||||||
|
"block_bits": "비트:",
|
||||||
|
"block_click_next": "클릭하여 다음 블록 보기",
|
||||||
|
"block_click_prev": "클릭하여 이전 블록 보기",
|
||||||
|
"block_explorer": "블록 탐색기",
|
||||||
|
"block_get_info": "블록 정보 조회",
|
||||||
|
"block_hash": "블록 해시:",
|
||||||
|
"block_height": "블록 높이:",
|
||||||
|
"block_info_title": "블록 정보",
|
||||||
|
"block_merkle_root": "머클 루트:",
|
||||||
|
"block_nav_next": "다음 >>",
|
||||||
|
"block_nav_prev": "<< 이전",
|
||||||
|
"block_next": "다음 블록:",
|
||||||
|
"block_previous": "이전 블록:",
|
||||||
|
"block_size": "크기:",
|
||||||
|
"block_timestamp": "타임스탬프:",
|
||||||
|
"block_transactions": "트랜잭션:",
|
||||||
|
"blockchain_syncing": "블록체인 동기화 중 (%.1f%%)... 잔액이 정확하지 않을 수 있습니다.",
|
||||||
|
"cancel": "취소",
|
||||||
|
"characters": "문자",
|
||||||
|
"clear": "지우기",
|
||||||
|
"clear_all_bans": "모든 차단 해제",
|
||||||
|
"clear_form_confirm": "모든 양식 필드를 지우시겠습니까?",
|
||||||
|
"clear_request": "요청 지우기",
|
||||||
|
"click_copy_address": "클릭하여 주소 복사",
|
||||||
|
"click_copy_uri": "클릭하여 URI 복사",
|
||||||
|
"close": "닫기",
|
||||||
|
"conf_count": "%d 확인",
|
||||||
|
"confirm_and_send": "확인 후 전송",
|
||||||
|
"confirm_send": "전송 확인",
|
||||||
|
"confirm_transaction": "거래 확인",
|
||||||
|
"confirmations": "확인 수",
|
||||||
|
"confirmations_display": "%d 확인 | %s",
|
||||||
|
"confirmed": "확인됨",
|
||||||
|
"connected": "연결됨",
|
||||||
|
"connected_peers": "연결된 피어",
|
||||||
|
"connecting": "연결 중...",
|
||||||
|
"console": "콘솔",
|
||||||
|
"console_auto_scroll": "자동 스크롤",
|
||||||
|
"console_available_commands": "사용 가능한 명령어:",
|
||||||
|
"console_capturing_output": "데몬 출력 캡처 중...",
|
||||||
|
"console_clear": "지우기",
|
||||||
|
"console_clear_console": "콘솔 지우기",
|
||||||
|
"console_cleared": "콘솔이 지워졌습니다",
|
||||||
|
"console_click_commands": "위의 명령어를 클릭하여 삽입",
|
||||||
|
"console_click_insert": "클릭하여 삽입",
|
||||||
|
"console_click_insert_params": "클릭하여 매개변수와 함께 삽입",
|
||||||
|
"console_close": "닫기",
|
||||||
|
"console_commands": "명령어",
|
||||||
|
"console_common_rpc": "일반 RPC 명령어:",
|
||||||
|
"console_completions": "자동 완성:",
|
||||||
|
"console_connected": "데몬에 연결됨",
|
||||||
|
"console_copy_all": "모두 복사",
|
||||||
|
"console_copy_selected": "복사",
|
||||||
|
"console_daemon": "데몬",
|
||||||
|
"console_daemon_error": "데몬 오류!",
|
||||||
|
"console_daemon_started": "데몬이 시작되었습니다",
|
||||||
|
"console_daemon_stopped": "데몬이 중지되었습니다",
|
||||||
|
"console_disconnected": "데몬 연결이 끊어졌습니다",
|
||||||
|
"console_errors": "오류",
|
||||||
|
"console_filter_hint": "출력 필터...",
|
||||||
|
"console_help_clear": " clear - 콘솔 지우기",
|
||||||
|
"console_help_getbalance": " getbalance - 투명 잔액 표시",
|
||||||
|
"console_help_getblockcount": " getblockcount - 현재 블록 높이 표시",
|
||||||
|
"console_help_getinfo": " getinfo - 노드 정보 표시",
|
||||||
|
"console_help_getmininginfo": " getmininginfo - 채굴 상태 표시",
|
||||||
|
"console_help_getpeerinfo": " getpeerinfo - 연결된 피어 표시",
|
||||||
|
"console_help_gettotalbalance": " gettotalbalance - 총 잔액 표시",
|
||||||
|
"console_help_help": " help - 도움말 표시",
|
||||||
|
"console_help_setgenerate": " setgenerate - 채굴 제어",
|
||||||
|
"console_help_stop": " stop - 데몬 중지",
|
||||||
|
"console_line_count": "%zu줄",
|
||||||
|
"console_new_lines": "%d 새 줄",
|
||||||
|
"console_no_daemon": "데몬 없음",
|
||||||
|
"console_not_connected": "오류: 데몬에 연결되지 않았습니다",
|
||||||
|
"console_rpc_reference": "RPC 명령어 참조",
|
||||||
|
"console_scanline": "콘솔 스캔라인",
|
||||||
|
"console_search_commands": "명령어 검색...",
|
||||||
|
"console_select_all": "모두 선택",
|
||||||
|
"console_show_daemon_output": "데몬 출력 표시",
|
||||||
|
"console_show_errors_only": "오류만 표시",
|
||||||
|
"console_show_rpc_ref": "RPC 명령어 참조 표시",
|
||||||
|
"console_showing_lines": "%zu / %zu줄 표시 중",
|
||||||
|
"console_starting_node": "노드 시작 중...",
|
||||||
|
"console_status_error": "오류",
|
||||||
|
"console_status_running": "실행 중",
|
||||||
|
"console_status_starting": "시작 중",
|
||||||
|
"console_status_stopped": "중지됨",
|
||||||
|
"console_status_stopping": "중지 중",
|
||||||
|
"console_status_unknown": "알 수 없음",
|
||||||
|
"console_tab_completion": "Tab으로 자동 완성",
|
||||||
|
"console_type_help": "'help'를 입력하여 사용 가능한 명령어 보기",
|
||||||
|
"console_welcome": "ObsidianDragon 콘솔에 오신 것을 환영합니다",
|
||||||
|
"console_zoom_in": "확대",
|
||||||
|
"console_zoom_out": "축소",
|
||||||
|
"copy": "복사",
|
||||||
|
"copy_address": "전체 주소 복사",
|
||||||
|
"copy_error": "오류 복사",
|
||||||
|
"copy_to_clipboard": "클립보드에 복사",
|
||||||
|
"copy_txid": "TxID 복사",
|
||||||
|
"copy_uri": "URI 복사",
|
||||||
|
"current_price": "현재 가격",
|
||||||
|
"custom_fees": "사용자 정의 수수료",
|
||||||
|
"dark": "다크",
|
||||||
|
"date": "날짜",
|
||||||
|
"date_label": "날짜:",
|
||||||
|
"delete": "삭제",
|
||||||
|
"difficulty": "난이도",
|
||||||
|
"disconnected": "연결 끊김",
|
||||||
|
"dismiss": "닫기",
|
||||||
|
"display": "디스플레이",
|
||||||
|
"dragonx_green": "DragonX(그린)",
|
||||||
|
"edit": "편집",
|
||||||
|
"error": "오류",
|
||||||
|
"est_time_to_block": "예상 블록 시간",
|
||||||
|
"exit": "종료",
|
||||||
|
"explorer": "탐색기",
|
||||||
|
"export": "내보내기",
|
||||||
|
"export_csv": "CSV 내보내기",
|
||||||
|
"export_keys_btn": "키 내보내기",
|
||||||
|
"export_keys_danger": "위험: 지갑의 모든 개인 키가 내보내집니다! 이 파일에 접근할 수 있는 사람은 누구나 자금을 훔칠 수 있습니다. 안전하게 보관하고 사용 후 삭제하세요.",
|
||||||
|
"export_keys_include_t": "T 주소 포함 (투명)",
|
||||||
|
"export_keys_include_z": "Z 주소 포함 (차폐)",
|
||||||
|
"export_keys_options": "내보내기 옵션:",
|
||||||
|
"export_keys_success": "키 내보내기 성공",
|
||||||
|
"export_keys_title": "모든 개인 키 내보내기",
|
||||||
|
"export_private_key": "개인 키 내보내기",
|
||||||
|
"export_tx_count": "%zu건의 거래를 CSV 파일로 내보냈습니다.",
|
||||||
|
"export_tx_file_fail": "CSV 파일 생성 실패",
|
||||||
|
"export_tx_none": "내보낼 거래가 없습니다",
|
||||||
|
"export_tx_success": "거래 내보내기 성공",
|
||||||
|
"export_tx_title": "거래를 CSV로 내보내기",
|
||||||
|
"export_viewing_key": "조회 키 내보내기",
|
||||||
|
"failed_create_shielded": "차폐 주소 생성 실패",
|
||||||
|
"failed_create_transparent": "투명 주소 생성 실패",
|
||||||
|
"fee": "수수료",
|
||||||
|
"fee_high": "높음",
|
||||||
|
"fee_label": "수수료:",
|
||||||
|
"fee_low": "낮음",
|
||||||
|
"fee_normal": "보통",
|
||||||
|
"fetch_prices": "가격 조회",
|
||||||
|
"file": "파일",
|
||||||
|
"file_save_location": "파일 저장 위치: ~/.config/ObsidianDragon/",
|
||||||
|
"font_scale": "글꼴 크기",
|
||||||
|
"from": "보낸 곳",
|
||||||
|
"from_upper": "보낸 곳",
|
||||||
|
"full_details": "전체 세부 정보",
|
||||||
|
"general": "일반",
|
||||||
|
"go_to_receive": "수신으로 이동",
|
||||||
|
"height": "높이",
|
||||||
|
"help": "도움말",
|
||||||
|
"hide": "숨기기",
|
||||||
|
"history": "내역",
|
||||||
|
"immature_type": "미성숙",
|
||||||
|
"import": "가져오기",
|
||||||
|
"import_key_btn": "키 가져오기",
|
||||||
|
"import_key_formats": "지원되는 키 형식:",
|
||||||
|
"import_key_full_rescan": "(0 = 전체 재스캔)",
|
||||||
|
"import_key_label": "개인 키:",
|
||||||
|
"import_key_no_valid": "입력에서 유효한 키를 찾을 수 없습니다",
|
||||||
|
"import_key_rescan": "가져오기 후 블록체인 재스캔",
|
||||||
|
"import_key_start_height": "시작 높이:",
|
||||||
|
"import_key_success": "키 가져오기 성공",
|
||||||
|
"import_key_t_format": "T 주소 WIF 개인 키",
|
||||||
|
"import_key_title": "개인 키 가져오기",
|
||||||
|
"import_key_tooltip": "한 줄에 하나의 개인 키를 입력하세요.\nz 주소와 t 주소 키 모두 지원됩니다.\n#으로 시작하는 줄은 주석으로 처리됩니다.",
|
||||||
|
"import_key_warning": "경고: 개인 키를 절대 공유하지 마세요! 신뢰할 수 없는 소스의 키를 가져오면 지갑이 위험해질 수 있습니다.",
|
||||||
|
"import_key_z_format": "Z 주소 지출 키 (secret-extended-key-...)",
|
||||||
|
"import_private_key": "개인 키 가져오기...",
|
||||||
|
"invalid_address": "잘못된 주소 형식",
|
||||||
|
"ip_address": "IP 주소",
|
||||||
|
"keep": "유지",
|
||||||
|
"keep_daemon": "데몬 계속 실행",
|
||||||
|
"key_export_fetching": "지갑에서 키를 가져오는 중...",
|
||||||
|
"key_export_private_key": "개인 키:",
|
||||||
|
"key_export_private_warning": "이 키를 비밀로 유지하세요! 이 키를 가진 사람은 누구나 자금을 사용할 수 있습니다. 온라인이나 신뢰할 수 없는 사람과 공유하지 마세요.",
|
||||||
|
"key_export_reveal": "키 표시",
|
||||||
|
"key_export_viewing_key": "조회 키:",
|
||||||
|
"key_export_viewing_warning": "이 조회 키를 사용하면 다른 사람이 수신 거래와 잔액을 볼 수 있지만 자금을 사용할 수는 없습니다. 신뢰할 수 있는 사람에게만 공유하세요.",
|
||||||
|
"label": "라벨:",
|
||||||
|
"language": "언어",
|
||||||
|
"light": "라이트",
|
||||||
|
"loading": "로딩 중...",
|
||||||
|
"loading_addresses": "주소 로딩 중...",
|
||||||
|
"local_hashrate": "로컬 해시레이트",
|
||||||
|
"low_spec_mode": "저사양 모드",
|
||||||
|
"market": "시장",
|
||||||
|
"market_12h": "12시간",
|
||||||
|
"market_18h": "18시간",
|
||||||
|
"market_24h": "24시간",
|
||||||
|
"market_24h_volume": "24시간 거래량",
|
||||||
|
"market_6h": "6시간",
|
||||||
|
"market_attribution": "가격 데이터: NonKYC 제공",
|
||||||
|
"market_btc_price": "BTC 가격",
|
||||||
|
"market_cap": "시가총액",
|
||||||
|
"market_no_history": "가격 내역 없음",
|
||||||
|
"market_no_price": "가격 데이터 없음",
|
||||||
|
"market_now": "현재",
|
||||||
|
"market_pct_shielded": "%.0f%% 차폐됨",
|
||||||
|
"market_portfolio": "포트폴리오",
|
||||||
|
"market_price_unavailable": "가격 데이터를 사용할 수 없습니다",
|
||||||
|
"market_refresh_price": "가격 데이터 새로고침",
|
||||||
|
"market_trade_on": "%s에서 거래",
|
||||||
|
"mature": "성숙됨",
|
||||||
|
"max": "최대",
|
||||||
|
"memo": "메모 (선택, 암호화)",
|
||||||
|
"memo_label": "메모:",
|
||||||
|
"memo_optional": "메모 (선택)",
|
||||||
|
"memo_upper": "메모",
|
||||||
|
"memo_z_only": "참고: 메모는 차폐 (z) 주소로 전송할 때만 사용할 수 있습니다",
|
||||||
|
"merge_description": "여러 UTXO를 단일 차폐 주소로 통합합니다. 지갑 크기를 줄이고 프라이버시를 향상시킵니다.",
|
||||||
|
"merge_funds": "자금 통합",
|
||||||
|
"merge_started": "통합 작업이 시작되었습니다",
|
||||||
|
"merge_title": "주소로 통합",
|
||||||
|
"mine_when_idle": "유휴 시 채굴",
|
||||||
|
"mined": "채굴됨",
|
||||||
|
"mined_filter": "채굴됨",
|
||||||
|
"mined_type": "채굴됨",
|
||||||
|
"mined_upper": "채굴됨",
|
||||||
|
"miner_fee": "채굴 수수료",
|
||||||
|
"mining": "채굴",
|
||||||
|
"mining_active": "활성",
|
||||||
|
"mining_address_copied": "채굴 주소가 복사되었습니다",
|
||||||
|
"mining_all_time": "전체 기간",
|
||||||
|
"mining_already_saved": "풀 URL이 이미 저장되어 있습니다",
|
||||||
|
"mining_block_copied": "블록 해시가 복사되었습니다",
|
||||||
|
"mining_chart_1m_ago": "1분 전",
|
||||||
|
"mining_chart_5m_ago": "5분 전",
|
||||||
|
"mining_chart_now": "현재",
|
||||||
|
"mining_chart_start": "시작",
|
||||||
|
"mining_click": "클릭",
|
||||||
|
"mining_click_copy_address": "클릭하여 주소 복사",
|
||||||
|
"mining_click_copy_block": "클릭하여 블록 해시 복사",
|
||||||
|
"mining_click_copy_difficulty": "클릭하여 난이도 복사",
|
||||||
|
"mining_connected": "연결됨",
|
||||||
|
"mining_connecting": "연결 중...",
|
||||||
|
"mining_control": "채굴 제어",
|
||||||
|
"mining_difficulty_copied": "난이도가 복사되었습니다",
|
||||||
|
"mining_est_block": "예상 블록",
|
||||||
|
"mining_est_daily": "예상 일일 수익",
|
||||||
|
"mining_filter_all": "전체",
|
||||||
|
"mining_filter_tip_all": "모든 수익 표시",
|
||||||
|
"mining_filter_tip_pool": "풀 수익만 표시",
|
||||||
|
"mining_filter_tip_solo": "솔로 수익만 표시",
|
||||||
|
"mining_idle_off_tooltip": "유휴 채굴 활성화",
|
||||||
|
"mining_idle_on_tooltip": "유휴 채굴 비활성화",
|
||||||
|
"mining_local_hashrate": "로컬 해시레이트",
|
||||||
|
"mining_mine": "채굴",
|
||||||
|
"mining_mining_addr": "채굴 주소",
|
||||||
|
"mining_network": "네트워크",
|
||||||
|
"mining_no_blocks_yet": "아직 블록을 찾지 못했습니다",
|
||||||
|
"mining_no_payouts_yet": "아직 풀 지급이 없습니다",
|
||||||
|
"mining_no_saved_addresses": "저장된 주소 없음",
|
||||||
|
"mining_no_saved_pools": "저장된 풀 없음",
|
||||||
|
"mining_off": "채굴이 꺼져 있습니다",
|
||||||
|
"mining_on": "채굴이 켜져 있습니다",
|
||||||
|
"mining_open_in_explorer": "탐색기에서 열기",
|
||||||
|
"mining_payout_address": "지급 주소",
|
||||||
|
"mining_payout_tooltip": "채굴 보상 수신 주소",
|
||||||
|
"mining_pool": "풀",
|
||||||
|
"mining_pool_hashrate": "풀 해시레이트",
|
||||||
|
"mining_pool_url": "풀 URL",
|
||||||
|
"mining_recent_blocks": "최근 블록",
|
||||||
|
"mining_recent_payouts": "최근 풀 지급",
|
||||||
|
"mining_remove": "제거",
|
||||||
|
"mining_reset_defaults": "기본값으로 재설정",
|
||||||
|
"mining_save_payout_address": "지급 주소 저장",
|
||||||
|
"mining_save_pool_url": "풀 URL 저장",
|
||||||
|
"mining_saved_addresses": "저장된 주소:",
|
||||||
|
"mining_saved_pools": "저장된 풀:",
|
||||||
|
"mining_shares": "셰어",
|
||||||
|
"mining_show_chart": "차트",
|
||||||
|
"mining_show_log": "로그",
|
||||||
|
"mining_solo": "솔로",
|
||||||
|
"mining_starting": "시작 중...",
|
||||||
|
"mining_starting_tooltip": "채굴기 시작 중...",
|
||||||
|
"mining_statistics": "채굴 통계",
|
||||||
|
"mining_stop": "중지",
|
||||||
|
"mining_stop_solo_for_pool": "풀 채굴을 시작하려면 솔로 채굴을 먼저 중지하세요",
|
||||||
|
"mining_stop_solo_for_pool_settings": "풀 설정을 변경하려면 솔로 채굴을 중지하세요",
|
||||||
|
"mining_stopping": "중지 중...",
|
||||||
|
"mining_stopping_tooltip": "채굴기 중지 중...",
|
||||||
|
"mining_syncing_tooltip": "블록체인 동기화 중...",
|
||||||
|
"mining_threads": "채굴 스레드",
|
||||||
|
"mining_to_save": "저장하려면",
|
||||||
|
"mining_today": "오늘",
|
||||||
|
"mining_uptime": "가동 시간",
|
||||||
|
"mining_yesterday": "어제",
|
||||||
|
"network": "네트워크",
|
||||||
|
"network_fee": "네트워크 수수료",
|
||||||
|
"network_hashrate": "네트워크 해시레이트",
|
||||||
|
"new": "+ 새로 만들기",
|
||||||
|
"new_shielded_created": "새 차폐 주소가 생성되었습니다",
|
||||||
|
"new_t_address": "새 T 주소",
|
||||||
|
"new_t_transparent": "새 t 주소 (투명)",
|
||||||
|
"new_transparent_created": "새 투명 주소가 생성되었습니다",
|
||||||
|
"new_z_address": "새 Z 주소",
|
||||||
|
"new_z_shielded": "새 z 주소 (차폐)",
|
||||||
|
"no_addresses": "주소가 없습니다. 위의 버튼을 사용하여 생성하세요.",
|
||||||
|
"no_addresses_available": "사용 가능한 주소 없음",
|
||||||
|
"no_addresses_match": "필터와 일치하는 주소가 없습니다",
|
||||||
|
"no_addresses_with_balance": "잔액이 있는 주소가 없습니다",
|
||||||
|
"no_matching": "일치하는 거래가 없습니다",
|
||||||
|
"no_recent_receives": "최근 수신 내역 없음",
|
||||||
|
"no_recent_sends": "최근 전송 내역 없음",
|
||||||
|
"no_transactions": "거래 내역이 없습니다",
|
||||||
|
"node": "노드",
|
||||||
|
"node_security": "노드 및 보안",
|
||||||
|
"noise": "노이즈",
|
||||||
|
"not_connected": "데몬에 연결되지 않음...",
|
||||||
|
"not_connected_to_daemon": "데몬에 연결되지 않음",
|
||||||
|
"notes": "메모",
|
||||||
|
"notes_optional": "메모 (선택):",
|
||||||
|
"output_filename": "출력 파일명:",
|
||||||
|
"overview": "개요",
|
||||||
|
"paste": "붙여넣기",
|
||||||
|
"paste_from_clipboard": "클립보드에서 붙여넣기",
|
||||||
|
"pay_from": "보낼 곳",
|
||||||
|
"payment_request": "결제 요청",
|
||||||
|
"payment_request_copied": "결제 요청이 복사되었습니다",
|
||||||
|
"payment_uri_copied": "결제 URI가 복사되었습니다",
|
||||||
|
"peers": "피어",
|
||||||
|
"peers_avg_ping": "평균 Ping",
|
||||||
|
"peers_ban_24h": "피어 24시간 차단",
|
||||||
|
"peers_ban_score": "차단 점수: %d",
|
||||||
|
"peers_banned": "차단됨",
|
||||||
|
"peers_banned_count": "차단됨: %d",
|
||||||
|
"peers_best_block": "최고 블록",
|
||||||
|
"peers_blockchain": "블록체인",
|
||||||
|
"peers_blocks": "블록",
|
||||||
|
"peers_blocks_left": "남은 블록: %d",
|
||||||
|
"peers_clear_all_bans": "모든 차단 해제",
|
||||||
|
"peers_click_copy": "클릭하여 복사",
|
||||||
|
"peers_connected": "연결됨",
|
||||||
|
"peers_connected_count": "연결됨: %d",
|
||||||
|
"peers_copy_ip": "IP 복사",
|
||||||
|
"peers_dir_in": "수신",
|
||||||
|
"peers_dir_out": "송신",
|
||||||
|
"peers_hash_copied": "해시가 복사되었습니다",
|
||||||
|
"peers_hashrate": "해시레이트",
|
||||||
|
"peers_in_out": "수신/송신",
|
||||||
|
"peers_longest": "최장",
|
||||||
|
"peers_longest_chain": "최장 체인",
|
||||||
|
"peers_memory": "메모리",
|
||||||
|
"peers_no_banned": "차단된 피어 없음",
|
||||||
|
"peers_no_connected": "연결된 피어 없음",
|
||||||
|
"peers_no_tls": "TLS 없음",
|
||||||
|
"peers_notarized": "공증됨",
|
||||||
|
"peers_p2p_port": "P2P 포트",
|
||||||
|
"peers_protocol": "프로토콜",
|
||||||
|
"peers_received": "수신됨",
|
||||||
|
"peers_refresh": "새로고침",
|
||||||
|
"peers_refresh_tooltip": "피어 목록 새로고침",
|
||||||
|
"peers_refreshing": "새로고침 중...",
|
||||||
|
"peers_sent": "전송됨",
|
||||||
|
"peers_tt_id": "ID: %d",
|
||||||
|
"peers_tt_received": "수신: %s",
|
||||||
|
"peers_tt_sent": "전송: %s",
|
||||||
|
"peers_tt_services": "서비스: %s",
|
||||||
|
"peers_tt_start_height": "시작 높이: %d",
|
||||||
|
"peers_tt_synced": "동기화 H/B: %d/%d",
|
||||||
|
"peers_tt_tls_cipher": "TLS: %s",
|
||||||
|
"peers_unban": "차단 해제",
|
||||||
|
"peers_upper": "피어",
|
||||||
|
"peers_version": "버전",
|
||||||
|
"pending": "대기 중",
|
||||||
|
"ping": "Ping",
|
||||||
|
"price_chart": "가격 차트",
|
||||||
|
"qr_code": "QR 코드",
|
||||||
|
"qr_failed": "QR 코드 생성 실패",
|
||||||
|
"qr_title": "QR 코드",
|
||||||
|
"qr_unavailable": "QR 사용 불가",
|
||||||
|
"receive": "수신",
|
||||||
|
"received": "수신됨",
|
||||||
|
"received_filter": "수신됨",
|
||||||
|
"received_label": "수신됨",
|
||||||
|
"received_upper": "수신됨",
|
||||||
|
"receiving_addresses": "수신 주소",
|
||||||
|
"recent_received": "최근 수신",
|
||||||
|
"recent_sends": "최근 전송",
|
||||||
|
"recipient": "수신자",
|
||||||
|
"recv_type": "수신",
|
||||||
|
"refresh": "새로고침",
|
||||||
|
"refresh_now": "지금 새로고침",
|
||||||
|
"report_bug": "버그 신고",
|
||||||
|
"request_amount": "금액 (선택):",
|
||||||
|
"request_copy_uri": "URI 복사",
|
||||||
|
"request_description": "다른 사람이 스캔하거나 복사할 수 있는 결제 요청을 생성합니다. QR 코드에는 주소와 선택적 금액/메모가 포함됩니다.",
|
||||||
|
"request_label": "라벨 (선택):",
|
||||||
|
"request_memo": "메모 (선택):",
|
||||||
|
"request_payment": "결제 요청",
|
||||||
|
"request_payment_uri": "결제 URI:",
|
||||||
|
"request_receive_address": "수신 주소:",
|
||||||
|
"request_select_address": "주소 선택...",
|
||||||
|
"request_shielded_addrs": "-- 차폐 주소 --",
|
||||||
|
"request_title": "결제 요청",
|
||||||
|
"request_transparent_addrs": "-- 투명 주소 --",
|
||||||
|
"request_uri_copied": "결제 URI가 클립보드에 복사되었습니다",
|
||||||
|
"rescan": "재스캔",
|
||||||
|
"reset_to_defaults": "기본값으로 재설정",
|
||||||
|
"review_send": "전송 검토",
|
||||||
|
"rpc_host": "RPC 호스트",
|
||||||
|
"rpc_pass": "비밀번호",
|
||||||
|
"rpc_port": "포트",
|
||||||
|
"rpc_user": "사용자명",
|
||||||
|
"save": "저장",
|
||||||
|
"save_settings": "설정 저장",
|
||||||
|
"save_z_transactions": "Z 거래를 거래 목록에 저장",
|
||||||
|
"search_placeholder": "검색...",
|
||||||
|
"security": "보안",
|
||||||
|
"select_address": "주소 선택...",
|
||||||
|
"select_receiving_address": "수신 주소 선택...",
|
||||||
|
"select_source_address": "보낼 주소 선택...",
|
||||||
|
"send": "전송",
|
||||||
|
"send_amount": "금액",
|
||||||
|
"send_amount_details": "금액 상세",
|
||||||
|
"send_amount_upper": "금액",
|
||||||
|
"send_clear_fields": "모든 양식 필드를 지우시겠습니까?",
|
||||||
|
"send_copy_error": "오류 복사",
|
||||||
|
"send_dismiss": "닫기",
|
||||||
|
"send_error_copied": "오류가 클립보드에 복사되었습니다",
|
||||||
|
"send_error_prefix": "오류: %s",
|
||||||
|
"send_exceeds_available": "사용 가능 금액 초과 (%.8f)",
|
||||||
|
"send_fee": "수수료",
|
||||||
|
"send_fee_high": "높음",
|
||||||
|
"send_fee_low": "낮음",
|
||||||
|
"send_fee_normal": "보통",
|
||||||
|
"send_form_restored": "양식이 복원되었습니다",
|
||||||
|
"send_from_this_address": "이 주소에서 전송",
|
||||||
|
"send_go_to_receive": "수신으로 이동",
|
||||||
|
"send_keep": "유지",
|
||||||
|
"send_network_fee": "네트워크 수수료",
|
||||||
|
"send_no_balance": "잔액 없음",
|
||||||
|
"send_no_recent": "최근 전송 없음",
|
||||||
|
"send_recent_sends": "최근 전송",
|
||||||
|
"send_recipient": "수신자",
|
||||||
|
"send_select_source": "보낼 주소 선택...",
|
||||||
|
"send_sending_from": "보내는 곳",
|
||||||
|
"send_submitting": "거래 제출 중...",
|
||||||
|
"send_switch_to_receive": "수신으로 전환하여 주소를 받고 자금 수신을 시작하세요.",
|
||||||
|
"send_to": "받는 곳",
|
||||||
|
"send_tooltip_enter_amount": "전송할 금액을 입력하세요",
|
||||||
|
"send_tooltip_exceeds_balance": "금액이 사용 가능 잔액을 초과합니다",
|
||||||
|
"send_tooltip_in_progress": "거래가 이미 진행 중입니다",
|
||||||
|
"send_tooltip_invalid_address": "유효한 수신자 주소를 입력하세요",
|
||||||
|
"send_tooltip_not_connected": "데몬에 연결되지 않음",
|
||||||
|
"send_tooltip_select_source": "먼저 보낼 주소를 선택하세요",
|
||||||
|
"send_tooltip_syncing": "블록체인 동기화를 기다려 주세요",
|
||||||
|
"send_total": "합계",
|
||||||
|
"send_transaction": "거래 전송",
|
||||||
|
"send_tx_failed": "거래 실패",
|
||||||
|
"send_tx_sent": "거래가 전송되었습니다!",
|
||||||
|
"send_tx_success": "거래 전송 성공!",
|
||||||
|
"send_txid_copied": "TxID가 클립보드에 복사되었습니다",
|
||||||
|
"send_txid_label": "TxID: %s",
|
||||||
|
"send_valid_shielded": "유효한 차폐 주소",
|
||||||
|
"send_valid_transparent": "유효한 투명 주소",
|
||||||
|
"send_wallet_empty": "지갑이 비어 있습니다",
|
||||||
|
"send_yes_clear": "예, 지우기",
|
||||||
|
"sending": "거래 전송 중",
|
||||||
|
"sending_from": "보내는 곳",
|
||||||
|
"sent": "전송됨",
|
||||||
|
"sent_filter": "전송됨",
|
||||||
|
"sent_type": "전송됨",
|
||||||
|
"sent_upper": "전송됨",
|
||||||
|
"settings": "설정",
|
||||||
|
"setup_wizard": "설정 마법사",
|
||||||
|
"share": "공유",
|
||||||
|
"shield_check_status": "상태 확인",
|
||||||
|
"shield_completed": "작업이 성공적으로 완료되었습니다!",
|
||||||
|
"shield_description": "투명 주소의 코인베이스 출력을 차폐 주소로 전송하여 채굴 보상을 차폐합니다. 채굴 수입을 숨겨 프라이버시가 향상됩니다.",
|
||||||
|
"shield_from_address": "보내는 주소:",
|
||||||
|
"shield_funds": "자금 차폐",
|
||||||
|
"shield_in_progress": "작업 진행 중...",
|
||||||
|
"shield_max_utxos": "작업당 최대 UTXO 수",
|
||||||
|
"shield_merge_done": "차폐/통합이 완료되었습니다!",
|
||||||
|
"shield_select_z": "z 주소 선택...",
|
||||||
|
"shield_started": "차폐 작업이 시작되었습니다",
|
||||||
|
"shield_title": "코인베이스 보상 차폐",
|
||||||
|
"shield_to_address": "받는 주소 (차폐):",
|
||||||
|
"shield_utxo_limit": "UTXO 제한:",
|
||||||
|
"shield_wildcard_hint": "'*'를 사용하여 모든 투명 주소에서 차폐",
|
||||||
|
"shielded": "차폐",
|
||||||
|
"shielded_to": "차폐 대상",
|
||||||
|
"shielded_type": "차폐",
|
||||||
|
"show": "표시",
|
||||||
|
"show_qr_code": "QR 코드 표시",
|
||||||
|
"showing_transactions": "%d\xe2\x80\x93%d / %d건의 거래 표시 중 (총: %zu)",
|
||||||
|
"simple_background": "단순 배경",
|
||||||
|
"start_mining": "채굴 시작",
|
||||||
|
"status": "상태",
|
||||||
|
"stop_external": "외부 데몬 중지",
|
||||||
|
"stop_mining": "채굴 중지",
|
||||||
|
"submitting_transaction": "거래 제출 중...",
|
||||||
|
"success": "성공",
|
||||||
|
"summary": "요약",
|
||||||
|
"syncing": "동기화 중...",
|
||||||
|
"t_addresses": "T 주소",
|
||||||
|
"test_connection": "테스트",
|
||||||
|
"theme": "테마",
|
||||||
|
"theme_effects": "테마 효과",
|
||||||
|
"time_days_ago": "%d일 전",
|
||||||
|
"time_hours_ago": "%d시간 전",
|
||||||
|
"time_minutes_ago": "%d분 전",
|
||||||
|
"time_seconds_ago": "%d초 전",
|
||||||
|
"to": "받는 곳",
|
||||||
|
"to_upper": "받는 곳",
|
||||||
|
"tools": "도구",
|
||||||
|
"total": "합계",
|
||||||
|
"transaction_id": "거래 ID",
|
||||||
|
"transaction_sent": "거래 전송 성공",
|
||||||
|
"transaction_sent_msg": "거래가 전송되었습니다!",
|
||||||
|
"transaction_url": "거래 URL",
|
||||||
|
"transactions": "거래",
|
||||||
|
"transactions_upper": "거래",
|
||||||
|
"transparent": "투명",
|
||||||
|
"tx_confirmations": "%d 확인",
|
||||||
|
"tx_details_title": "거래 상세",
|
||||||
|
"tx_from_address": "보낸 주소:",
|
||||||
|
"tx_id_label": "거래 ID:",
|
||||||
|
"tx_immature": "미성숙",
|
||||||
|
"tx_mined": "채굴됨",
|
||||||
|
"tx_received": "수신됨",
|
||||||
|
"tx_sent": "전송됨",
|
||||||
|
"tx_to_address": "받는 주소:",
|
||||||
|
"tx_view_explorer": "탐색기에서 보기",
|
||||||
|
"txs_count": "%d건",
|
||||||
|
"type": "유형",
|
||||||
|
"ui_opacity": "UI 투명도",
|
||||||
|
"unban": "차단 해제",
|
||||||
|
"unconfirmed": "미확인",
|
||||||
|
"undo_clear": "지우기 취소",
|
||||||
|
"unknown": "알 수 없음",
|
||||||
|
"use_embedded_daemon": "내장 dragonxd 사용",
|
||||||
|
"use_tor": "Tor 사용",
|
||||||
|
"validate_btn": "검증",
|
||||||
|
"validate_description": "DragonX 주소를 입력하여 유효한지 그리고 이 지갑에 속하는지 확인합니다.",
|
||||||
|
"validate_invalid": "유효하지 않음",
|
||||||
|
"validate_is_mine": "이 지갑이 이 주소를 소유합니다",
|
||||||
|
"validate_not_mine": "이 지갑에 속하지 않음",
|
||||||
|
"validate_ownership": "소유자:",
|
||||||
|
"validate_results": "결과:",
|
||||||
|
"validate_shielded_type": "차폐 (z 주소)",
|
||||||
|
"validate_status": "상태:",
|
||||||
|
"validate_title": "주소 검증",
|
||||||
|
"validate_transparent_type": "투명 (t 주소)",
|
||||||
|
"validate_type": "유형:",
|
||||||
|
"validate_valid": "유효함",
|
||||||
|
"validating": "검증 중...",
|
||||||
|
"verbose_logging": "상세 로깅",
|
||||||
|
"version": "버전",
|
||||||
|
"view": "보기",
|
||||||
|
"view_details": "상세 보기",
|
||||||
|
"view_on_explorer": "탐색기에서 보기",
|
||||||
|
"waiting_for_daemon": "데몬 연결 대기 중...",
|
||||||
|
"wallet": "지갑",
|
||||||
|
"wallet_empty": "지갑이 비어 있습니다",
|
||||||
|
"wallet_empty_hint": "수신으로 전환하여 주소를 받고 자금 수신을 시작하세요.",
|
||||||
|
"warning": "경고",
|
||||||
|
"warning_upper": "경고!",
|
||||||
|
"website": "웹사이트",
|
||||||
|
"window_opacity": "창 투명도",
|
||||||
|
"yes_clear": "예, 지우기",
|
||||||
|
"your_addresses": "내 주소",
|
||||||
|
"z_addresses": "Z 주소",
|
||||||
|
}
|
||||||
|
|
||||||
|
out = os.path.join(os.path.dirname(__file__), "..", "res", "lang", "ko.json")
|
||||||
|
with open(out, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(translations, f, indent=4, ensure_ascii=False, sort_keys=True)
|
||||||
|
print(f"Wrote {len(translations)} Korean translations to {os.path.abspath(out)}")
|
||||||
646
scripts/gen_pt.py
Normal file
@@ -0,0 +1,646 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Generate Portuguese (pt) translations for ObsidianDragon wallet."""
|
||||||
|
import json, os
|
||||||
|
|
||||||
|
translations = {
|
||||||
|
"24h_change": "Variação 24h",
|
||||||
|
"24h_volume": "Volume 24h",
|
||||||
|
"about": "Sobre",
|
||||||
|
"about_block_explorer": "Explorador de Blocos",
|
||||||
|
"about_block_height": "Altura do Bloco:",
|
||||||
|
"about_build_date": "Data de Compilação:",
|
||||||
|
"about_build_type": "Tipo de Build:",
|
||||||
|
"about_chain": "Chain:",
|
||||||
|
"about_connections": "Conexões:",
|
||||||
|
"about_credits": "Créditos",
|
||||||
|
"about_daemon": "Daemon:",
|
||||||
|
"about_debug": "Depuração",
|
||||||
|
"about_dragonx": "Sobre o ObsidianDragon",
|
||||||
|
"about_edition": "Edição ImGui",
|
||||||
|
"about_github": "GitHub",
|
||||||
|
"about_imgui": "ImGui:",
|
||||||
|
"about_license": "Licença",
|
||||||
|
"about_license_text": "Este software é disponibilizado sob a Licença Pública Geral GNU v3 (GPLv3). Você é livre para usar, modificar e distribuir este software sob os termos da licença.",
|
||||||
|
"about_peers_count": "%zu pares",
|
||||||
|
"about_release": "Versão",
|
||||||
|
"about_title": "Sobre o ObsidianDragon",
|
||||||
|
"about_version": "Versão:",
|
||||||
|
"about_website": "Website",
|
||||||
|
"acrylic": "Acrílico",
|
||||||
|
"add": "Adicionar",
|
||||||
|
"address": "Endereço",
|
||||||
|
"address_book_add": "Adicionar Endereço",
|
||||||
|
"address_book_add_new": "Adicionar Novo",
|
||||||
|
"address_book_added": "Endereço adicionado ao livro",
|
||||||
|
"address_book_count": "%zu endereços salvos",
|
||||||
|
"address_book_deleted": "Entrada excluída",
|
||||||
|
"address_book_edit": "Editar Endereço",
|
||||||
|
"address_book_empty": "Nenhum endereço salvo. Clique em 'Adicionar Novo' para criar um.",
|
||||||
|
"address_book_exists": "Endereço já existe no livro",
|
||||||
|
"address_book_title": "Livro de Endereços",
|
||||||
|
"address_book_update_failed": "Falha na atualização - endereço pode ser duplicado",
|
||||||
|
"address_book_updated": "Endereço atualizado",
|
||||||
|
"address_copied": "Endereço copiado para a área de transferência",
|
||||||
|
"address_details": "Detalhes do Endereço",
|
||||||
|
"address_label": "Endereço:",
|
||||||
|
"address_upper": "ENDEREÇO",
|
||||||
|
"address_url": "URL do Endereço",
|
||||||
|
"addresses_appear_here": "Seus endereços de recebimento aparecerão aqui após a conexão.",
|
||||||
|
"advanced": "AVANÇADO",
|
||||||
|
"all_filter": "Todos",
|
||||||
|
"allow_custom_fees": "Permitir taxas personalizadas",
|
||||||
|
"amount": "Valor",
|
||||||
|
"amount_details": "DETALHES DO VALOR",
|
||||||
|
"amount_exceeds_balance": "Valor excede o saldo",
|
||||||
|
"amount_label": "Valor:",
|
||||||
|
"appearance": "APARÊNCIA",
|
||||||
|
"auto_shield": "Auto-blindar mineração",
|
||||||
|
"available": "Disponível",
|
||||||
|
"backup_backing_up": "Fazendo backup...",
|
||||||
|
"backup_create": "Criar Backup",
|
||||||
|
"backup_created": "Backup da carteira criado",
|
||||||
|
"backup_data": "BACKUP & DADOS",
|
||||||
|
"backup_description": "Crie um backup do seu arquivo wallet.dat. Este arquivo contém todas as suas chaves privadas e histórico de transações. Guarde o backup em um local seguro.",
|
||||||
|
"backup_destination": "Destino do backup:",
|
||||||
|
"backup_tip_external": "Armazene backups em unidades externas ou armazenamento em nuvem",
|
||||||
|
"backup_tip_multiple": "Crie múltiplos backups em diferentes locais",
|
||||||
|
"backup_tip_test": "Teste a restauração do backup periodicamente",
|
||||||
|
"backup_tips": "Dicas:",
|
||||||
|
"backup_title": "Backup da Carteira",
|
||||||
|
"backup_wallet": "Fazer Backup da Carteira...",
|
||||||
|
"backup_wallet_not_found": "Aviso: wallet.dat não encontrado no local esperado",
|
||||||
|
"balance": "Saldo",
|
||||||
|
"balance_layout": "Layout do Saldo",
|
||||||
|
"ban": "Banir",
|
||||||
|
"banned_peers": "Pares Banidos",
|
||||||
|
"block": "Bloco",
|
||||||
|
"block_bits": "Bits:",
|
||||||
|
"block_click_next": "Clique para ver o próximo bloco",
|
||||||
|
"block_click_prev": "Clique para ver o bloco anterior",
|
||||||
|
"block_explorer": "Explorador de Blocos",
|
||||||
|
"block_get_info": "Obter Info do Bloco",
|
||||||
|
"block_hash": "Hash do Bloco:",
|
||||||
|
"block_height": "Altura do Bloco:",
|
||||||
|
"block_info_title": "Informações do Bloco",
|
||||||
|
"block_merkle_root": "Raiz Merkle:",
|
||||||
|
"block_nav_next": "Próximo >>",
|
||||||
|
"block_nav_prev": "<< Anterior",
|
||||||
|
"block_next": "Próximo Bloco:",
|
||||||
|
"block_previous": "Bloco Anterior:",
|
||||||
|
"block_size": "Tamanho:",
|
||||||
|
"block_timestamp": "Carimbo de Data:",
|
||||||
|
"block_transactions": "Transações:",
|
||||||
|
"blockchain_syncing": "Blockchain sincronizando (%.1f%%)... Os saldos podem ser imprecisos.",
|
||||||
|
"cancel": "Cancelar",
|
||||||
|
"characters": "caracteres",
|
||||||
|
"clear": "Limpar",
|
||||||
|
"clear_all_bans": "Remover Todos os Banimentos",
|
||||||
|
"clear_form_confirm": "Limpar todos os campos do formulário?",
|
||||||
|
"clear_request": "Limpar Solicitação",
|
||||||
|
"click_copy_address": "Clique para copiar o endereço",
|
||||||
|
"click_copy_uri": "Clique para copiar a URI",
|
||||||
|
"close": "Fechar",
|
||||||
|
"conf_count": "%d conf.",
|
||||||
|
"confirm_and_send": "Confirmar & Enviar",
|
||||||
|
"confirm_send": "Confirmar Envio",
|
||||||
|
"confirm_transaction": "Confirmar Transação",
|
||||||
|
"confirmations": "Confirmações",
|
||||||
|
"confirmations_display": "%d confirmações | %s",
|
||||||
|
"confirmed": "Confirmado",
|
||||||
|
"connected": "Conectado",
|
||||||
|
"connected_peers": "Pares Conectados",
|
||||||
|
"connecting": "Conectando...",
|
||||||
|
"console": "Console",
|
||||||
|
"console_auto_scroll": "Rolagem automática",
|
||||||
|
"console_available_commands": "Comandos disponíveis:",
|
||||||
|
"console_capturing_output": "Capturando saída do daemon...",
|
||||||
|
"console_clear": "Limpar",
|
||||||
|
"console_clear_console": "Limpar Console",
|
||||||
|
"console_cleared": "Console limpo",
|
||||||
|
"console_click_commands": "Clique nos comandos acima para inseri-los",
|
||||||
|
"console_click_insert": "Clique para inserir",
|
||||||
|
"console_click_insert_params": "Clique para inserir com parâmetros",
|
||||||
|
"console_close": "Fechar",
|
||||||
|
"console_commands": "Comandos",
|
||||||
|
"console_common_rpc": "Comandos RPC comuns:",
|
||||||
|
"console_completions": "Completações:",
|
||||||
|
"console_connected": "Conectado ao daemon",
|
||||||
|
"console_copy_all": "Copiar Tudo",
|
||||||
|
"console_copy_selected": "Copiar",
|
||||||
|
"console_daemon": "Daemon",
|
||||||
|
"console_daemon_error": "Erro do daemon!",
|
||||||
|
"console_daemon_started": "Daemon iniciado",
|
||||||
|
"console_daemon_stopped": "Daemon parado",
|
||||||
|
"console_disconnected": "Desconectado do daemon",
|
||||||
|
"console_errors": "Erros",
|
||||||
|
"console_filter_hint": "Filtrar saída...",
|
||||||
|
"console_help_clear": " clear - Limpar o console",
|
||||||
|
"console_help_getbalance": " getbalance - Mostrar saldo transparente",
|
||||||
|
"console_help_getblockcount": " getblockcount - Mostrar altura atual do bloco",
|
||||||
|
"console_help_getinfo": " getinfo - Mostrar informações do nó",
|
||||||
|
"console_help_getmininginfo": " getmininginfo - Mostrar status da mineração",
|
||||||
|
"console_help_getpeerinfo": " getpeerinfo - Mostrar pares conectados",
|
||||||
|
"console_help_gettotalbalance": " gettotalbalance - Mostrar saldo total",
|
||||||
|
"console_help_help": " help - Mostrar esta mensagem de ajuda",
|
||||||
|
"console_help_setgenerate": " setgenerate - Controlar mineração",
|
||||||
|
"console_help_stop": " stop - Parar o daemon",
|
||||||
|
"console_line_count": "%zu linhas",
|
||||||
|
"console_new_lines": "%d novas linhas",
|
||||||
|
"console_no_daemon": "Sem daemon",
|
||||||
|
"console_not_connected": "Erro: Não conectado ao daemon",
|
||||||
|
"console_rpc_reference": "Referência de Comandos RPC",
|
||||||
|
"console_scanline": "Scanline do console",
|
||||||
|
"console_search_commands": "Pesquisar comandos...",
|
||||||
|
"console_select_all": "Selecionar Tudo",
|
||||||
|
"console_show_daemon_output": "Mostrar saída do daemon",
|
||||||
|
"console_show_errors_only": "Mostrar apenas erros",
|
||||||
|
"console_show_rpc_ref": "Mostrar referência de comandos RPC",
|
||||||
|
"console_showing_lines": "Mostrando %zu de %zu linhas",
|
||||||
|
"console_starting_node": "Iniciando nó...",
|
||||||
|
"console_status_error": "Erro",
|
||||||
|
"console_status_running": "Em execução",
|
||||||
|
"console_status_starting": "Iniciando",
|
||||||
|
"console_status_stopped": "Parado",
|
||||||
|
"console_status_stopping": "Parando",
|
||||||
|
"console_status_unknown": "Desconhecido",
|
||||||
|
"console_tab_completion": "Tab para completar",
|
||||||
|
"console_type_help": "Digite 'help' para comandos disponíveis",
|
||||||
|
"console_welcome": "Bem-vindo ao Console ObsidianDragon",
|
||||||
|
"console_zoom_in": "Aumentar zoom",
|
||||||
|
"console_zoom_out": "Diminuir zoom",
|
||||||
|
"copy": "Copiar",
|
||||||
|
"copy_address": "Copiar Endereço Completo",
|
||||||
|
"copy_error": "Copiar Erro",
|
||||||
|
"copy_to_clipboard": "Copiar para Área de Transferência",
|
||||||
|
"copy_txid": "Copiar TxID",
|
||||||
|
"copy_uri": "Copiar URI",
|
||||||
|
"current_price": "Preço Atual",
|
||||||
|
"custom_fees": "Taxas personalizadas",
|
||||||
|
"dark": "Escuro",
|
||||||
|
"date": "Data",
|
||||||
|
"date_label": "Data:",
|
||||||
|
"delete": "Excluir",
|
||||||
|
"difficulty": "Dificuldade",
|
||||||
|
"disconnected": "Desconectado",
|
||||||
|
"dismiss": "Dispensar",
|
||||||
|
"display": "Exibição",
|
||||||
|
"dragonx_green": "DragonX (Verde)",
|
||||||
|
"edit": "Editar",
|
||||||
|
"error": "Erro",
|
||||||
|
"est_time_to_block": "Tempo Est. por Bloco",
|
||||||
|
"exit": "Sair",
|
||||||
|
"explorer": "EXPLORADOR",
|
||||||
|
"export": "Exportar",
|
||||||
|
"export_csv": "Exportar CSV",
|
||||||
|
"export_keys_btn": "Exportar Chaves",
|
||||||
|
"export_keys_danger": "PERIGO: Isto exportará TODAS as chaves privadas da sua carteira! Qualquer pessoa com acesso a este arquivo pode roubar seus fundos. Guarde com segurança e exclua após o uso.",
|
||||||
|
"export_keys_include_t": "Incluir endereços T (transparentes)",
|
||||||
|
"export_keys_include_z": "Incluir endereços Z (blindados)",
|
||||||
|
"export_keys_options": "Opções de exportação:",
|
||||||
|
"export_keys_success": "Chaves exportadas com sucesso",
|
||||||
|
"export_keys_title": "Exportar Todas as Chaves Privadas",
|
||||||
|
"export_private_key": "Exportar Chave Privada",
|
||||||
|
"export_tx_count": "Exportar %zu transações para arquivo CSV.",
|
||||||
|
"export_tx_file_fail": "Falha ao criar arquivo CSV",
|
||||||
|
"export_tx_none": "Nenhuma transação para exportar",
|
||||||
|
"export_tx_success": "Transações exportadas com sucesso",
|
||||||
|
"export_tx_title": "Exportar Transações para CSV",
|
||||||
|
"export_viewing_key": "Exportar Chave de Visualização",
|
||||||
|
"failed_create_shielded": "Falha ao criar endereço blindado",
|
||||||
|
"failed_create_transparent": "Falha ao criar endereço transparente",
|
||||||
|
"fee": "Taxa",
|
||||||
|
"fee_high": "Alta",
|
||||||
|
"fee_label": "Taxa:",
|
||||||
|
"fee_low": "Baixa",
|
||||||
|
"fee_normal": "Normal",
|
||||||
|
"fetch_prices": "Buscar preços",
|
||||||
|
"file": "Arquivo",
|
||||||
|
"file_save_location": "O arquivo será salvo em: ~/.config/ObsidianDragon/",
|
||||||
|
"font_scale": "Escala da Fonte",
|
||||||
|
"from": "De",
|
||||||
|
"from_upper": "DE",
|
||||||
|
"full_details": "Detalhes Completos",
|
||||||
|
"general": "Geral",
|
||||||
|
"go_to_receive": "Ir para Receber",
|
||||||
|
"height": "Altura",
|
||||||
|
"help": "Ajuda",
|
||||||
|
"hide": "Ocultar",
|
||||||
|
"history": "Histórico",
|
||||||
|
"immature_type": "Imaturo",
|
||||||
|
"import": "Importar",
|
||||||
|
"import_key_btn": "Importar Chave(s)",
|
||||||
|
"import_key_formats": "Formatos de chave suportados:",
|
||||||
|
"import_key_full_rescan": "(0 = rescan completo)",
|
||||||
|
"import_key_label": "Chave(s) Privada(s):",
|
||||||
|
"import_key_no_valid": "Nenhuma chave válida encontrada na entrada",
|
||||||
|
"import_key_rescan": "Reescanear blockchain após importação",
|
||||||
|
"import_key_start_height": "Altura inicial:",
|
||||||
|
"import_key_success": "Chaves importadas com sucesso",
|
||||||
|
"import_key_t_format": "Chaves privadas WIF de endereços T",
|
||||||
|
"import_key_title": "Importar Chave Privada",
|
||||||
|
"import_key_tooltip": "Digite uma ou mais chaves privadas, uma por linha.\nSuporta chaves de z-endereço e t-endereço.\nLinhas começando com # são tratadas como comentários.",
|
||||||
|
"import_key_warning": "Aviso: Nunca compartilhe suas chaves privadas! Importar chaves de fontes não confiáveis pode comprometer sua carteira.",
|
||||||
|
"import_key_z_format": "Chaves de gasto de z-endereço (secret-extended-key-...)",
|
||||||
|
"import_private_key": "Importar Chave Privada...",
|
||||||
|
"invalid_address": "Formato de endereço inválido",
|
||||||
|
"ip_address": "Endereço IP",
|
||||||
|
"keep": "Manter",
|
||||||
|
"keep_daemon": "Manter daemon em execução",
|
||||||
|
"key_export_fetching": "Buscando chave da carteira...",
|
||||||
|
"key_export_private_key": "Chave Privada:",
|
||||||
|
"key_export_private_warning": "Mantenha esta chave em SEGREDO! Qualquer pessoa com esta chave pode gastar seus fundos. Nunca a compartilhe online ou com terceiros não confiáveis.",
|
||||||
|
"key_export_reveal": "Revelar Chave",
|
||||||
|
"key_export_viewing_key": "Chave de Visualização:",
|
||||||
|
"key_export_viewing_warning": "Esta chave de visualização permite que outros vejam suas transações recebidas e saldo, mas NÃO gastem seus fundos. Compartilhe apenas com partes confiáveis.",
|
||||||
|
"label": "Rótulo:",
|
||||||
|
"language": "Idioma",
|
||||||
|
"light": "Claro",
|
||||||
|
"loading": "Carregando...",
|
||||||
|
"loading_addresses": "Carregando endereços...",
|
||||||
|
"local_hashrate": "Hashrate Local",
|
||||||
|
"low_spec_mode": "Modo econômico",
|
||||||
|
"market": "Mercado",
|
||||||
|
"market_12h": "12h",
|
||||||
|
"market_18h": "18h",
|
||||||
|
"market_24h": "24h",
|
||||||
|
"market_24h_volume": "VOLUME 24H",
|
||||||
|
"market_6h": "6h",
|
||||||
|
"market_attribution": "Dados de preço do NonKYC",
|
||||||
|
"market_btc_price": "PREÇO BTC",
|
||||||
|
"market_cap": "Capitalização",
|
||||||
|
"market_no_history": "Nenhum histórico de preços disponível",
|
||||||
|
"market_no_price": "Sem dados de preço",
|
||||||
|
"market_now": "Agora",
|
||||||
|
"market_pct_shielded": "%.0f%% Blindado",
|
||||||
|
"market_portfolio": "PORTFÓLIO",
|
||||||
|
"market_price_unavailable": "Dados de preço indisponíveis",
|
||||||
|
"market_refresh_price": "Atualizar dados de preço",
|
||||||
|
"market_trade_on": "Negociar no %s",
|
||||||
|
"mature": "Maduro",
|
||||||
|
"max": "Máx",
|
||||||
|
"memo": "Memo (opcional, criptografado)",
|
||||||
|
"memo_label": "Memo:",
|
||||||
|
"memo_optional": "MEMO (OPCIONAL)",
|
||||||
|
"memo_upper": "MEMO",
|
||||||
|
"memo_z_only": "Nota: Memos só estão disponíveis ao enviar para endereços blindados (z)",
|
||||||
|
"merge_description": "Fundir múltiplos UTXOs em um único endereço blindado. Isso pode ajudar a reduzir o tamanho da carteira e melhorar a privacidade.",
|
||||||
|
"merge_funds": "Fundir Fundos",
|
||||||
|
"merge_started": "Operação de fusão iniciada",
|
||||||
|
"merge_title": "Fundir para Endereço",
|
||||||
|
"mine_when_idle": "Minerar quando ocioso",
|
||||||
|
"mined": "minerado",
|
||||||
|
"mined_filter": "Minerado",
|
||||||
|
"mined_type": "Minerado",
|
||||||
|
"mined_upper": "MINERADO",
|
||||||
|
"miner_fee": "Taxa de Minerador",
|
||||||
|
"mining": "Mineração",
|
||||||
|
"mining_active": "Ativo",
|
||||||
|
"mining_address_copied": "Endereço de mineração copiado",
|
||||||
|
"mining_all_time": "Todo o Tempo",
|
||||||
|
"mining_already_saved": "URL do pool já salva",
|
||||||
|
"mining_block_copied": "Hash do bloco copiado",
|
||||||
|
"mining_chart_1m_ago": "1m atrás",
|
||||||
|
"mining_chart_5m_ago": "5m atrás",
|
||||||
|
"mining_chart_now": "Agora",
|
||||||
|
"mining_chart_start": "Início",
|
||||||
|
"mining_click": "Clique",
|
||||||
|
"mining_click_copy_address": "Clique para copiar o endereço",
|
||||||
|
"mining_click_copy_block": "Clique para copiar o hash do bloco",
|
||||||
|
"mining_click_copy_difficulty": "Clique para copiar a dificuldade",
|
||||||
|
"mining_connected": "Conectado",
|
||||||
|
"mining_connecting": "Conectando...",
|
||||||
|
"mining_control": "Controle de Mineração",
|
||||||
|
"mining_difficulty_copied": "Dificuldade copiada",
|
||||||
|
"mining_est_block": "Bloco Est.",
|
||||||
|
"mining_est_daily": "Est. Diário",
|
||||||
|
"mining_filter_all": "Todos",
|
||||||
|
"mining_filter_tip_all": "Mostrar todos os ganhos",
|
||||||
|
"mining_filter_tip_pool": "Mostrar apenas ganhos do pool",
|
||||||
|
"mining_filter_tip_solo": "Mostrar apenas ganhos solo",
|
||||||
|
"mining_idle_off_tooltip": "Ativar mineração ociosa",
|
||||||
|
"mining_idle_on_tooltip": "Desativar mineração ociosa",
|
||||||
|
"mining_local_hashrate": "Hashrate Local",
|
||||||
|
"mining_mine": "Minerar",
|
||||||
|
"mining_mining_addr": "End. Mineração",
|
||||||
|
"mining_network": "Rede",
|
||||||
|
"mining_no_blocks_yet": "Nenhum bloco encontrado ainda",
|
||||||
|
"mining_no_payouts_yet": "Nenhum pagamento de pool ainda",
|
||||||
|
"mining_no_saved_addresses": "Nenhum endereço salvo",
|
||||||
|
"mining_no_saved_pools": "Nenhum pool salvo",
|
||||||
|
"mining_off": "Mineração está DESLIGADA",
|
||||||
|
"mining_on": "Mineração está LIGADA",
|
||||||
|
"mining_open_in_explorer": "Abrir no explorador",
|
||||||
|
"mining_payout_address": "Endereço de Pagamento",
|
||||||
|
"mining_payout_tooltip": "Endereço para receber recompensas de mineração",
|
||||||
|
"mining_pool": "Pool",
|
||||||
|
"mining_pool_hashrate": "Hashrate do Pool",
|
||||||
|
"mining_pool_url": "URL do Pool",
|
||||||
|
"mining_recent_blocks": "BLOCOS RECENTES",
|
||||||
|
"mining_recent_payouts": "PAGAMENTOS DE POOL RECENTES",
|
||||||
|
"mining_remove": "Remover",
|
||||||
|
"mining_reset_defaults": "Redefinir Padrões",
|
||||||
|
"mining_save_payout_address": "Salvar endereço de pagamento",
|
||||||
|
"mining_save_pool_url": "Salvar URL do pool",
|
||||||
|
"mining_saved_addresses": "Endereços Salvos:",
|
||||||
|
"mining_saved_pools": "Pools Salvos:",
|
||||||
|
"mining_shares": "Shares",
|
||||||
|
"mining_show_chart": "Gráfico",
|
||||||
|
"mining_show_log": "Log",
|
||||||
|
"mining_solo": "Solo",
|
||||||
|
"mining_starting": "Iniciando...",
|
||||||
|
"mining_starting_tooltip": "Minerador está iniciando...",
|
||||||
|
"mining_statistics": "Estatísticas de Mineração",
|
||||||
|
"mining_stop": "Parar",
|
||||||
|
"mining_stop_solo_for_pool": "Pare a mineração solo antes de iniciar a mineração em pool",
|
||||||
|
"mining_stop_solo_for_pool_settings": "Pare a mineração solo para alterar as configurações do pool",
|
||||||
|
"mining_stopping": "Parando...",
|
||||||
|
"mining_stopping_tooltip": "Minerador está parando...",
|
||||||
|
"mining_syncing_tooltip": "Blockchain está sincronizando...",
|
||||||
|
"mining_threads": "Threads de Mineração",
|
||||||
|
"mining_to_save": "para salvar",
|
||||||
|
"mining_today": "Hoje",
|
||||||
|
"mining_uptime": "Tempo Ativo",
|
||||||
|
"mining_yesterday": "Ontem",
|
||||||
|
"network": "Rede",
|
||||||
|
"network_fee": "TAXA DA REDE",
|
||||||
|
"network_hashrate": "Hashrate da Rede",
|
||||||
|
"new": "+ Novo",
|
||||||
|
"new_shielded_created": "Novo endereço blindado criado",
|
||||||
|
"new_t_address": "Novo Endereço T",
|
||||||
|
"new_t_transparent": "Novo endereço t (Transparente)",
|
||||||
|
"new_transparent_created": "Novo endereço transparente criado",
|
||||||
|
"new_z_address": "Novo Endereço Z",
|
||||||
|
"new_z_shielded": "Novo endereço z (Blindado)",
|
||||||
|
"no_addresses": "Nenhum endereço encontrado. Crie um usando os botões acima.",
|
||||||
|
"no_addresses_available": "Nenhum endereço disponível",
|
||||||
|
"no_addresses_match": "Nenhum endereço corresponde ao filtro",
|
||||||
|
"no_addresses_with_balance": "Nenhum endereço com saldo",
|
||||||
|
"no_matching": "Nenhuma transação correspondente",
|
||||||
|
"no_recent_receives": "Nenhum recebimento recente",
|
||||||
|
"no_recent_sends": "Nenhum envio recente",
|
||||||
|
"no_transactions": "Nenhuma transação encontrada",
|
||||||
|
"node": "NÓ",
|
||||||
|
"node_security": "NÓ & SEGURANÇA",
|
||||||
|
"noise": "Ruído",
|
||||||
|
"not_connected": "Não conectado ao daemon...",
|
||||||
|
"not_connected_to_daemon": "Não conectado ao daemon",
|
||||||
|
"notes": "Notas",
|
||||||
|
"notes_optional": "Notas (opcional):",
|
||||||
|
"output_filename": "Nome do arquivo de saída:",
|
||||||
|
"overview": "Visão Geral",
|
||||||
|
"paste": "Colar",
|
||||||
|
"paste_from_clipboard": "Colar da Área de Transferência",
|
||||||
|
"pay_from": "Pagar de",
|
||||||
|
"payment_request": "SOLICITAÇÃO DE PAGAMENTO",
|
||||||
|
"payment_request_copied": "Solicitação de pagamento copiada",
|
||||||
|
"payment_uri_copied": "URI de pagamento copiada",
|
||||||
|
"peers": "Pares",
|
||||||
|
"peers_avg_ping": "Ping Médio",
|
||||||
|
"peers_ban_24h": "Banir Par 24h",
|
||||||
|
"peers_ban_score": "Score de Ban: %d",
|
||||||
|
"peers_banned": "Banidos",
|
||||||
|
"peers_banned_count": "Banidos: %d",
|
||||||
|
"peers_best_block": "Melhor Bloco",
|
||||||
|
"peers_blockchain": "BLOCKCHAIN",
|
||||||
|
"peers_blocks": "Blocos",
|
||||||
|
"peers_blocks_left": "%d blocos restantes",
|
||||||
|
"peers_clear_all_bans": "Remover Todos os Banimentos",
|
||||||
|
"peers_click_copy": "Clique para copiar",
|
||||||
|
"peers_connected": "Conectados",
|
||||||
|
"peers_connected_count": "Conectados: %d",
|
||||||
|
"peers_copy_ip": "Copiar IP",
|
||||||
|
"peers_dir_in": "Ent.",
|
||||||
|
"peers_dir_out": "Saí.",
|
||||||
|
"peers_hash_copied": "Hash copiado",
|
||||||
|
"peers_hashrate": "Hashrate",
|
||||||
|
"peers_in_out": "Ent./Saí.",
|
||||||
|
"peers_longest": "Mais longa",
|
||||||
|
"peers_longest_chain": "Chain Mais Longa",
|
||||||
|
"peers_memory": "Memória",
|
||||||
|
"peers_no_banned": "Nenhum par banido",
|
||||||
|
"peers_no_connected": "Nenhum par conectado",
|
||||||
|
"peers_no_tls": "Sem TLS",
|
||||||
|
"peers_notarized": "Notarizado",
|
||||||
|
"peers_p2p_port": "Porta P2P",
|
||||||
|
"peers_protocol": "Protocolo",
|
||||||
|
"peers_received": "Recebido",
|
||||||
|
"peers_refresh": "Atualizar",
|
||||||
|
"peers_refresh_tooltip": "Atualizar lista de pares",
|
||||||
|
"peers_refreshing": "Atualizando...",
|
||||||
|
"peers_sent": "Enviado",
|
||||||
|
"peers_tt_id": "ID: %d",
|
||||||
|
"peers_tt_received": "Recebido: %s",
|
||||||
|
"peers_tt_sent": "Enviado: %s",
|
||||||
|
"peers_tt_services": "Serviços: %s",
|
||||||
|
"peers_tt_start_height": "Altura Inicial: %d",
|
||||||
|
"peers_tt_synced": "Sincronizado H/B: %d/%d",
|
||||||
|
"peers_tt_tls_cipher": "TLS: %s",
|
||||||
|
"peers_unban": "Desbanir",
|
||||||
|
"peers_upper": "PARES",
|
||||||
|
"peers_version": "Versão",
|
||||||
|
"pending": "Pendente",
|
||||||
|
"ping": "Ping",
|
||||||
|
"price_chart": "Gráfico de Preços",
|
||||||
|
"qr_code": "Código QR",
|
||||||
|
"qr_failed": "Falha ao gerar código QR",
|
||||||
|
"qr_title": "Código QR",
|
||||||
|
"qr_unavailable": "QR indisponível",
|
||||||
|
"receive": "Receber",
|
||||||
|
"received": "recebido",
|
||||||
|
"received_filter": "Recebido",
|
||||||
|
"received_label": "Recebido",
|
||||||
|
"received_upper": "RECEBIDO",
|
||||||
|
"receiving_addresses": "Seus Endereços de Recebimento",
|
||||||
|
"recent_received": "RECEBIDOS RECENTES",
|
||||||
|
"recent_sends": "ENVIOS RECENTES",
|
||||||
|
"recipient": "DESTINATÁRIO",
|
||||||
|
"recv_type": "Receb.",
|
||||||
|
"refresh": "Atualizar",
|
||||||
|
"refresh_now": "Atualizar Agora",
|
||||||
|
"report_bug": "Reportar Bug",
|
||||||
|
"request_amount": "Valor (opcional):",
|
||||||
|
"request_copy_uri": "Copiar URI",
|
||||||
|
"request_description": "Gere uma solicitação de pagamento que outros podem escanear ou copiar. O código QR contém seu endereço e valor/memo opcionais.",
|
||||||
|
"request_label": "Rótulo (opcional):",
|
||||||
|
"request_memo": "Memo (opcional):",
|
||||||
|
"request_payment": "Solicitar Pagamento",
|
||||||
|
"request_payment_uri": "URI de Pagamento:",
|
||||||
|
"request_receive_address": "Endereço de Recebimento:",
|
||||||
|
"request_select_address": "Selecionar endereço...",
|
||||||
|
"request_shielded_addrs": "-- Endereços Blindados --",
|
||||||
|
"request_title": "Solicitar Pagamento",
|
||||||
|
"request_transparent_addrs": "-- Endereços Transparentes --",
|
||||||
|
"request_uri_copied": "URI de pagamento copiada para a área de transferência",
|
||||||
|
"rescan": "Reescanear",
|
||||||
|
"reset_to_defaults": "Redefinir Padrões",
|
||||||
|
"review_send": "Revisar Envio",
|
||||||
|
"rpc_host": "Host RPC",
|
||||||
|
"rpc_pass": "Senha",
|
||||||
|
"rpc_port": "Porta",
|
||||||
|
"rpc_user": "Usuário",
|
||||||
|
"save": "Salvar",
|
||||||
|
"save_settings": "Salvar Configurações",
|
||||||
|
"save_z_transactions": "Salvar Z-tx na lista de tx",
|
||||||
|
"search_placeholder": "Pesquisar...",
|
||||||
|
"security": "SEGURANÇA",
|
||||||
|
"select_address": "Selecionar endereço...",
|
||||||
|
"select_receiving_address": "Selecionar endereço de recebimento...",
|
||||||
|
"select_source_address": "Selecionar endereço de origem...",
|
||||||
|
"send": "Enviar",
|
||||||
|
"send_amount": "Valor",
|
||||||
|
"send_amount_details": "DETALHES DO VALOR",
|
||||||
|
"send_amount_upper": "VALOR",
|
||||||
|
"send_clear_fields": "Limpar todos os campos do formulário?",
|
||||||
|
"send_copy_error": "Copiar Erro",
|
||||||
|
"send_dismiss": "Dispensar",
|
||||||
|
"send_error_copied": "Erro copiado para a área de transferência",
|
||||||
|
"send_error_prefix": "Erro: %s",
|
||||||
|
"send_exceeds_available": "Excede o disponível (%.8f)",
|
||||||
|
"send_fee": "Taxa",
|
||||||
|
"send_fee_high": "Alta",
|
||||||
|
"send_fee_low": "Baixa",
|
||||||
|
"send_fee_normal": "Normal",
|
||||||
|
"send_form_restored": "Formulário restaurado",
|
||||||
|
"send_from_this_address": "Enviar deste endereço",
|
||||||
|
"send_go_to_receive": "Ir para Receber",
|
||||||
|
"send_keep": "Manter",
|
||||||
|
"send_network_fee": "TAXA DA REDE",
|
||||||
|
"send_no_balance": "Sem saldo",
|
||||||
|
"send_no_recent": "Nenhum envio recente",
|
||||||
|
"send_recent_sends": "ENVIOS RECENTES",
|
||||||
|
"send_recipient": "DESTINATÁRIO",
|
||||||
|
"send_select_source": "Selecionar endereço de origem...",
|
||||||
|
"send_sending_from": "ENVIANDO DE",
|
||||||
|
"send_submitting": "Enviando transação...",
|
||||||
|
"send_switch_to_receive": "Mude para Receber para obter seu endereço e começar a receber fundos.",
|
||||||
|
"send_to": "Enviar para",
|
||||||
|
"send_tooltip_enter_amount": "Digite um valor para enviar",
|
||||||
|
"send_tooltip_exceeds_balance": "Valor excede o saldo disponível",
|
||||||
|
"send_tooltip_in_progress": "Transação já em andamento",
|
||||||
|
"send_tooltip_invalid_address": "Digite um endereço de destinatário válido",
|
||||||
|
"send_tooltip_not_connected": "Não conectado ao daemon",
|
||||||
|
"send_tooltip_select_source": "Selecione primeiro um endereço de origem",
|
||||||
|
"send_tooltip_syncing": "Aguarde a sincronização da blockchain",
|
||||||
|
"send_total": "Total",
|
||||||
|
"send_transaction": "Enviar Transação",
|
||||||
|
"send_tx_failed": "Transação falhou",
|
||||||
|
"send_tx_sent": "Transação enviada!",
|
||||||
|
"send_tx_success": "Transação enviada com sucesso!",
|
||||||
|
"send_txid_copied": "TxID copiado para a área de transferência",
|
||||||
|
"send_txid_label": "TxID: %s",
|
||||||
|
"send_valid_shielded": "Endereço blindado válido",
|
||||||
|
"send_valid_transparent": "Endereço transparente válido",
|
||||||
|
"send_wallet_empty": "Sua carteira está vazia",
|
||||||
|
"send_yes_clear": "Sim, Limpar",
|
||||||
|
"sending": "Enviando transação",
|
||||||
|
"sending_from": "ENVIANDO DE",
|
||||||
|
"sent": "enviado",
|
||||||
|
"sent_filter": "Enviado",
|
||||||
|
"sent_type": "Enviado",
|
||||||
|
"sent_upper": "ENVIADO",
|
||||||
|
"settings": "Configurações",
|
||||||
|
"setup_wizard": "Assistente de Configuração",
|
||||||
|
"share": "Compartilhar",
|
||||||
|
"shield_check_status": "Verificar Status",
|
||||||
|
"shield_completed": "Operação concluída com sucesso!",
|
||||||
|
"shield_description": "Blinde suas recompensas de mineração enviando saídas coinbase de endereços transparentes para um endereço blindado. Isso melhora a privacidade ocultando sua renda de mineração.",
|
||||||
|
"shield_from_address": "Do Endereço:",
|
||||||
|
"shield_funds": "Blindar Fundos",
|
||||||
|
"shield_in_progress": "Operação em andamento...",
|
||||||
|
"shield_max_utxos": "Máx. UTXOs por operação",
|
||||||
|
"shield_merge_done": "Blindagem/fusão concluída!",
|
||||||
|
"shield_select_z": "Selecionar z-endereço...",
|
||||||
|
"shield_started": "Operação de blindagem iniciada",
|
||||||
|
"shield_title": "Blindar Recompensas Coinbase",
|
||||||
|
"shield_to_address": "Para Endereço (Blindado):",
|
||||||
|
"shield_utxo_limit": "Limite UTXO:",
|
||||||
|
"shield_wildcard_hint": "Use '*' para blindar de todos os endereços transparentes",
|
||||||
|
"shielded": "Blindado",
|
||||||
|
"shielded_to": "BLINDADO PARA",
|
||||||
|
"shielded_type": "Blindado",
|
||||||
|
"show": "Mostrar",
|
||||||
|
"show_qr_code": "Mostrar Código QR",
|
||||||
|
"showing_transactions": "Mostrando %d\xe2\x80\x93%d de %d transações (total: %zu)",
|
||||||
|
"simple_background": "Fundo simples",
|
||||||
|
"start_mining": "Iniciar Mineração",
|
||||||
|
"status": "Status",
|
||||||
|
"stop_external": "Parar daemon externo",
|
||||||
|
"stop_mining": "Parar Mineração",
|
||||||
|
"submitting_transaction": "Enviando transação...",
|
||||||
|
"success": "Sucesso",
|
||||||
|
"summary": "Resumo",
|
||||||
|
"syncing": "Sincronizando...",
|
||||||
|
"t_addresses": "Endereços T",
|
||||||
|
"test_connection": "Testar",
|
||||||
|
"theme": "Tema",
|
||||||
|
"theme_effects": "Efeitos de tema",
|
||||||
|
"time_days_ago": "há %d dias",
|
||||||
|
"time_hours_ago": "há %d horas",
|
||||||
|
"time_minutes_ago": "há %d minutos",
|
||||||
|
"time_seconds_ago": "há %d segundos",
|
||||||
|
"to": "Para",
|
||||||
|
"to_upper": "PARA",
|
||||||
|
"tools": "FERRAMENTAS",
|
||||||
|
"total": "Total",
|
||||||
|
"transaction_id": "ID DA TRANSAÇÃO",
|
||||||
|
"transaction_sent": "Transação enviada com sucesso",
|
||||||
|
"transaction_sent_msg": "Transação enviada!",
|
||||||
|
"transaction_url": "URL da Transação",
|
||||||
|
"transactions": "Transações",
|
||||||
|
"transactions_upper": "TRANSAÇÕES",
|
||||||
|
"transparent": "Transparente",
|
||||||
|
"tx_confirmations": "%d confirmações",
|
||||||
|
"tx_details_title": "Detalhes da Transação",
|
||||||
|
"tx_from_address": "Endereço de Origem:",
|
||||||
|
"tx_id_label": "ID da Transação:",
|
||||||
|
"tx_immature": "IMATURO",
|
||||||
|
"tx_mined": "MINERADO",
|
||||||
|
"tx_received": "RECEBIDO",
|
||||||
|
"tx_sent": "ENVIADO",
|
||||||
|
"tx_to_address": "Endereço de Destino:",
|
||||||
|
"tx_view_explorer": "Ver no Explorador",
|
||||||
|
"txs_count": "%d txs",
|
||||||
|
"type": "Tipo",
|
||||||
|
"ui_opacity": "Opacidade da Interface",
|
||||||
|
"unban": "Desbanir",
|
||||||
|
"unconfirmed": "Não confirmado",
|
||||||
|
"undo_clear": "Desfazer Limpeza",
|
||||||
|
"unknown": "Desconhecido",
|
||||||
|
"use_embedded_daemon": "Usar dragonxd integrado",
|
||||||
|
"use_tor": "Usar Tor",
|
||||||
|
"validate_btn": "Validar",
|
||||||
|
"validate_description": "Digite um endereço DragonX para verificar se é válido e se pertence a esta carteira.",
|
||||||
|
"validate_invalid": "INVÁLIDO",
|
||||||
|
"validate_is_mine": "Esta carteira possui este endereço",
|
||||||
|
"validate_not_mine": "Não pertence a esta carteira",
|
||||||
|
"validate_ownership": "Propriedade:",
|
||||||
|
"validate_results": "Resultados:",
|
||||||
|
"validate_shielded_type": "Blindado (z-endereço)",
|
||||||
|
"validate_status": "Status:",
|
||||||
|
"validate_title": "Validar Endereço",
|
||||||
|
"validate_transparent_type": "Transparente (t-endereço)",
|
||||||
|
"validate_type": "Tipo:",
|
||||||
|
"validate_valid": "VÁLIDO",
|
||||||
|
"validating": "Validando...",
|
||||||
|
"verbose_logging": "Log detalhado",
|
||||||
|
"version": "Versão",
|
||||||
|
"view": "Visualizar",
|
||||||
|
"view_details": "Ver Detalhes",
|
||||||
|
"view_on_explorer": "Ver no Explorador",
|
||||||
|
"waiting_for_daemon": "Aguardando conexão com o daemon...",
|
||||||
|
"wallet": "CARTEIRA",
|
||||||
|
"wallet_empty": "Sua carteira está vazia",
|
||||||
|
"wallet_empty_hint": "Mude para Receber para obter seu endereço e começar a receber fundos.",
|
||||||
|
"warning": "Aviso",
|
||||||
|
"warning_upper": "AVISO!",
|
||||||
|
"website": "Website",
|
||||||
|
"window_opacity": "Opacidade da Janela",
|
||||||
|
"yes_clear": "Sim, Limpar",
|
||||||
|
"your_addresses": "Seus Endereços",
|
||||||
|
"z_addresses": "Endereços Z",
|
||||||
|
}
|
||||||
|
|
||||||
|
out = os.path.join(os.path.dirname(__file__), "..", "res", "lang", "pt.json")
|
||||||
|
with open(out, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(translations, f, indent=4, ensure_ascii=False, sort_keys=True)
|
||||||
|
print(f"Wrote {len(translations)} Portuguese translations to {os.path.abspath(out)}")
|
||||||
646
scripts/gen_ru.py
Normal file
@@ -0,0 +1,646 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Generate Russian (ru) translations for ObsidianDragon wallet."""
|
||||||
|
import json, os
|
||||||
|
|
||||||
|
translations = {
|
||||||
|
"24h_change": "Изменение за 24ч",
|
||||||
|
"24h_volume": "Объём за 24ч",
|
||||||
|
"about": "О программе",
|
||||||
|
"about_block_explorer": "Обозреватель блоков",
|
||||||
|
"about_block_height": "Высота блока:",
|
||||||
|
"about_build_date": "Дата сборки:",
|
||||||
|
"about_build_type": "Тип сборки:",
|
||||||
|
"about_chain": "Цепочка:",
|
||||||
|
"about_connections": "Подключения:",
|
||||||
|
"about_credits": "Благодарности",
|
||||||
|
"about_daemon": "Daemon:",
|
||||||
|
"about_debug": "Отладка",
|
||||||
|
"about_dragonx": "Об ObsidianDragon",
|
||||||
|
"about_edition": "Редакция ImGui",
|
||||||
|
"about_github": "GitHub",
|
||||||
|
"about_imgui": "ImGui:",
|
||||||
|
"about_license": "Лицензия",
|
||||||
|
"about_license_text": "Это программное обеспечение выпущено под лицензией GNU General Public License v3 (GPLv3). Вы можете свободно использовать, изменять и распространять это ПО в соответствии с условиями лицензии.",
|
||||||
|
"about_peers_count": "%zu узлов",
|
||||||
|
"about_release": "Релиз",
|
||||||
|
"about_title": "Об ObsidianDragon",
|
||||||
|
"about_version": "Версия:",
|
||||||
|
"about_website": "Веб-сайт",
|
||||||
|
"acrylic": "Акрил",
|
||||||
|
"add": "Добавить",
|
||||||
|
"address": "Адрес",
|
||||||
|
"address_book_add": "Добавить адрес",
|
||||||
|
"address_book_add_new": "Добавить новый",
|
||||||
|
"address_book_added": "Адрес добавлен в книгу",
|
||||||
|
"address_book_count": "%zu адресов сохранено",
|
||||||
|
"address_book_deleted": "Запись удалена",
|
||||||
|
"address_book_edit": "Редактировать адрес",
|
||||||
|
"address_book_empty": "Нет сохранённых адресов. Нажмите 'Добавить новый', чтобы создать.",
|
||||||
|
"address_book_exists": "Адрес уже существует в книге",
|
||||||
|
"address_book_title": "Адресная книга",
|
||||||
|
"address_book_update_failed": "Не удалось обновить — адрес может быть дубликатом",
|
||||||
|
"address_book_updated": "Адрес обновлён",
|
||||||
|
"address_copied": "Адрес скопирован в буфер обмена",
|
||||||
|
"address_details": "Детали адреса",
|
||||||
|
"address_label": "Адрес:",
|
||||||
|
"address_upper": "АДРЕС",
|
||||||
|
"address_url": "URL адреса",
|
||||||
|
"addresses_appear_here": "Ваши адреса для получения появятся здесь после подключения.",
|
||||||
|
"advanced": "РАСШИРЕННЫЕ",
|
||||||
|
"all_filter": "Все",
|
||||||
|
"allow_custom_fees": "Разрешить пользовательские комиссии",
|
||||||
|
"amount": "Сумма",
|
||||||
|
"amount_details": "ДЕТАЛИ СУММЫ",
|
||||||
|
"amount_exceeds_balance": "Сумма превышает баланс",
|
||||||
|
"amount_label": "Сумма:",
|
||||||
|
"appearance": "ВНЕШНИЙ ВИД",
|
||||||
|
"auto_shield": "Авто-экранирование майнинга",
|
||||||
|
"available": "Доступно",
|
||||||
|
"backup_backing_up": "Создание резервной копии...",
|
||||||
|
"backup_create": "Создать резервную копию",
|
||||||
|
"backup_created": "Резервная копия кошелька создана",
|
||||||
|
"backup_data": "РЕЗЕРВНОЕ КОПИРОВАНИЕ И ДАННЫЕ",
|
||||||
|
"backup_description": "Создайте резервную копию файла wallet.dat. Этот файл содержит все ваши приватные ключи и историю транзакций. Храните копию в безопасном месте.",
|
||||||
|
"backup_destination": "Место сохранения:",
|
||||||
|
"backup_tip_external": "Храните резервные копии на внешних дисках или в облаке",
|
||||||
|
"backup_tip_multiple": "Создавайте несколько копий в разных местах",
|
||||||
|
"backup_tip_test": "Периодически проверяйте восстановление из резервной копии",
|
||||||
|
"backup_tips": "Советы:",
|
||||||
|
"backup_title": "Резервное копирование кошелька",
|
||||||
|
"backup_wallet": "Создать резервную копию...",
|
||||||
|
"backup_wallet_not_found": "Предупреждение: wallet.dat не найден в ожидаемом расположении",
|
||||||
|
"balance": "Баланс",
|
||||||
|
"balance_layout": "Макет баланса",
|
||||||
|
"ban": "Заблокировать",
|
||||||
|
"banned_peers": "Заблокированные узлы",
|
||||||
|
"block": "Блок",
|
||||||
|
"block_bits": "Биты:",
|
||||||
|
"block_click_next": "Нажмите для следующего блока",
|
||||||
|
"block_click_prev": "Нажмите для предыдущего блока",
|
||||||
|
"block_explorer": "Обозреватель блоков",
|
||||||
|
"block_get_info": "Получить информацию о блоке",
|
||||||
|
"block_hash": "Хэш блока:",
|
||||||
|
"block_height": "Высота блока:",
|
||||||
|
"block_info_title": "Информация о блоке",
|
||||||
|
"block_merkle_root": "Корень Меркла:",
|
||||||
|
"block_nav_next": "Далее >>",
|
||||||
|
"block_nav_prev": "<< Назад",
|
||||||
|
"block_next": "Следующий блок:",
|
||||||
|
"block_previous": "Предыдущий блок:",
|
||||||
|
"block_size": "Размер:",
|
||||||
|
"block_timestamp": "Временная метка:",
|
||||||
|
"block_transactions": "Транзакции:",
|
||||||
|
"blockchain_syncing": "Синхронизация блокчейна (%.1f%%)... Балансы могут быть неточными.",
|
||||||
|
"cancel": "Отмена",
|
||||||
|
"characters": "символов",
|
||||||
|
"clear": "Очистить",
|
||||||
|
"clear_all_bans": "Снять все блокировки",
|
||||||
|
"clear_form_confirm": "Очистить все поля формы?",
|
||||||
|
"clear_request": "Очистить запрос",
|
||||||
|
"click_copy_address": "Нажмите, чтобы скопировать адрес",
|
||||||
|
"click_copy_uri": "Нажмите, чтобы скопировать URI",
|
||||||
|
"close": "Закрыть",
|
||||||
|
"conf_count": "%d подтв.",
|
||||||
|
"confirm_and_send": "Подтвердить и отправить",
|
||||||
|
"confirm_send": "Подтвердить отправку",
|
||||||
|
"confirm_transaction": "Подтвердить транзакцию",
|
||||||
|
"confirmations": "Подтверждения",
|
||||||
|
"confirmations_display": "%d подтверждений | %s",
|
||||||
|
"confirmed": "Подтверждено",
|
||||||
|
"connected": "Подключено",
|
||||||
|
"connected_peers": "Подключённые узлы",
|
||||||
|
"connecting": "Подключение...",
|
||||||
|
"console": "Консоль",
|
||||||
|
"console_auto_scroll": "Авто-прокрутка",
|
||||||
|
"console_available_commands": "Доступные команды:",
|
||||||
|
"console_capturing_output": "Захват вывода daemon...",
|
||||||
|
"console_clear": "Очистить",
|
||||||
|
"console_clear_console": "Очистить консоль",
|
||||||
|
"console_cleared": "Консоль очищена",
|
||||||
|
"console_click_commands": "Нажмите на команды выше, чтобы вставить их",
|
||||||
|
"console_click_insert": "Нажмите для вставки",
|
||||||
|
"console_click_insert_params": "Нажмите для вставки с параметрами",
|
||||||
|
"console_close": "Закрыть",
|
||||||
|
"console_commands": "Команды",
|
||||||
|
"console_common_rpc": "Частые RPC-команды:",
|
||||||
|
"console_completions": "Дополнения:",
|
||||||
|
"console_connected": "Подключено к daemon",
|
||||||
|
"console_copy_all": "Копировать всё",
|
||||||
|
"console_copy_selected": "Копировать",
|
||||||
|
"console_daemon": "Daemon",
|
||||||
|
"console_daemon_error": "Ошибка daemon!",
|
||||||
|
"console_daemon_started": "Daemon запущен",
|
||||||
|
"console_daemon_stopped": "Daemon остановлен",
|
||||||
|
"console_disconnected": "Отключено от daemon",
|
||||||
|
"console_errors": "Ошибки",
|
||||||
|
"console_filter_hint": "Фильтр вывода...",
|
||||||
|
"console_help_clear": " clear - Очистить консоль",
|
||||||
|
"console_help_getbalance": " getbalance - Показать прозрачный баланс",
|
||||||
|
"console_help_getblockcount": " getblockcount - Показать текущую высоту блока",
|
||||||
|
"console_help_getinfo": " getinfo - Показать информацию об узле",
|
||||||
|
"console_help_getmininginfo": " getmininginfo - Показать статус майнинга",
|
||||||
|
"console_help_getpeerinfo": " getpeerinfo - Показать подключённые узлы",
|
||||||
|
"console_help_gettotalbalance": " gettotalbalance - Показать общий баланс",
|
||||||
|
"console_help_help": " help - Показать эту справку",
|
||||||
|
"console_help_setgenerate": " setgenerate - Управление майнингом",
|
||||||
|
"console_help_stop": " stop - Остановить daemon",
|
||||||
|
"console_line_count": "%zu строк",
|
||||||
|
"console_new_lines": "%d новых строк",
|
||||||
|
"console_no_daemon": "Нет daemon",
|
||||||
|
"console_not_connected": "Ошибка: Не подключено к daemon",
|
||||||
|
"console_rpc_reference": "Справочник RPC-команд",
|
||||||
|
"console_scanline": "Скан-линия консоли",
|
||||||
|
"console_search_commands": "Поиск команд...",
|
||||||
|
"console_select_all": "Выбрать всё",
|
||||||
|
"console_show_daemon_output": "Показать вывод daemon",
|
||||||
|
"console_show_errors_only": "Показать только ошибки",
|
||||||
|
"console_show_rpc_ref": "Показать справочник RPC-команд",
|
||||||
|
"console_showing_lines": "Показано %zu из %zu строк",
|
||||||
|
"console_starting_node": "Запуск узла...",
|
||||||
|
"console_status_error": "Ошибка",
|
||||||
|
"console_status_running": "Работает",
|
||||||
|
"console_status_starting": "Запуск",
|
||||||
|
"console_status_stopped": "Остановлен",
|
||||||
|
"console_status_stopping": "Остановка",
|
||||||
|
"console_status_unknown": "Неизвестно",
|
||||||
|
"console_tab_completion": "Tab для дополнения",
|
||||||
|
"console_type_help": "Введите 'help' для списка команд",
|
||||||
|
"console_welcome": "Добро пожаловать в консоль ObsidianDragon",
|
||||||
|
"console_zoom_in": "Увеличить",
|
||||||
|
"console_zoom_out": "Уменьшить",
|
||||||
|
"copy": "Копировать",
|
||||||
|
"copy_address": "Копировать полный адрес",
|
||||||
|
"copy_error": "Копировать ошибку",
|
||||||
|
"copy_to_clipboard": "Копировать в буфер обмена",
|
||||||
|
"copy_txid": "Копировать TxID",
|
||||||
|
"copy_uri": "Копировать URI",
|
||||||
|
"current_price": "Текущая цена",
|
||||||
|
"custom_fees": "Пользовательские комиссии",
|
||||||
|
"dark": "Тёмная",
|
||||||
|
"date": "Дата",
|
||||||
|
"date_label": "Дата:",
|
||||||
|
"delete": "Удалить",
|
||||||
|
"difficulty": "Сложность",
|
||||||
|
"disconnected": "Отключено",
|
||||||
|
"dismiss": "Отклонить",
|
||||||
|
"display": "Отображение",
|
||||||
|
"dragonx_green": "DragonX (Зелёная)",
|
||||||
|
"edit": "Редактировать",
|
||||||
|
"error": "Ошибка",
|
||||||
|
"est_time_to_block": "Расч. время до блока",
|
||||||
|
"exit": "Выход",
|
||||||
|
"explorer": "ОБОЗРЕВАТЕЛЬ",
|
||||||
|
"export": "Экспорт",
|
||||||
|
"export_csv": "Экспорт в CSV",
|
||||||
|
"export_keys_btn": "Экспорт ключей",
|
||||||
|
"export_keys_danger": "ОПАСНОСТЬ: Будут экспортированы ВСЕ приватные ключи из вашего кошелька! Любой, кто получит доступ к этому файлу, сможет украсть ваши средства. Храните его в безопасности и удалите после использования.",
|
||||||
|
"export_keys_include_t": "Включить T-адреса (прозрачные)",
|
||||||
|
"export_keys_include_z": "Включить Z-адреса (экранированные)",
|
||||||
|
"export_keys_options": "Параметры экспорта:",
|
||||||
|
"export_keys_success": "Ключи успешно экспортированы",
|
||||||
|
"export_keys_title": "Экспорт всех приватных ключей",
|
||||||
|
"export_private_key": "Экспорт приватного ключа",
|
||||||
|
"export_tx_count": "Экспортировать %zu транзакций в файл CSV.",
|
||||||
|
"export_tx_file_fail": "Не удалось создать файл CSV",
|
||||||
|
"export_tx_none": "Нет транзакций для экспорта",
|
||||||
|
"export_tx_success": "Транзакции успешно экспортированы",
|
||||||
|
"export_tx_title": "Экспорт транзакций в CSV",
|
||||||
|
"export_viewing_key": "Экспорт ключа просмотра",
|
||||||
|
"failed_create_shielded": "Не удалось создать экранированный адрес",
|
||||||
|
"failed_create_transparent": "Не удалось создать прозрачный адрес",
|
||||||
|
"fee": "Комиссия",
|
||||||
|
"fee_high": "Высокая",
|
||||||
|
"fee_label": "Комиссия:",
|
||||||
|
"fee_low": "Низкая",
|
||||||
|
"fee_normal": "Обычная",
|
||||||
|
"fetch_prices": "Получить цены",
|
||||||
|
"file": "Файл",
|
||||||
|
"file_save_location": "Файл будет сохранён в: ~/.config/ObsidianDragon/",
|
||||||
|
"font_scale": "Масштаб шрифта",
|
||||||
|
"from": "От",
|
||||||
|
"from_upper": "ОТ",
|
||||||
|
"full_details": "Полные детали",
|
||||||
|
"general": "Общие",
|
||||||
|
"go_to_receive": "Перейти к получению",
|
||||||
|
"height": "Высота",
|
||||||
|
"help": "Справка",
|
||||||
|
"hide": "Скрыть",
|
||||||
|
"history": "История",
|
||||||
|
"immature_type": "Незрелая",
|
||||||
|
"import": "Импорт",
|
||||||
|
"import_key_btn": "Импорт ключей",
|
||||||
|
"import_key_formats": "Поддерживаемые форматы ключей:",
|
||||||
|
"import_key_full_rescan": "(0 = полное сканирование)",
|
||||||
|
"import_key_label": "Приватный ключ(и):",
|
||||||
|
"import_key_no_valid": "В введённых данных не найдено действительных ключей",
|
||||||
|
"import_key_rescan": "Пересканировать блокчейн после импорта",
|
||||||
|
"import_key_start_height": "Начальная высота:",
|
||||||
|
"import_key_success": "Ключи успешно импортированы",
|
||||||
|
"import_key_t_format": "Приватные ключи WIF для T-адресов",
|
||||||
|
"import_key_title": "Импорт приватного ключа",
|
||||||
|
"import_key_tooltip": "Введите один или несколько приватных ключей, по одному на строку.\nПоддерживаются ключи z-адресов и t-адресов.\nСтроки, начинающиеся с #, считаются комментариями.",
|
||||||
|
"import_key_warning": "Предупреждение: Никогда не делитесь своими приватными ключами! Импорт ключей из ненадёжных источников может скомпрометировать ваш кошелёк.",
|
||||||
|
"import_key_z_format": "Ключи расходования z-адресов (secret-extended-key-...)",
|
||||||
|
"import_private_key": "Импорт приватного ключа...",
|
||||||
|
"invalid_address": "Неверный формат адреса",
|
||||||
|
"ip_address": "IP-адрес",
|
||||||
|
"keep": "Сохранить",
|
||||||
|
"keep_daemon": "Оставить daemon работающим",
|
||||||
|
"key_export_fetching": "Получение ключа из кошелька...",
|
||||||
|
"key_export_private_key": "Приватный ключ:",
|
||||||
|
"key_export_private_warning": "Держите этот ключ в ТАЙНЕ! Любой, кто владеет этим ключом, может потратить ваши средства. Никогда не делитесь им в интернете или с ненадёжными лицами.",
|
||||||
|
"key_export_reveal": "Показать ключ",
|
||||||
|
"key_export_viewing_key": "Ключ просмотра:",
|
||||||
|
"key_export_viewing_warning": "Этот ключ просмотра позволяет другим видеть входящие транзакции и баланс, но НЕ тратить ваши средства. Делитесь только с доверенными лицами.",
|
||||||
|
"label": "Метка:",
|
||||||
|
"language": "Язык",
|
||||||
|
"light": "Светлая",
|
||||||
|
"loading": "Загрузка...",
|
||||||
|
"loading_addresses": "Загрузка адресов...",
|
||||||
|
"local_hashrate": "Локальный хешрейт",
|
||||||
|
"low_spec_mode": "Режим экономии",
|
||||||
|
"market": "Рынок",
|
||||||
|
"market_12h": "12ч",
|
||||||
|
"market_18h": "18ч",
|
||||||
|
"market_24h": "24ч",
|
||||||
|
"market_24h_volume": "ОБЪЁМ 24Ч",
|
||||||
|
"market_6h": "6ч",
|
||||||
|
"market_attribution": "Данные о ценах с NonKYC",
|
||||||
|
"market_btc_price": "ЦЕНА BTC",
|
||||||
|
"market_cap": "Рыночная капитализация",
|
||||||
|
"market_no_history": "Нет истории цен",
|
||||||
|
"market_no_price": "Нет данных о ценах",
|
||||||
|
"market_now": "Сейчас",
|
||||||
|
"market_pct_shielded": "%.0f%% Экранировано",
|
||||||
|
"market_portfolio": "ПОРТФЕЛЬ",
|
||||||
|
"market_price_unavailable": "Данные о ценах недоступны",
|
||||||
|
"market_refresh_price": "Обновить данные о ценах",
|
||||||
|
"market_trade_on": "Торговать на %s",
|
||||||
|
"mature": "Зрелая",
|
||||||
|
"max": "Макс",
|
||||||
|
"memo": "Заметка (необязательно, зашифровано)",
|
||||||
|
"memo_label": "Заметка:",
|
||||||
|
"memo_optional": "ЗАМЕТКА (НЕОБЯЗАТЕЛЬНО)",
|
||||||
|
"memo_upper": "ЗАМЕТКА",
|
||||||
|
"memo_z_only": "Примечание: Заметки доступны только при отправке на экранированные (z) адреса",
|
||||||
|
"merge_description": "Объедините несколько UTXO в один экранированный адрес. Это может уменьшить размер кошелька и улучшить конфиденциальность.",
|
||||||
|
"merge_funds": "Объединить средства",
|
||||||
|
"merge_started": "Операция объединения начата",
|
||||||
|
"merge_title": "Объединить на адрес",
|
||||||
|
"mine_when_idle": "Майнить в простое",
|
||||||
|
"mined": "добыто",
|
||||||
|
"mined_filter": "Добытые",
|
||||||
|
"mined_type": "Добытая",
|
||||||
|
"mined_upper": "ДОБЫТО",
|
||||||
|
"miner_fee": "Комиссия майнера",
|
||||||
|
"mining": "Майнинг",
|
||||||
|
"mining_active": "Активен",
|
||||||
|
"mining_address_copied": "Адрес майнинга скопирован",
|
||||||
|
"mining_all_time": "За всё время",
|
||||||
|
"mining_already_saved": "URL пула уже сохранён",
|
||||||
|
"mining_block_copied": "Хэш блока скопирован",
|
||||||
|
"mining_chart_1m_ago": "1м назад",
|
||||||
|
"mining_chart_5m_ago": "5м назад",
|
||||||
|
"mining_chart_now": "Сейчас",
|
||||||
|
"mining_chart_start": "Старт",
|
||||||
|
"mining_click": "Нажмите",
|
||||||
|
"mining_click_copy_address": "Нажмите, чтобы скопировать адрес",
|
||||||
|
"mining_click_copy_block": "Нажмите, чтобы скопировать хэш блока",
|
||||||
|
"mining_click_copy_difficulty": "Нажмите, чтобы скопировать сложность",
|
||||||
|
"mining_connected": "Подключено",
|
||||||
|
"mining_connecting": "Подключение...",
|
||||||
|
"mining_control": "Управление майнингом",
|
||||||
|
"mining_difficulty_copied": "Сложность скопирована",
|
||||||
|
"mining_est_block": "Расч. блок",
|
||||||
|
"mining_est_daily": "Расч. за день",
|
||||||
|
"mining_filter_all": "Все",
|
||||||
|
"mining_filter_tip_all": "Показать все доходы",
|
||||||
|
"mining_filter_tip_pool": "Показать только доходы пула",
|
||||||
|
"mining_filter_tip_solo": "Показать только доходы соло",
|
||||||
|
"mining_idle_off_tooltip": "Включить майнинг в простое",
|
||||||
|
"mining_idle_on_tooltip": "Отключить майнинг в простое",
|
||||||
|
"mining_local_hashrate": "Локальный хешрейт",
|
||||||
|
"mining_mine": "Майнить",
|
||||||
|
"mining_mining_addr": "Адрес майн.",
|
||||||
|
"mining_network": "Сеть",
|
||||||
|
"mining_no_blocks_yet": "Блоки пока не найдены",
|
||||||
|
"mining_no_payouts_yet": "Выплат пула пока нет",
|
||||||
|
"mining_no_saved_addresses": "Нет сохранённых адресов",
|
||||||
|
"mining_no_saved_pools": "Нет сохранённых пулов",
|
||||||
|
"mining_off": "Майнинг ВЫКЛЮЧЕН",
|
||||||
|
"mining_on": "Майнинг ВКЛЮЧЁН",
|
||||||
|
"mining_open_in_explorer": "Открыть в обозревателе",
|
||||||
|
"mining_payout_address": "Адрес выплат",
|
||||||
|
"mining_payout_tooltip": "Адрес для получения вознаграждений за майнинг",
|
||||||
|
"mining_pool": "Пул",
|
||||||
|
"mining_pool_hashrate": "Хешрейт пула",
|
||||||
|
"mining_pool_url": "URL пула",
|
||||||
|
"mining_recent_blocks": "ПОСЛЕДНИЕ БЛОКИ",
|
||||||
|
"mining_recent_payouts": "ПОСЛЕДНИЕ ВЫПЛАТЫ ПУЛА",
|
||||||
|
"mining_remove": "Удалить",
|
||||||
|
"mining_reset_defaults": "Сбросить настройки",
|
||||||
|
"mining_save_payout_address": "Сохранить адрес выплат",
|
||||||
|
"mining_save_pool_url": "Сохранить URL пула",
|
||||||
|
"mining_saved_addresses": "Сохранённые адреса:",
|
||||||
|
"mining_saved_pools": "Сохранённые пулы:",
|
||||||
|
"mining_shares": "Шары",
|
||||||
|
"mining_show_chart": "График",
|
||||||
|
"mining_show_log": "Журнал",
|
||||||
|
"mining_solo": "Соло",
|
||||||
|
"mining_starting": "Запуск...",
|
||||||
|
"mining_starting_tooltip": "Майнер запускается...",
|
||||||
|
"mining_statistics": "Статистика майнинга",
|
||||||
|
"mining_stop": "Стоп",
|
||||||
|
"mining_stop_solo_for_pool": "Остановите соло-майнинг перед запуском пул-майнинга",
|
||||||
|
"mining_stop_solo_for_pool_settings": "Остановите соло-майнинг для изменения настроек пула",
|
||||||
|
"mining_stopping": "Остановка...",
|
||||||
|
"mining_stopping_tooltip": "Майнер останавливается...",
|
||||||
|
"mining_syncing_tooltip": "Блокчейн синхронизируется...",
|
||||||
|
"mining_threads": "Потоки майнинга",
|
||||||
|
"mining_to_save": "для сохранения",
|
||||||
|
"mining_today": "Сегодня",
|
||||||
|
"mining_uptime": "Время работы",
|
||||||
|
"mining_yesterday": "Вчера",
|
||||||
|
"network": "Сеть",
|
||||||
|
"network_fee": "СЕТЕВАЯ КОМИССИЯ",
|
||||||
|
"network_hashrate": "Хешрейт сети",
|
||||||
|
"new": "+ Новый",
|
||||||
|
"new_shielded_created": "Создан новый экранированный адрес",
|
||||||
|
"new_t_address": "Новый T-адрес",
|
||||||
|
"new_t_transparent": "Новый t-адрес (Прозрачный)",
|
||||||
|
"new_transparent_created": "Создан новый прозрачный адрес",
|
||||||
|
"new_z_address": "Новый Z-адрес",
|
||||||
|
"new_z_shielded": "Новый z-адрес (Экранированный)",
|
||||||
|
"no_addresses": "Адреса не найдены. Создайте один, используя кнопки выше.",
|
||||||
|
"no_addresses_available": "Нет доступных адресов",
|
||||||
|
"no_addresses_match": "Нет адресов, соответствующих фильтру",
|
||||||
|
"no_addresses_with_balance": "Нет адресов с балансом",
|
||||||
|
"no_matching": "Нет подходящих транзакций",
|
||||||
|
"no_recent_receives": "Нет недавних получений",
|
||||||
|
"no_recent_sends": "Нет недавних отправлений",
|
||||||
|
"no_transactions": "Транзакции не найдены",
|
||||||
|
"node": "УЗЕЛ",
|
||||||
|
"node_security": "УЗЕЛ И БЕЗОПАСНОСТЬ",
|
||||||
|
"noise": "Шум",
|
||||||
|
"not_connected": "Не подключено к daemon...",
|
||||||
|
"not_connected_to_daemon": "Не подключено к daemon",
|
||||||
|
"notes": "Заметки",
|
||||||
|
"notes_optional": "Заметки (необязательно):",
|
||||||
|
"output_filename": "Имя выходного файла:",
|
||||||
|
"overview": "Обзор",
|
||||||
|
"paste": "Вставить",
|
||||||
|
"paste_from_clipboard": "Вставить из буфера обмена",
|
||||||
|
"pay_from": "Оплатить с",
|
||||||
|
"payment_request": "ЗАПРОС НА ОПЛАТУ",
|
||||||
|
"payment_request_copied": "Запрос на оплату скопирован",
|
||||||
|
"payment_uri_copied": "URI платежа скопирован",
|
||||||
|
"peers": "Узлы",
|
||||||
|
"peers_avg_ping": "Средний пинг",
|
||||||
|
"peers_ban_24h": "Заблокировать узел на 24ч",
|
||||||
|
"peers_ban_score": "Очки блокировки: %d",
|
||||||
|
"peers_banned": "Заблокированные",
|
||||||
|
"peers_banned_count": "Заблокировано: %d",
|
||||||
|
"peers_best_block": "Лучший блок",
|
||||||
|
"peers_blockchain": "БЛОКЧЕЙН",
|
||||||
|
"peers_blocks": "Блоки",
|
||||||
|
"peers_blocks_left": "Осталось %d блоков",
|
||||||
|
"peers_clear_all_bans": "Снять все блокировки",
|
||||||
|
"peers_click_copy": "Нажмите, чтобы скопировать",
|
||||||
|
"peers_connected": "Подключено",
|
||||||
|
"peers_connected_count": "Подключено: %d",
|
||||||
|
"peers_copy_ip": "Копировать IP",
|
||||||
|
"peers_dir_in": "Вх.",
|
||||||
|
"peers_dir_out": "Исх.",
|
||||||
|
"peers_hash_copied": "Хэш скопирован",
|
||||||
|
"peers_hashrate": "Хешрейт",
|
||||||
|
"peers_in_out": "Вх./Исх.",
|
||||||
|
"peers_longest": "Длиннейшая",
|
||||||
|
"peers_longest_chain": "Длиннейшая цепь",
|
||||||
|
"peers_memory": "Память",
|
||||||
|
"peers_no_banned": "Нет заблокированных узлов",
|
||||||
|
"peers_no_connected": "Нет подключённых узлов",
|
||||||
|
"peers_no_tls": "Без TLS",
|
||||||
|
"peers_notarized": "Нотаризован",
|
||||||
|
"peers_p2p_port": "P2P-порт",
|
||||||
|
"peers_protocol": "Протокол",
|
||||||
|
"peers_received": "Получено",
|
||||||
|
"peers_refresh": "Обновить",
|
||||||
|
"peers_refresh_tooltip": "Обновить список узлов",
|
||||||
|
"peers_refreshing": "Обновление...",
|
||||||
|
"peers_sent": "Отправлено",
|
||||||
|
"peers_tt_id": "ID: %d",
|
||||||
|
"peers_tt_received": "Получено: %s",
|
||||||
|
"peers_tt_sent": "Отправлено: %s",
|
||||||
|
"peers_tt_services": "Сервисы: %s",
|
||||||
|
"peers_tt_start_height": "Начальная высота: %d",
|
||||||
|
"peers_tt_synced": "Синхронизировано В/Б: %d/%d",
|
||||||
|
"peers_tt_tls_cipher": "TLS: %s",
|
||||||
|
"peers_unban": "Разблокировать",
|
||||||
|
"peers_upper": "УЗЛЫ",
|
||||||
|
"peers_version": "Версия",
|
||||||
|
"pending": "Ожидание",
|
||||||
|
"ping": "Пинг",
|
||||||
|
"price_chart": "График цен",
|
||||||
|
"qr_code": "QR-код",
|
||||||
|
"qr_failed": "Не удалось сгенерировать QR-код",
|
||||||
|
"qr_title": "QR-код",
|
||||||
|
"qr_unavailable": "QR недоступен",
|
||||||
|
"receive": "Получить",
|
||||||
|
"received": "получено",
|
||||||
|
"received_filter": "Получено",
|
||||||
|
"received_label": "Получено",
|
||||||
|
"received_upper": "ПОЛУЧЕНО",
|
||||||
|
"receiving_addresses": "Ваши адреса для получения",
|
||||||
|
"recent_received": "НЕДАВНО ПОЛУЧЕНО",
|
||||||
|
"recent_sends": "НЕДАВНО ОТПРАВЛЕНО",
|
||||||
|
"recipient": "ПОЛУЧАТЕЛЬ",
|
||||||
|
"recv_type": "Получ.",
|
||||||
|
"refresh": "Обновить",
|
||||||
|
"refresh_now": "Обновить сейчас",
|
||||||
|
"report_bug": "Сообщить об ошибке",
|
||||||
|
"request_amount": "Сумма (необязательно):",
|
||||||
|
"request_copy_uri": "Копировать URI",
|
||||||
|
"request_description": "Создайте запрос на оплату, который другие могут отсканировать или скопировать. QR-код содержит ваш адрес и опциональную сумму/заметку.",
|
||||||
|
"request_label": "Метка (необязательно):",
|
||||||
|
"request_memo": "Заметка (необязательно):",
|
||||||
|
"request_payment": "Запрос оплаты",
|
||||||
|
"request_payment_uri": "URI платежа:",
|
||||||
|
"request_receive_address": "Адрес получения:",
|
||||||
|
"request_select_address": "Выбрать адрес...",
|
||||||
|
"request_shielded_addrs": "-- Экранированные адреса --",
|
||||||
|
"request_title": "Запрос оплаты",
|
||||||
|
"request_transparent_addrs": "-- Прозрачные адреса --",
|
||||||
|
"request_uri_copied": "URI платежа скопирован в буфер обмена",
|
||||||
|
"rescan": "Пересканировать",
|
||||||
|
"reset_to_defaults": "Сбросить настройки",
|
||||||
|
"review_send": "Проверить отправку",
|
||||||
|
"rpc_host": "RPC-хост",
|
||||||
|
"rpc_pass": "Пароль",
|
||||||
|
"rpc_port": "Порт",
|
||||||
|
"rpc_user": "Имя пользователя",
|
||||||
|
"save": "Сохранить",
|
||||||
|
"save_settings": "Сохранить настройки",
|
||||||
|
"save_z_transactions": "Сохранять Z-tx в списке транзакций",
|
||||||
|
"search_placeholder": "Поиск...",
|
||||||
|
"security": "БЕЗОПАСНОСТЬ",
|
||||||
|
"select_address": "Выбрать адрес...",
|
||||||
|
"select_receiving_address": "Выбрать адрес получения...",
|
||||||
|
"select_source_address": "Выбрать адрес-источник...",
|
||||||
|
"send": "Отправить",
|
||||||
|
"send_amount": "Сумма",
|
||||||
|
"send_amount_details": "ДЕТАЛИ СУММЫ",
|
||||||
|
"send_amount_upper": "СУММА",
|
||||||
|
"send_clear_fields": "Очистить все поля формы?",
|
||||||
|
"send_copy_error": "Копировать ошибку",
|
||||||
|
"send_dismiss": "Отклонить",
|
||||||
|
"send_error_copied": "Ошибка скопирована в буфер обмена",
|
||||||
|
"send_error_prefix": "Ошибка: %s",
|
||||||
|
"send_exceeds_available": "Превышает доступное (%.8f)",
|
||||||
|
"send_fee": "Комиссия",
|
||||||
|
"send_fee_high": "Высокая",
|
||||||
|
"send_fee_low": "Низкая",
|
||||||
|
"send_fee_normal": "Обычная",
|
||||||
|
"send_form_restored": "Форма восстановлена",
|
||||||
|
"send_from_this_address": "Отправить с этого адреса",
|
||||||
|
"send_go_to_receive": "Перейти к получению",
|
||||||
|
"send_keep": "Сохранить",
|
||||||
|
"send_network_fee": "СЕТЕВАЯ КОМИССИЯ",
|
||||||
|
"send_no_balance": "Нет баланса",
|
||||||
|
"send_no_recent": "Нет недавних отправлений",
|
||||||
|
"send_recent_sends": "НЕДАВНО ОТПРАВЛЕНО",
|
||||||
|
"send_recipient": "ПОЛУЧАТЕЛЬ",
|
||||||
|
"send_select_source": "Выбрать адрес-источник...",
|
||||||
|
"send_sending_from": "ОТПРАВКА С",
|
||||||
|
"send_submitting": "Отправка транзакции...",
|
||||||
|
"send_switch_to_receive": "Перейдите к получению, чтобы получить свой адрес и начать получать средства.",
|
||||||
|
"send_to": "Отправить на",
|
||||||
|
"send_tooltip_enter_amount": "Введите сумму для отправки",
|
||||||
|
"send_tooltip_exceeds_balance": "Сумма превышает доступный баланс",
|
||||||
|
"send_tooltip_in_progress": "Транзакция уже выполняется",
|
||||||
|
"send_tooltip_invalid_address": "Введите действительный адрес получателя",
|
||||||
|
"send_tooltip_not_connected": "Не подключено к daemon",
|
||||||
|
"send_tooltip_select_source": "Сначала выберите адрес-источник",
|
||||||
|
"send_tooltip_syncing": "Дождитесь синхронизации блокчейна",
|
||||||
|
"send_total": "Итого",
|
||||||
|
"send_transaction": "Отправить транзакцию",
|
||||||
|
"send_tx_failed": "Транзакция не удалась",
|
||||||
|
"send_tx_sent": "Транзакция отправлена!",
|
||||||
|
"send_tx_success": "Транзакция успешно отправлена!",
|
||||||
|
"send_txid_copied": "TxID скопирован в буфер обмена",
|
||||||
|
"send_txid_label": "TxID: %s",
|
||||||
|
"send_valid_shielded": "Действительный экранированный адрес",
|
||||||
|
"send_valid_transparent": "Действительный прозрачный адрес",
|
||||||
|
"send_wallet_empty": "Ваш кошелёк пуст",
|
||||||
|
"send_yes_clear": "Да, очистить",
|
||||||
|
"sending": "Отправка транзакции",
|
||||||
|
"sending_from": "ОТПРАВКА С",
|
||||||
|
"sent": "отправлено",
|
||||||
|
"sent_filter": "Отправлено",
|
||||||
|
"sent_type": "Отправлено",
|
||||||
|
"sent_upper": "ОТПРАВЛЕНО",
|
||||||
|
"settings": "Настройки",
|
||||||
|
"setup_wizard": "Мастер настройки",
|
||||||
|
"share": "Поделиться",
|
||||||
|
"shield_check_status": "Проверить статус",
|
||||||
|
"shield_completed": "Операция успешно завершена!",
|
||||||
|
"shield_description": "Экранируйте вознаграждения за майнинг, отправив coinbase-выходы с прозрачных адресов на экранированный адрес. Это улучшает конфиденциальность, скрывая ваш доход от майнинга.",
|
||||||
|
"shield_from_address": "С адреса:",
|
||||||
|
"shield_funds": "Экранировать средства",
|
||||||
|
"shield_in_progress": "Операция выполняется...",
|
||||||
|
"shield_max_utxos": "Макс. UTXO за операцию",
|
||||||
|
"shield_merge_done": "Экранирование/объединение завершено!",
|
||||||
|
"shield_select_z": "Выбрать z-адрес...",
|
||||||
|
"shield_started": "Операция экранирования начата",
|
||||||
|
"shield_title": "Экранировать вознаграждения coinbase",
|
||||||
|
"shield_to_address": "На адрес (экранированный):",
|
||||||
|
"shield_utxo_limit": "Лимит UTXO:",
|
||||||
|
"shield_wildcard_hint": "Используйте '*' для экранирования со всех прозрачных адресов",
|
||||||
|
"shielded": "Экранированный",
|
||||||
|
"shielded_to": "ЭКРАНИРОВАНО НА",
|
||||||
|
"shielded_type": "Экранированный",
|
||||||
|
"show": "Показать",
|
||||||
|
"show_qr_code": "Показать QR-код",
|
||||||
|
"showing_transactions": "Показано %d\xe2\x80\x93%d из %d транзакций (всего: %zu)",
|
||||||
|
"simple_background": "Простой фон",
|
||||||
|
"start_mining": "Начать майнинг",
|
||||||
|
"status": "Статус",
|
||||||
|
"stop_external": "Остановить внешний daemon",
|
||||||
|
"stop_mining": "Остановить майнинг",
|
||||||
|
"submitting_transaction": "Отправка транзакции...",
|
||||||
|
"success": "Успешно",
|
||||||
|
"summary": "Итоги",
|
||||||
|
"syncing": "Синхронизация...",
|
||||||
|
"t_addresses": "T-адреса",
|
||||||
|
"test_connection": "Тест",
|
||||||
|
"theme": "Тема",
|
||||||
|
"theme_effects": "Эффекты темы",
|
||||||
|
"time_days_ago": "%d дней назад",
|
||||||
|
"time_hours_ago": "%d часов назад",
|
||||||
|
"time_minutes_ago": "%d минут назад",
|
||||||
|
"time_seconds_ago": "%d секунд назад",
|
||||||
|
"to": "Кому",
|
||||||
|
"to_upper": "КОМУ",
|
||||||
|
"tools": "ИНСТРУМЕНТЫ",
|
||||||
|
"total": "Итого",
|
||||||
|
"transaction_id": "ID ТРАНЗАКЦИИ",
|
||||||
|
"transaction_sent": "Транзакция успешно отправлена",
|
||||||
|
"transaction_sent_msg": "Транзакция отправлена!",
|
||||||
|
"transaction_url": "URL транзакции",
|
||||||
|
"transactions": "Транзакции",
|
||||||
|
"transactions_upper": "ТРАНЗАКЦИИ",
|
||||||
|
"transparent": "Прозрачный",
|
||||||
|
"tx_confirmations": "%d подтверждений",
|
||||||
|
"tx_details_title": "Детали транзакции",
|
||||||
|
"tx_from_address": "Адрес отправителя:",
|
||||||
|
"tx_id_label": "ID транзакции:",
|
||||||
|
"tx_immature": "НЕЗРЕЛАЯ",
|
||||||
|
"tx_mined": "ДОБЫТА",
|
||||||
|
"tx_received": "ПОЛУЧЕНО",
|
||||||
|
"tx_sent": "ОТПРАВЛЕНО",
|
||||||
|
"tx_to_address": "Адрес получателя:",
|
||||||
|
"tx_view_explorer": "Посмотреть в обозревателе",
|
||||||
|
"txs_count": "%d тр.",
|
||||||
|
"type": "Тип",
|
||||||
|
"ui_opacity": "Прозрачность интерфейса",
|
||||||
|
"unban": "Разблокировать",
|
||||||
|
"unconfirmed": "Не подтверждено",
|
||||||
|
"undo_clear": "Отменить очистку",
|
||||||
|
"unknown": "Неизвестно",
|
||||||
|
"use_embedded_daemon": "Использовать встроенный dragonxd",
|
||||||
|
"use_tor": "Использовать Tor",
|
||||||
|
"validate_btn": "Проверить",
|
||||||
|
"validate_description": "Введите адрес DragonX, чтобы проверить его действительность и принадлежность к этому кошельку.",
|
||||||
|
"validate_invalid": "НЕДЕЙСТВИТЕЛЕН",
|
||||||
|
"validate_is_mine": "Этот кошелёк владеет этим адресом",
|
||||||
|
"validate_not_mine": "Не принадлежит этому кошельку",
|
||||||
|
"validate_ownership": "Принадлежность:",
|
||||||
|
"validate_results": "Результаты:",
|
||||||
|
"validate_shielded_type": "Экранированный (z-адрес)",
|
||||||
|
"validate_status": "Статус:",
|
||||||
|
"validate_title": "Проверить адрес",
|
||||||
|
"validate_transparent_type": "Прозрачный (t-адрес)",
|
||||||
|
"validate_type": "Тип:",
|
||||||
|
"validate_valid": "ДЕЙСТВИТЕЛЕН",
|
||||||
|
"validating": "Проверка...",
|
||||||
|
"verbose_logging": "Подробное логирование",
|
||||||
|
"version": "Версия",
|
||||||
|
"view": "Просмотр",
|
||||||
|
"view_details": "Подробнее",
|
||||||
|
"view_on_explorer": "Посмотреть в обозревателе",
|
||||||
|
"waiting_for_daemon": "Ожидание подключения к daemon...",
|
||||||
|
"wallet": "КОШЕЛЁК",
|
||||||
|
"wallet_empty": "Ваш кошелёк пуст",
|
||||||
|
"wallet_empty_hint": "Перейдите к получению, чтобы получить свой адрес и начать получать средства.",
|
||||||
|
"warning": "Предупреждение",
|
||||||
|
"warning_upper": "ПРЕДУПРЕЖДЕНИЕ!",
|
||||||
|
"website": "Веб-сайт",
|
||||||
|
"window_opacity": "Прозрачность окна",
|
||||||
|
"yes_clear": "Да, очистить",
|
||||||
|
"your_addresses": "Ваши адреса",
|
||||||
|
"z_addresses": "Z-адреса",
|
||||||
|
}
|
||||||
|
|
||||||
|
out = os.path.join(os.path.dirname(__file__), "..", "res", "lang", "ru.json")
|
||||||
|
with open(out, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(translations, f, indent=4, ensure_ascii=False, sort_keys=True)
|
||||||
|
print(f"Wrote {len(translations)} Russian translations to {os.path.abspath(out)}")
|
||||||
646
scripts/gen_zh.py
Normal file
@@ -0,0 +1,646 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Generate Chinese Simplified (zh) translations for ObsidianDragon wallet."""
|
||||||
|
import json, os
|
||||||
|
|
||||||
|
translations = {
|
||||||
|
"24h_change": "24小时变化",
|
||||||
|
"24h_volume": "24小时交易量",
|
||||||
|
"about": "关于",
|
||||||
|
"about_block_explorer": "区块浏览器",
|
||||||
|
"about_block_height": "区块高度:",
|
||||||
|
"about_build_date": "构建日期:",
|
||||||
|
"about_build_type": "构建类型:",
|
||||||
|
"about_chain": "链:",
|
||||||
|
"about_connections": "连接数:",
|
||||||
|
"about_credits": "致谢",
|
||||||
|
"about_daemon": "守护进程:",
|
||||||
|
"about_debug": "调试",
|
||||||
|
"about_dragonx": "关于 ObsidianDragon",
|
||||||
|
"about_edition": "ImGui 版本",
|
||||||
|
"about_github": "GitHub",
|
||||||
|
"about_imgui": "ImGui:",
|
||||||
|
"about_license": "许可证",
|
||||||
|
"about_license_text": "本软件根据 GNU 通用公共许可证 v3 (GPLv3) 发布。您可以根据许可证条款自由使用、修改和分发本软件。",
|
||||||
|
"about_peers_count": "%zu 个节点",
|
||||||
|
"about_release": "发布版",
|
||||||
|
"about_title": "关于 ObsidianDragon",
|
||||||
|
"about_version": "版本:",
|
||||||
|
"about_website": "网站",
|
||||||
|
"acrylic": "亚克力",
|
||||||
|
"add": "添加",
|
||||||
|
"address": "地址",
|
||||||
|
"address_book_add": "添加地址",
|
||||||
|
"address_book_add_new": "添加新地址",
|
||||||
|
"address_book_added": "地址已添加到通讯录",
|
||||||
|
"address_book_count": "已保存 %zu 个地址",
|
||||||
|
"address_book_deleted": "条目已删除",
|
||||||
|
"address_book_edit": "编辑地址",
|
||||||
|
"address_book_empty": "没有保存的地址。点击'添加新地址'创建一个。",
|
||||||
|
"address_book_exists": "地址已存在于通讯录中",
|
||||||
|
"address_book_title": "地址簿",
|
||||||
|
"address_book_update_failed": "更新失败——地址可能重复",
|
||||||
|
"address_book_updated": "地址已更新",
|
||||||
|
"address_copied": "地址已复制到剪贴板",
|
||||||
|
"address_details": "地址详情",
|
||||||
|
"address_label": "地址:",
|
||||||
|
"address_upper": "地址",
|
||||||
|
"address_url": "地址 URL",
|
||||||
|
"addresses_appear_here": "连接后,您的接收地址将显示在此处。",
|
||||||
|
"advanced": "高级",
|
||||||
|
"all_filter": "全部",
|
||||||
|
"allow_custom_fees": "允许自定义手续费",
|
||||||
|
"amount": "金额",
|
||||||
|
"amount_details": "金额详情",
|
||||||
|
"amount_exceeds_balance": "金额超过余额",
|
||||||
|
"amount_label": "金额:",
|
||||||
|
"appearance": "外观",
|
||||||
|
"auto_shield": "自动屏蔽挖矿",
|
||||||
|
"available": "可用",
|
||||||
|
"backup_backing_up": "正在备份...",
|
||||||
|
"backup_create": "创建备份",
|
||||||
|
"backup_created": "钱包备份已创建",
|
||||||
|
"backup_data": "备份与数据",
|
||||||
|
"backup_description": "创建 wallet.dat 文件的备份。此文件包含您所有的私钥和交易历史。请将备份存放在安全的地方。",
|
||||||
|
"backup_destination": "备份目标:",
|
||||||
|
"backup_tip_external": "将备份存储在外部驱动器或云存储中",
|
||||||
|
"backup_tip_multiple": "在不同位置创建多个备份",
|
||||||
|
"backup_tip_test": "定期测试从备份恢复",
|
||||||
|
"backup_tips": "提示:",
|
||||||
|
"backup_title": "备份钱包",
|
||||||
|
"backup_wallet": "备份钱包...",
|
||||||
|
"backup_wallet_not_found": "警告:在预期位置未找到 wallet.dat",
|
||||||
|
"balance": "余额",
|
||||||
|
"balance_layout": "余额布局",
|
||||||
|
"ban": "封禁",
|
||||||
|
"banned_peers": "已封禁节点",
|
||||||
|
"block": "区块",
|
||||||
|
"block_bits": "比特:",
|
||||||
|
"block_click_next": "点击查看下一个区块",
|
||||||
|
"block_click_prev": "点击查看上一个区块",
|
||||||
|
"block_explorer": "区块浏览器",
|
||||||
|
"block_get_info": "获取区块信息",
|
||||||
|
"block_hash": "区块哈希:",
|
||||||
|
"block_height": "区块高度:",
|
||||||
|
"block_info_title": "区块信息",
|
||||||
|
"block_merkle_root": "默克尔根:",
|
||||||
|
"block_nav_next": "下一个 >>",
|
||||||
|
"block_nav_prev": "<< 上一个",
|
||||||
|
"block_next": "下一个区块:",
|
||||||
|
"block_previous": "上一个区块:",
|
||||||
|
"block_size": "大小:",
|
||||||
|
"block_timestamp": "时间戳:",
|
||||||
|
"block_transactions": "交易:",
|
||||||
|
"blockchain_syncing": "区块链同步中 (%.1f%%)... 余额可能不准确。",
|
||||||
|
"cancel": "取消",
|
||||||
|
"characters": "字符",
|
||||||
|
"clear": "清除",
|
||||||
|
"clear_all_bans": "解除所有封禁",
|
||||||
|
"clear_form_confirm": "清除所有表单字段?",
|
||||||
|
"clear_request": "清除请求",
|
||||||
|
"click_copy_address": "点击复制地址",
|
||||||
|
"click_copy_uri": "点击复制 URI",
|
||||||
|
"close": "关闭",
|
||||||
|
"conf_count": "%d 确认",
|
||||||
|
"confirm_and_send": "确认并发送",
|
||||||
|
"confirm_send": "确认发送",
|
||||||
|
"confirm_transaction": "确认交易",
|
||||||
|
"confirmations": "确认数",
|
||||||
|
"confirmations_display": "%d 次确认 | %s",
|
||||||
|
"confirmed": "已确认",
|
||||||
|
"connected": "已连接",
|
||||||
|
"connected_peers": "已连接节点",
|
||||||
|
"connecting": "连接中...",
|
||||||
|
"console": "控制台",
|
||||||
|
"console_auto_scroll": "自动滚动",
|
||||||
|
"console_available_commands": "可用命令:",
|
||||||
|
"console_capturing_output": "正在捕获守护进程输出...",
|
||||||
|
"console_clear": "清除",
|
||||||
|
"console_clear_console": "清除控制台",
|
||||||
|
"console_cleared": "控制台已清除",
|
||||||
|
"console_click_commands": "点击上方命令以插入",
|
||||||
|
"console_click_insert": "点击插入",
|
||||||
|
"console_click_insert_params": "点击插入(含参数)",
|
||||||
|
"console_close": "关闭",
|
||||||
|
"console_commands": "命令",
|
||||||
|
"console_common_rpc": "常用 RPC 命令:",
|
||||||
|
"console_completions": "补全:",
|
||||||
|
"console_connected": "已连接到守护进程",
|
||||||
|
"console_copy_all": "全部复制",
|
||||||
|
"console_copy_selected": "复制",
|
||||||
|
"console_daemon": "守护进程",
|
||||||
|
"console_daemon_error": "守护进程错误!",
|
||||||
|
"console_daemon_started": "守护进程已启动",
|
||||||
|
"console_daemon_stopped": "守护进程已停止",
|
||||||
|
"console_disconnected": "已断开与守护进程的连接",
|
||||||
|
"console_errors": "错误",
|
||||||
|
"console_filter_hint": "过滤输出...",
|
||||||
|
"console_help_clear": " clear - 清除控制台",
|
||||||
|
"console_help_getbalance": " getbalance - 显示透明余额",
|
||||||
|
"console_help_getblockcount": " getblockcount - 显示当前区块高度",
|
||||||
|
"console_help_getinfo": " getinfo - 显示节点信息",
|
||||||
|
"console_help_getmininginfo": " getmininginfo - 显示挖矿状态",
|
||||||
|
"console_help_getpeerinfo": " getpeerinfo - 显示已连接节点",
|
||||||
|
"console_help_gettotalbalance": " gettotalbalance - 显示总余额",
|
||||||
|
"console_help_help": " help - 显示此帮助信息",
|
||||||
|
"console_help_setgenerate": " setgenerate - 控制挖矿",
|
||||||
|
"console_help_stop": " stop - 停止守护进程",
|
||||||
|
"console_line_count": "%zu 行",
|
||||||
|
"console_new_lines": "%d 新行",
|
||||||
|
"console_no_daemon": "无守护进程",
|
||||||
|
"console_not_connected": "错误:未连接到守护进程",
|
||||||
|
"console_rpc_reference": "RPC 命令参考",
|
||||||
|
"console_scanline": "控制台扫描线",
|
||||||
|
"console_search_commands": "搜索命令...",
|
||||||
|
"console_select_all": "全选",
|
||||||
|
"console_show_daemon_output": "显示守护进程输出",
|
||||||
|
"console_show_errors_only": "仅显示错误",
|
||||||
|
"console_show_rpc_ref": "显示 RPC 命令参考",
|
||||||
|
"console_showing_lines": "显示 %zu / %zu 行",
|
||||||
|
"console_starting_node": "正在启动节点...",
|
||||||
|
"console_status_error": "错误",
|
||||||
|
"console_status_running": "运行中",
|
||||||
|
"console_status_starting": "启动中",
|
||||||
|
"console_status_stopped": "已停止",
|
||||||
|
"console_status_stopping": "停止中",
|
||||||
|
"console_status_unknown": "未知",
|
||||||
|
"console_tab_completion": "Tab 补全",
|
||||||
|
"console_type_help": "输入 'help' 查看可用命令",
|
||||||
|
"console_welcome": "欢迎使用 ObsidianDragon 控制台",
|
||||||
|
"console_zoom_in": "放大",
|
||||||
|
"console_zoom_out": "缩小",
|
||||||
|
"copy": "复制",
|
||||||
|
"copy_address": "复制完整地址",
|
||||||
|
"copy_error": "复制错误",
|
||||||
|
"copy_to_clipboard": "复制到剪贴板",
|
||||||
|
"copy_txid": "复制交易ID",
|
||||||
|
"copy_uri": "复制 URI",
|
||||||
|
"current_price": "当前价格",
|
||||||
|
"custom_fees": "自定义手续费",
|
||||||
|
"dark": "深色",
|
||||||
|
"date": "日期",
|
||||||
|
"date_label": "日期:",
|
||||||
|
"delete": "删除",
|
||||||
|
"difficulty": "难度",
|
||||||
|
"disconnected": "已断开",
|
||||||
|
"dismiss": "关闭",
|
||||||
|
"display": "显示",
|
||||||
|
"dragonx_green": "DragonX(绿色)",
|
||||||
|
"edit": "编辑",
|
||||||
|
"error": "错误",
|
||||||
|
"est_time_to_block": "预计出块时间",
|
||||||
|
"exit": "退出",
|
||||||
|
"explorer": "浏览器",
|
||||||
|
"export": "导出",
|
||||||
|
"export_csv": "导出 CSV",
|
||||||
|
"export_keys_btn": "导出密钥",
|
||||||
|
"export_keys_danger": "危险:这将导出您钱包中的所有私钥!任何获得此文件的人都可以窃取您的资金。请安全保管并在使用后删除。",
|
||||||
|
"export_keys_include_t": "包含 T 地址(透明)",
|
||||||
|
"export_keys_include_z": "包含 Z 地址(屏蔽)",
|
||||||
|
"export_keys_options": "导出选项:",
|
||||||
|
"export_keys_success": "密钥导出成功",
|
||||||
|
"export_keys_title": "导出所有私钥",
|
||||||
|
"export_private_key": "导出私钥",
|
||||||
|
"export_tx_count": "导出 %zu 笔交易到 CSV 文件。",
|
||||||
|
"export_tx_file_fail": "无法创建 CSV 文件",
|
||||||
|
"export_tx_none": "没有交易可导出",
|
||||||
|
"export_tx_success": "交易导出成功",
|
||||||
|
"export_tx_title": "导出交易到 CSV",
|
||||||
|
"export_viewing_key": "导出查看密钥",
|
||||||
|
"failed_create_shielded": "无法创建屏蔽地址",
|
||||||
|
"failed_create_transparent": "无法创建透明地址",
|
||||||
|
"fee": "手续费",
|
||||||
|
"fee_high": "高",
|
||||||
|
"fee_label": "手续费:",
|
||||||
|
"fee_low": "低",
|
||||||
|
"fee_normal": "普通",
|
||||||
|
"fetch_prices": "获取价格",
|
||||||
|
"file": "文件",
|
||||||
|
"file_save_location": "文件将保存至:~/.config/ObsidianDragon/",
|
||||||
|
"font_scale": "字体大小",
|
||||||
|
"from": "从",
|
||||||
|
"from_upper": "从",
|
||||||
|
"full_details": "完整详情",
|
||||||
|
"general": "常规",
|
||||||
|
"go_to_receive": "前往接收",
|
||||||
|
"height": "高度",
|
||||||
|
"help": "帮助",
|
||||||
|
"hide": "隐藏",
|
||||||
|
"history": "历史",
|
||||||
|
"immature_type": "未成熟",
|
||||||
|
"import": "导入",
|
||||||
|
"import_key_btn": "导入密钥",
|
||||||
|
"import_key_formats": "支持的密钥格式:",
|
||||||
|
"import_key_full_rescan": "(0 = 完整重扫)",
|
||||||
|
"import_key_label": "私钥:",
|
||||||
|
"import_key_no_valid": "输入中未找到有效密钥",
|
||||||
|
"import_key_rescan": "导入后重新扫描区块链",
|
||||||
|
"import_key_start_height": "起始高度:",
|
||||||
|
"import_key_success": "密钥导入成功",
|
||||||
|
"import_key_t_format": "T 地址 WIF 私钥",
|
||||||
|
"import_key_title": "导入私钥",
|
||||||
|
"import_key_tooltip": "输入一个或多个私钥,每行一个。\n支持 z 地址和 t 地址密钥。\n以 # 开头的行视为注释。",
|
||||||
|
"import_key_warning": "警告:切勿分享您的私钥!从不可信来源导入密钥可能会危及您的钱包安全。",
|
||||||
|
"import_key_z_format": "Z 地址花费密钥 (secret-extended-key-...)",
|
||||||
|
"import_private_key": "导入私钥...",
|
||||||
|
"invalid_address": "无效的地址格式",
|
||||||
|
"ip_address": "IP 地址",
|
||||||
|
"keep": "保留",
|
||||||
|
"keep_daemon": "保持守护进程运行",
|
||||||
|
"key_export_fetching": "正在从钱包获取密钥...",
|
||||||
|
"key_export_private_key": "私钥:",
|
||||||
|
"key_export_private_warning": "请保密此密钥!任何拥有此密钥的人都可以花费您的资金。切勿在网上或与不可信的人分享。",
|
||||||
|
"key_export_reveal": "显示密钥",
|
||||||
|
"key_export_viewing_key": "查看密钥:",
|
||||||
|
"key_export_viewing_warning": "此查看密钥允许他人查看您的入账交易和余额,但不能花费您的资金。仅与信任的人分享。",
|
||||||
|
"label": "标签:",
|
||||||
|
"language": "语言",
|
||||||
|
"light": "浅色",
|
||||||
|
"loading": "加载中...",
|
||||||
|
"loading_addresses": "正在加载地址...",
|
||||||
|
"local_hashrate": "本地算力",
|
||||||
|
"low_spec_mode": "低配模式",
|
||||||
|
"market": "市场",
|
||||||
|
"market_12h": "12小时",
|
||||||
|
"market_18h": "18小时",
|
||||||
|
"market_24h": "24小时",
|
||||||
|
"market_24h_volume": "24小时交易量",
|
||||||
|
"market_6h": "6小时",
|
||||||
|
"market_attribution": "价格数据来自 NonKYC",
|
||||||
|
"market_btc_price": "BTC 价格",
|
||||||
|
"market_cap": "市值",
|
||||||
|
"market_no_history": "无价格历史",
|
||||||
|
"market_no_price": "无价格数据",
|
||||||
|
"market_now": "现在",
|
||||||
|
"market_pct_shielded": "%.0f%% 屏蔽",
|
||||||
|
"market_portfolio": "投资组合",
|
||||||
|
"market_price_unavailable": "价格数据不可用",
|
||||||
|
"market_refresh_price": "刷新价格数据",
|
||||||
|
"market_trade_on": "在 %s 交易",
|
||||||
|
"mature": "已成熟",
|
||||||
|
"max": "最大",
|
||||||
|
"memo": "备注(可选,加密)",
|
||||||
|
"memo_label": "备注:",
|
||||||
|
"memo_optional": "备注(可选)",
|
||||||
|
"memo_upper": "备注",
|
||||||
|
"memo_z_only": "注意:备注仅在发送到屏蔽 (z) 地址时可用",
|
||||||
|
"merge_description": "将多个 UTXO 合并到一个屏蔽地址。这可以帮助减小钱包大小并提高隐私性。",
|
||||||
|
"merge_funds": "合并资金",
|
||||||
|
"merge_started": "合并操作已开始",
|
||||||
|
"merge_title": "合并到地址",
|
||||||
|
"mine_when_idle": "空闲时挖矿",
|
||||||
|
"mined": "已挖得",
|
||||||
|
"mined_filter": "已挖得",
|
||||||
|
"mined_type": "已挖得",
|
||||||
|
"mined_upper": "已挖得",
|
||||||
|
"miner_fee": "矿工费",
|
||||||
|
"mining": "挖矿",
|
||||||
|
"mining_active": "活跃",
|
||||||
|
"mining_address_copied": "挖矿地址已复制",
|
||||||
|
"mining_all_time": "所有时间",
|
||||||
|
"mining_already_saved": "矿池 URL 已保存",
|
||||||
|
"mining_block_copied": "区块哈希已复制",
|
||||||
|
"mining_chart_1m_ago": "1分钟前",
|
||||||
|
"mining_chart_5m_ago": "5分钟前",
|
||||||
|
"mining_chart_now": "现在",
|
||||||
|
"mining_chart_start": "开始",
|
||||||
|
"mining_click": "点击",
|
||||||
|
"mining_click_copy_address": "点击复制地址",
|
||||||
|
"mining_click_copy_block": "点击复制区块哈希",
|
||||||
|
"mining_click_copy_difficulty": "点击复制难度",
|
||||||
|
"mining_connected": "已连接",
|
||||||
|
"mining_connecting": "连接中...",
|
||||||
|
"mining_control": "挖矿控制",
|
||||||
|
"mining_difficulty_copied": "难度已复制",
|
||||||
|
"mining_est_block": "预计区块",
|
||||||
|
"mining_est_daily": "预计日收益",
|
||||||
|
"mining_filter_all": "全部",
|
||||||
|
"mining_filter_tip_all": "显示所有收益",
|
||||||
|
"mining_filter_tip_pool": "仅显示矿池收益",
|
||||||
|
"mining_filter_tip_solo": "仅显示单人收益",
|
||||||
|
"mining_idle_off_tooltip": "启用空闲挖矿",
|
||||||
|
"mining_idle_on_tooltip": "禁用空闲挖矿",
|
||||||
|
"mining_local_hashrate": "本地算力",
|
||||||
|
"mining_mine": "挖矿",
|
||||||
|
"mining_mining_addr": "挖矿地址",
|
||||||
|
"mining_network": "网络",
|
||||||
|
"mining_no_blocks_yet": "尚未找到区块",
|
||||||
|
"mining_no_payouts_yet": "尚无矿池支付",
|
||||||
|
"mining_no_saved_addresses": "没有保存的地址",
|
||||||
|
"mining_no_saved_pools": "没有保存的矿池",
|
||||||
|
"mining_off": "挖矿已关闭",
|
||||||
|
"mining_on": "挖矿已开启",
|
||||||
|
"mining_open_in_explorer": "在浏览器中打开",
|
||||||
|
"mining_payout_address": "支付地址",
|
||||||
|
"mining_payout_tooltip": "接收挖矿奖励的地址",
|
||||||
|
"mining_pool": "矿池",
|
||||||
|
"mining_pool_hashrate": "矿池算力",
|
||||||
|
"mining_pool_url": "矿池 URL",
|
||||||
|
"mining_recent_blocks": "最近区块",
|
||||||
|
"mining_recent_payouts": "最近矿池支付",
|
||||||
|
"mining_remove": "移除",
|
||||||
|
"mining_reset_defaults": "重置默认值",
|
||||||
|
"mining_save_payout_address": "保存支付地址",
|
||||||
|
"mining_save_pool_url": "保存矿池 URL",
|
||||||
|
"mining_saved_addresses": "已保存地址:",
|
||||||
|
"mining_saved_pools": "已保存矿池:",
|
||||||
|
"mining_shares": "份额",
|
||||||
|
"mining_show_chart": "图表",
|
||||||
|
"mining_show_log": "日志",
|
||||||
|
"mining_solo": "单人",
|
||||||
|
"mining_starting": "启动中...",
|
||||||
|
"mining_starting_tooltip": "矿工正在启动...",
|
||||||
|
"mining_statistics": "挖矿统计",
|
||||||
|
"mining_stop": "停止",
|
||||||
|
"mining_stop_solo_for_pool": "启动矿池挖矿前请先停止单人挖矿",
|
||||||
|
"mining_stop_solo_for_pool_settings": "请停止单人挖矿以更改矿池设置",
|
||||||
|
"mining_stopping": "停止中...",
|
||||||
|
"mining_stopping_tooltip": "矿工正在停止...",
|
||||||
|
"mining_syncing_tooltip": "区块链同步中...",
|
||||||
|
"mining_threads": "挖矿线程",
|
||||||
|
"mining_to_save": "保存",
|
||||||
|
"mining_today": "今天",
|
||||||
|
"mining_uptime": "运行时间",
|
||||||
|
"mining_yesterday": "昨天",
|
||||||
|
"network": "网络",
|
||||||
|
"network_fee": "网络手续费",
|
||||||
|
"network_hashrate": "全网算力",
|
||||||
|
"new": "+ 新建",
|
||||||
|
"new_shielded_created": "新屏蔽地址已创建",
|
||||||
|
"new_t_address": "新 T 地址",
|
||||||
|
"new_t_transparent": "新 t 地址(透明)",
|
||||||
|
"new_transparent_created": "新透明地址已创建",
|
||||||
|
"new_z_address": "新 Z 地址",
|
||||||
|
"new_z_shielded": "新 z 地址(屏蔽)",
|
||||||
|
"no_addresses": "未找到地址。请使用上方按钮创建一个。",
|
||||||
|
"no_addresses_available": "无可用地址",
|
||||||
|
"no_addresses_match": "没有匹配过滤器的地址",
|
||||||
|
"no_addresses_with_balance": "没有有余额的地址",
|
||||||
|
"no_matching": "没有匹配的交易",
|
||||||
|
"no_recent_receives": "没有最近的接收",
|
||||||
|
"no_recent_sends": "没有最近的发送",
|
||||||
|
"no_transactions": "未找到交易",
|
||||||
|
"node": "节点",
|
||||||
|
"node_security": "节点与安全",
|
||||||
|
"noise": "噪点",
|
||||||
|
"not_connected": "未连接到守护进程...",
|
||||||
|
"not_connected_to_daemon": "未连接到守护进程",
|
||||||
|
"notes": "备注",
|
||||||
|
"notes_optional": "备注(可选):",
|
||||||
|
"output_filename": "输出文件名:",
|
||||||
|
"overview": "概览",
|
||||||
|
"paste": "粘贴",
|
||||||
|
"paste_from_clipboard": "从剪贴板粘贴",
|
||||||
|
"pay_from": "付款来源",
|
||||||
|
"payment_request": "付款请求",
|
||||||
|
"payment_request_copied": "付款请求已复制",
|
||||||
|
"payment_uri_copied": "付款 URI 已复制",
|
||||||
|
"peers": "节点",
|
||||||
|
"peers_avg_ping": "平均延迟",
|
||||||
|
"peers_ban_24h": "封禁节点 24 小时",
|
||||||
|
"peers_ban_score": "封禁评分:%d",
|
||||||
|
"peers_banned": "已封禁",
|
||||||
|
"peers_banned_count": "已封禁:%d",
|
||||||
|
"peers_best_block": "最佳区块",
|
||||||
|
"peers_blockchain": "区块链",
|
||||||
|
"peers_blocks": "区块",
|
||||||
|
"peers_blocks_left": "剩余 %d 个区块",
|
||||||
|
"peers_clear_all_bans": "解除所有封禁",
|
||||||
|
"peers_click_copy": "点击复制",
|
||||||
|
"peers_connected": "已连接",
|
||||||
|
"peers_connected_count": "已连接:%d",
|
||||||
|
"peers_copy_ip": "复制 IP",
|
||||||
|
"peers_dir_in": "入",
|
||||||
|
"peers_dir_out": "出",
|
||||||
|
"peers_hash_copied": "哈希已复制",
|
||||||
|
"peers_hashrate": "算力",
|
||||||
|
"peers_in_out": "入/出",
|
||||||
|
"peers_longest": "最长",
|
||||||
|
"peers_longest_chain": "最长链",
|
||||||
|
"peers_memory": "内存",
|
||||||
|
"peers_no_banned": "无已封禁节点",
|
||||||
|
"peers_no_connected": "无已连接节点",
|
||||||
|
"peers_no_tls": "无 TLS",
|
||||||
|
"peers_notarized": "已公证",
|
||||||
|
"peers_p2p_port": "P2P 端口",
|
||||||
|
"peers_protocol": "协议",
|
||||||
|
"peers_received": "已接收",
|
||||||
|
"peers_refresh": "刷新",
|
||||||
|
"peers_refresh_tooltip": "刷新节点列表",
|
||||||
|
"peers_refreshing": "刷新中...",
|
||||||
|
"peers_sent": "已发送",
|
||||||
|
"peers_tt_id": "ID:%d",
|
||||||
|
"peers_tt_received": "已接收:%s",
|
||||||
|
"peers_tt_sent": "已发送:%s",
|
||||||
|
"peers_tt_services": "服务:%s",
|
||||||
|
"peers_tt_start_height": "起始高度:%d",
|
||||||
|
"peers_tt_synced": "已同步 H/B:%d/%d",
|
||||||
|
"peers_tt_tls_cipher": "TLS:%s",
|
||||||
|
"peers_unban": "解除封禁",
|
||||||
|
"peers_upper": "节点",
|
||||||
|
"peers_version": "版本",
|
||||||
|
"pending": "待处理",
|
||||||
|
"ping": "延迟",
|
||||||
|
"price_chart": "价格图表",
|
||||||
|
"qr_code": "二维码",
|
||||||
|
"qr_failed": "无法生成二维码",
|
||||||
|
"qr_title": "二维码",
|
||||||
|
"qr_unavailable": "二维码不可用",
|
||||||
|
"receive": "接收",
|
||||||
|
"received": "已接收",
|
||||||
|
"received_filter": "已接收",
|
||||||
|
"received_label": "已接收",
|
||||||
|
"received_upper": "已接收",
|
||||||
|
"receiving_addresses": "您的接收地址",
|
||||||
|
"recent_received": "最近接收",
|
||||||
|
"recent_sends": "最近发送",
|
||||||
|
"recipient": "收款方",
|
||||||
|
"recv_type": "接收",
|
||||||
|
"refresh": "刷新",
|
||||||
|
"refresh_now": "立即刷新",
|
||||||
|
"report_bug": "报告错误",
|
||||||
|
"request_amount": "金额(可选):",
|
||||||
|
"request_copy_uri": "复制 URI",
|
||||||
|
"request_description": "生成一个付款请求,他人可以扫描或复制。二维码包含您的地址和可选的金额/备注。",
|
||||||
|
"request_label": "标签(可选):",
|
||||||
|
"request_memo": "备注(可选):",
|
||||||
|
"request_payment": "请求付款",
|
||||||
|
"request_payment_uri": "付款 URI:",
|
||||||
|
"request_receive_address": "接收地址:",
|
||||||
|
"request_select_address": "选择地址...",
|
||||||
|
"request_shielded_addrs": "-- 屏蔽地址 --",
|
||||||
|
"request_title": "请求付款",
|
||||||
|
"request_transparent_addrs": "-- 透明地址 --",
|
||||||
|
"request_uri_copied": "付款 URI 已复制到剪贴板",
|
||||||
|
"rescan": "重新扫描",
|
||||||
|
"reset_to_defaults": "重置为默认值",
|
||||||
|
"review_send": "审核发送",
|
||||||
|
"rpc_host": "RPC 主机",
|
||||||
|
"rpc_pass": "密码",
|
||||||
|
"rpc_port": "端口",
|
||||||
|
"rpc_user": "用户名",
|
||||||
|
"save": "保存",
|
||||||
|
"save_settings": "保存设置",
|
||||||
|
"save_z_transactions": "将 Z 交易保存到列表",
|
||||||
|
"search_placeholder": "搜索...",
|
||||||
|
"security": "安全",
|
||||||
|
"select_address": "选择地址...",
|
||||||
|
"select_receiving_address": "选择接收地址...",
|
||||||
|
"select_source_address": "选择来源地址...",
|
||||||
|
"send": "发送",
|
||||||
|
"send_amount": "金额",
|
||||||
|
"send_amount_details": "金额详情",
|
||||||
|
"send_amount_upper": "金额",
|
||||||
|
"send_clear_fields": "清除所有表单字段?",
|
||||||
|
"send_copy_error": "复制错误",
|
||||||
|
"send_dismiss": "关闭",
|
||||||
|
"send_error_copied": "错误已复制到剪贴板",
|
||||||
|
"send_error_prefix": "错误:%s",
|
||||||
|
"send_exceeds_available": "超过可用额 (%.8f)",
|
||||||
|
"send_fee": "手续费",
|
||||||
|
"send_fee_high": "高",
|
||||||
|
"send_fee_low": "低",
|
||||||
|
"send_fee_normal": "普通",
|
||||||
|
"send_form_restored": "表单已恢复",
|
||||||
|
"send_from_this_address": "从此地址发送",
|
||||||
|
"send_go_to_receive": "前往接收",
|
||||||
|
"send_keep": "保留",
|
||||||
|
"send_network_fee": "网络手续费",
|
||||||
|
"send_no_balance": "无余额",
|
||||||
|
"send_no_recent": "没有最近的发送",
|
||||||
|
"send_recent_sends": "最近发送",
|
||||||
|
"send_recipient": "收款方",
|
||||||
|
"send_select_source": "选择来源地址...",
|
||||||
|
"send_sending_from": "发送来源",
|
||||||
|
"send_submitting": "正在提交交易...",
|
||||||
|
"send_switch_to_receive": "切换到接收页面获取您的地址并开始接收资金。",
|
||||||
|
"send_to": "发送至",
|
||||||
|
"send_tooltip_enter_amount": "请输入发送金额",
|
||||||
|
"send_tooltip_exceeds_balance": "金额超过可用余额",
|
||||||
|
"send_tooltip_in_progress": "交易正在进行中",
|
||||||
|
"send_tooltip_invalid_address": "请输入有效的收款地址",
|
||||||
|
"send_tooltip_not_connected": "未连接到守护进程",
|
||||||
|
"send_tooltip_select_source": "请先选择来源地址",
|
||||||
|
"send_tooltip_syncing": "请等待区块链同步",
|
||||||
|
"send_total": "合计",
|
||||||
|
"send_transaction": "发送交易",
|
||||||
|
"send_tx_failed": "交易失败",
|
||||||
|
"send_tx_sent": "交易已发送!",
|
||||||
|
"send_tx_success": "交易发送成功!",
|
||||||
|
"send_txid_copied": "交易ID 已复制到剪贴板",
|
||||||
|
"send_txid_label": "TxID:%s",
|
||||||
|
"send_valid_shielded": "有效的屏蔽地址",
|
||||||
|
"send_valid_transparent": "有效的透明地址",
|
||||||
|
"send_wallet_empty": "您的钱包是空的",
|
||||||
|
"send_yes_clear": "是,清除",
|
||||||
|
"sending": "正在发送交易",
|
||||||
|
"sending_from": "发送来源",
|
||||||
|
"sent": "已发送",
|
||||||
|
"sent_filter": "已发送",
|
||||||
|
"sent_type": "已发送",
|
||||||
|
"sent_upper": "已发送",
|
||||||
|
"settings": "设置",
|
||||||
|
"setup_wizard": "设置向导",
|
||||||
|
"share": "分享",
|
||||||
|
"shield_check_status": "检查状态",
|
||||||
|
"shield_completed": "操作成功完成!",
|
||||||
|
"shield_description": "通过将透明地址的 coinbase 输出发送到屏蔽地址来屏蔽您的挖矿奖励。这可以隐藏您的挖矿收入,提高隐私性。",
|
||||||
|
"shield_from_address": "从地址:",
|
||||||
|
"shield_funds": "屏蔽资金",
|
||||||
|
"shield_in_progress": "操作进行中...",
|
||||||
|
"shield_max_utxos": "每次操作最大 UTXO 数",
|
||||||
|
"shield_merge_done": "屏蔽/合并完成!",
|
||||||
|
"shield_select_z": "选择 z 地址...",
|
||||||
|
"shield_started": "屏蔽操作已开始",
|
||||||
|
"shield_title": "屏蔽 Coinbase 奖励",
|
||||||
|
"shield_to_address": "至地址(屏蔽):",
|
||||||
|
"shield_utxo_limit": "UTXO 限制:",
|
||||||
|
"shield_wildcard_hint": "使用 '*' 从所有透明地址屏蔽",
|
||||||
|
"shielded": "屏蔽",
|
||||||
|
"shielded_to": "屏蔽至",
|
||||||
|
"shielded_type": "屏蔽",
|
||||||
|
"show": "显示",
|
||||||
|
"show_qr_code": "显示二维码",
|
||||||
|
"showing_transactions": "显示第 %d\xe2\x80\x93%d 笔,共 %d 笔交易(总计:%zu)",
|
||||||
|
"simple_background": "简单背景",
|
||||||
|
"start_mining": "开始挖矿",
|
||||||
|
"status": "状态",
|
||||||
|
"stop_external": "停止外部守护进程",
|
||||||
|
"stop_mining": "停止挖矿",
|
||||||
|
"submitting_transaction": "正在提交交易...",
|
||||||
|
"success": "成功",
|
||||||
|
"summary": "摘要",
|
||||||
|
"syncing": "同步中...",
|
||||||
|
"t_addresses": "T 地址",
|
||||||
|
"test_connection": "测试",
|
||||||
|
"theme": "主题",
|
||||||
|
"theme_effects": "主题效果",
|
||||||
|
"time_days_ago": "%d 天前",
|
||||||
|
"time_hours_ago": "%d 小时前",
|
||||||
|
"time_minutes_ago": "%d 分钟前",
|
||||||
|
"time_seconds_ago": "%d 秒前",
|
||||||
|
"to": "至",
|
||||||
|
"to_upper": "至",
|
||||||
|
"tools": "工具",
|
||||||
|
"total": "合计",
|
||||||
|
"transaction_id": "交易 ID",
|
||||||
|
"transaction_sent": "交易发送成功",
|
||||||
|
"transaction_sent_msg": "交易已发送!",
|
||||||
|
"transaction_url": "交易 URL",
|
||||||
|
"transactions": "交易",
|
||||||
|
"transactions_upper": "交易",
|
||||||
|
"transparent": "透明",
|
||||||
|
"tx_confirmations": "%d 次确认",
|
||||||
|
"tx_details_title": "交易详情",
|
||||||
|
"tx_from_address": "发送地址:",
|
||||||
|
"tx_id_label": "交易 ID:",
|
||||||
|
"tx_immature": "未成熟",
|
||||||
|
"tx_mined": "已挖得",
|
||||||
|
"tx_received": "已接收",
|
||||||
|
"tx_sent": "已发送",
|
||||||
|
"tx_to_address": "接收地址:",
|
||||||
|
"tx_view_explorer": "在浏览器中查看",
|
||||||
|
"txs_count": "%d 笔交易",
|
||||||
|
"type": "类型",
|
||||||
|
"ui_opacity": "界面透明度",
|
||||||
|
"unban": "解除封禁",
|
||||||
|
"unconfirmed": "未确认",
|
||||||
|
"undo_clear": "撤销清除",
|
||||||
|
"unknown": "未知",
|
||||||
|
"use_embedded_daemon": "使用内置 dragonxd",
|
||||||
|
"use_tor": "使用 Tor",
|
||||||
|
"validate_btn": "验证",
|
||||||
|
"validate_description": "输入一个 DragonX 地址来检查它是否有效以及是否属于此钱包。",
|
||||||
|
"validate_invalid": "无效",
|
||||||
|
"validate_is_mine": "此钱包拥有该地址",
|
||||||
|
"validate_not_mine": "不属于此钱包",
|
||||||
|
"validate_ownership": "所有权:",
|
||||||
|
"validate_results": "结果:",
|
||||||
|
"validate_shielded_type": "屏蔽(z 地址)",
|
||||||
|
"validate_status": "状态:",
|
||||||
|
"validate_title": "验证地址",
|
||||||
|
"validate_transparent_type": "透明(t 地址)",
|
||||||
|
"validate_type": "类型:",
|
||||||
|
"validate_valid": "有效",
|
||||||
|
"validating": "验证中...",
|
||||||
|
"verbose_logging": "详细日志",
|
||||||
|
"version": "版本",
|
||||||
|
"view": "查看",
|
||||||
|
"view_details": "查看详情",
|
||||||
|
"view_on_explorer": "在浏览器中查看",
|
||||||
|
"waiting_for_daemon": "等待守护进程连接...",
|
||||||
|
"wallet": "钱包",
|
||||||
|
"wallet_empty": "您的钱包是空的",
|
||||||
|
"wallet_empty_hint": "切换到接收页面获取您的地址并开始接收资金。",
|
||||||
|
"warning": "警告",
|
||||||
|
"warning_upper": "警告!",
|
||||||
|
"website": "网站",
|
||||||
|
"window_opacity": "窗口透明度",
|
||||||
|
"yes_clear": "是,清除",
|
||||||
|
"your_addresses": "您的地址",
|
||||||
|
"z_addresses": "Z 地址",
|
||||||
|
}
|
||||||
|
|
||||||
|
out = os.path.join(os.path.dirname(__file__), "..", "res", "lang", "zh.json")
|
||||||
|
with open(out, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(translations, f, indent=4, ensure_ascii=False, sort_keys=True)
|
||||||
|
print(f"Wrote {len(translations)} Chinese translations to {os.path.abspath(out)}")
|
||||||
@@ -7,7 +7,7 @@ set -e
|
|||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
BUILD_DIR="${SCRIPT_DIR}/build/linux"
|
BUILD_DIR="${SCRIPT_DIR}/build/linux"
|
||||||
VERSION="1.0.0"
|
VERSION="1.2.0"
|
||||||
|
|
||||||
# Colors for output
|
# Colors for output
|
||||||
RED='\033[0;31m'
|
RED='\033[0;31m'
|
||||||
@@ -137,7 +137,7 @@ if [ -f "bin/ObsidianDragon" ]; then
|
|||||||
"${SCRIPT_DIR}/prebuilt-binaries/dragonxd-linux/hush-arrakis-chain"
|
"${SCRIPT_DIR}/prebuilt-binaries/dragonxd-linux/hush-arrakis-chain"
|
||||||
"${SCRIPT_DIR}/../hush-arrakis-chain"
|
"${SCRIPT_DIR}/../hush-arrakis-chain"
|
||||||
"${SCRIPT_DIR}/hush-arrakis-chain"
|
"${SCRIPT_DIR}/hush-arrakis-chain"
|
||||||
"$HOME/hush3/src/hush-arrakis-chain"
|
"$HOME/dragonx/src/hush-arrakis-chain"
|
||||||
)
|
)
|
||||||
|
|
||||||
for lpath in "${LAUNCHER_PATHS[@]}"; do
|
for lpath in "${LAUNCHER_PATHS[@]}"; do
|
||||||
@@ -155,7 +155,7 @@ if [ -f "bin/ObsidianDragon" ]; then
|
|||||||
"${SCRIPT_DIR}/prebuilt-binaries/dragonxd-linux/hushd"
|
"${SCRIPT_DIR}/prebuilt-binaries/dragonxd-linux/hushd"
|
||||||
"${SCRIPT_DIR}/../hushd"
|
"${SCRIPT_DIR}/../hushd"
|
||||||
"${SCRIPT_DIR}/hushd"
|
"${SCRIPT_DIR}/hushd"
|
||||||
"$HOME/hush3/src/hushd"
|
"$HOME/dragonx/src/hushd"
|
||||||
)
|
)
|
||||||
|
|
||||||
for hpath in "${HUSHD_PATHS[@]}"; do
|
for hpath in "${HUSHD_PATHS[@]}"; do
|
||||||
@@ -172,7 +172,7 @@ if [ -f "bin/ObsidianDragon" ]; then
|
|||||||
"${SCRIPT_DIR}/prebuilt-binaries/dragonxd-linux/dragonxd"
|
"${SCRIPT_DIR}/prebuilt-binaries/dragonxd-linux/dragonxd"
|
||||||
"${SCRIPT_DIR}/../dragonxd"
|
"${SCRIPT_DIR}/../dragonxd"
|
||||||
"${SCRIPT_DIR}/dragonxd"
|
"${SCRIPT_DIR}/dragonxd"
|
||||||
"$HOME/hush3/src/dragonxd"
|
"$HOME/dragonx/src/dragonxd"
|
||||||
)
|
)
|
||||||
|
|
||||||
for dpath in "${DRAGONXD_PATHS[@]}"; do
|
for dpath in "${DRAGONXD_PATHS[@]}"; do
|
||||||
@@ -189,8 +189,8 @@ if [ -f "bin/ObsidianDragon" ]; then
|
|||||||
"${SCRIPT_DIR}/prebuilt-binaries/dragonxd-linux/asmap.dat"
|
"${SCRIPT_DIR}/prebuilt-binaries/dragonxd-linux/asmap.dat"
|
||||||
"${SCRIPT_DIR}/../asmap.dat"
|
"${SCRIPT_DIR}/../asmap.dat"
|
||||||
"${SCRIPT_DIR}/asmap.dat"
|
"${SCRIPT_DIR}/asmap.dat"
|
||||||
"$HOME/hush3/asmap.dat"
|
"$HOME/dragonx/asmap.dat"
|
||||||
"$HOME/hush3/contrib/asmap/asmap.dat"
|
"$HOME/dragonx/contrib/asmap/asmap.dat"
|
||||||
)
|
)
|
||||||
|
|
||||||
for apath in "${ASMAP_PATHS[@]}"; do
|
for apath in "${ASMAP_PATHS[@]}"; do
|
||||||
|
|||||||
@@ -130,8 +130,8 @@ done
|
|||||||
|
|
||||||
# Look for asmap.dat
|
# Look for asmap.dat
|
||||||
ASMAP_PATHS=(
|
ASMAP_PATHS=(
|
||||||
"$HOME/hush3/asmap.dat"
|
"$HOME/dragonx/asmap.dat"
|
||||||
"$HOME/hush3/contrib/asmap/asmap.dat"
|
"$HOME/dragonx/contrib/asmap/asmap.dat"
|
||||||
"$SCRIPT_DIR/../asmap.dat"
|
"$SCRIPT_DIR/../asmap.dat"
|
||||||
"$SCRIPT_DIR/asmap.dat"
|
"$SCRIPT_DIR/asmap.dat"
|
||||||
"$SCRIPT_DIR/../SilentDragonX/asmap.dat"
|
"$SCRIPT_DIR/../SilentDragonX/asmap.dat"
|
||||||
@@ -411,7 +411,7 @@ if [ -f "bin/ObsidianDragon.exe" ]; then
|
|||||||
echo ""
|
echo ""
|
||||||
echo -e "${GREEN}Creating distribution package...${NC}"
|
echo -e "${GREEN}Creating distribution package...${NC}"
|
||||||
cd bin
|
cd bin
|
||||||
DIST_NAME="DragonX-Wallet-Windows-x64"
|
DIST_NAME="ObsidianDragon-Windows-x64"
|
||||||
rm -rf "$DIST_NAME" "$DIST_NAME.zip"
|
rm -rf "$DIST_NAME" "$DIST_NAME.zip"
|
||||||
mkdir -p "$DIST_NAME"
|
mkdir -p "$DIST_NAME"
|
||||||
cp ObsidianDragon.exe "$DIST_NAME/"
|
cp ObsidianDragon.exe "$DIST_NAME/"
|
||||||
@@ -441,7 +441,7 @@ The wallet will look for the daemon config at:
|
|||||||
|
|
||||||
This will be auto-created on first run if the daemon is present.
|
This will be auto-created on first run if the daemon is present.
|
||||||
|
|
||||||
For support: https://git.hush.is/hush/ObsidianDragon
|
For support: https://git.dragonx.is/dragonx/ObsidianDragon
|
||||||
READMEEOF
|
READMEEOF
|
||||||
|
|
||||||
if command -v zip &> /dev/null; then
|
if command -v zip &> /dev/null; then
|
||||||
|
|||||||
414
scripts/setup.sh
@@ -1,414 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# ── scripts/setup.sh ────────────────────────────────────────────────────────
|
|
||||||
# DragonX Wallet — Development Environment Setup
|
|
||||||
# Copyright 2024-2026 The Hush Developers
|
|
||||||
# Released under the GPLv3
|
|
||||||
#
|
|
||||||
# Detects your OS/distro, installs build prerequisites, fetches libraries,
|
|
||||||
# and validates the environment so you can build immediately with:
|
|
||||||
#
|
|
||||||
# ./build.sh # dev build
|
|
||||||
# ./build.sh --win-release # Windows cross-compile
|
|
||||||
# ./build.sh --linux-release # Linux release + AppImage
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# ./scripts/setup.sh # Interactive — install everything needed
|
|
||||||
# ./scripts/setup.sh --check # Just report what's missing, don't install
|
|
||||||
# ./scripts/setup.sh --all # Install dev + all cross-compile targets
|
|
||||||
# ./scripts/setup.sh --win # Also install Windows cross-compile deps
|
|
||||||
# ./scripts/setup.sh --mac # Also install macOS cross-compile deps
|
|
||||||
# ./scripts/setup.sh --sapling # Also download Sapling params (~51 MB)
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
||||||
|
|
||||||
# ── Colours ──────────────────────────────────────────────────────────────────
|
|
||||||
RED='\033[0;31m'
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
CYAN='\033[0;36m'
|
|
||||||
BOLD='\033[1m'
|
|
||||||
NC='\033[0m'
|
|
||||||
|
|
||||||
ok() { echo -e " ${GREEN}✓${NC} $1"; }
|
|
||||||
miss() { echo -e " ${RED}✗${NC} $1"; }
|
|
||||||
skip() { echo -e " ${YELLOW}—${NC} $1"; }
|
|
||||||
info() { echo -e "${GREEN}[*]${NC} $1"; }
|
|
||||||
warn() { echo -e "${YELLOW}[!]${NC} $1"; }
|
|
||||||
err() { echo -e "${RED}[ERROR]${NC} $1"; }
|
|
||||||
header(){ echo -e "\n${CYAN}── $1 ──${NC}"; }
|
|
||||||
|
|
||||||
# ── Parse args ───────────────────────────────────────────────────────────────
|
|
||||||
CHECK_ONLY=false
|
|
||||||
SETUP_WIN=false
|
|
||||||
SETUP_MAC=false
|
|
||||||
SETUP_SAPLING=false
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case $1 in
|
|
||||||
--check) CHECK_ONLY=true; shift ;;
|
|
||||||
--win) SETUP_WIN=true; shift ;;
|
|
||||||
--mac) SETUP_MAC=true; shift ;;
|
|
||||||
--sapling) SETUP_SAPLING=true; shift ;;
|
|
||||||
--all) SETUP_WIN=true; SETUP_MAC=true; SETUP_SAPLING=true; shift ;;
|
|
||||||
-h|--help)
|
|
||||||
sed -n '2,/^# ─\{10\}/{ /^# ─\{10\}/d; s/^# \?//p; }' "$0"
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*) err "Unknown option: $1"; exit 1 ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
# ── Detect OS / distro ──────────────────────────────────────────────────────
|
|
||||||
detect_os() {
|
|
||||||
OS="$(uname -s)"
|
|
||||||
DISTRO="unknown"
|
|
||||||
PKG=""
|
|
||||||
|
|
||||||
case "$OS" in
|
|
||||||
Linux)
|
|
||||||
if [[ -f /etc/os-release ]]; then
|
|
||||||
. /etc/os-release
|
|
||||||
case "${ID:-}" in
|
|
||||||
ubuntu|debian|linuxmint|pop|elementary|zorin|neon)
|
|
||||||
DISTRO="debian"; PKG="apt" ;;
|
|
||||||
fedora|rhel|centos|rocky|alma)
|
|
||||||
DISTRO="fedora"; PKG="dnf" ;;
|
|
||||||
arch|manjaro|endeavouros|garuda)
|
|
||||||
DISTRO="arch"; PKG="pacman" ;;
|
|
||||||
opensuse*|suse*)
|
|
||||||
DISTRO="suse"; PKG="zypper" ;;
|
|
||||||
void)
|
|
||||||
DISTRO="void"; PKG="xbps" ;;
|
|
||||||
gentoo)
|
|
||||||
DISTRO="gentoo"; PKG="emerge" ;;
|
|
||||||
*)
|
|
||||||
# Fallback: check for package managers
|
|
||||||
command -v apt &>/dev/null && { DISTRO="debian"; PKG="apt"; } ||
|
|
||||||
command -v dnf &>/dev/null && { DISTRO="fedora"; PKG="dnf"; } ||
|
|
||||||
command -v pacman &>/dev/null && { DISTRO="arch"; PKG="pacman"; }
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
Darwin)
|
|
||||||
DISTRO="macos"
|
|
||||||
command -v brew &>/dev/null && PKG="brew"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
err "Unsupported OS: $OS"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── Package lists per distro ────────────────────────────────────────────────
|
|
||||||
# Core: minimum to do a dev build (Linux native)
|
|
||||||
pkgs_core_debian="build-essential cmake git pkg-config
|
|
||||||
libgl1-mesa-dev libx11-dev libxcursor-dev libxrandr-dev
|
|
||||||
libxinerama-dev libxi-dev libxkbcommon-dev libwayland-dev
|
|
||||||
libsodium-dev libcurl4-openssl-dev"
|
|
||||||
|
|
||||||
pkgs_core_fedora="gcc gcc-c++ cmake git pkg-config
|
|
||||||
mesa-libGL-devel libX11-devel libXcursor-devel libXrandr-devel
|
|
||||||
libXinerama-devel libXi-devel libxkbcommon-devel wayland-devel
|
|
||||||
libsodium-devel libcurl-devel"
|
|
||||||
|
|
||||||
pkgs_core_arch="base-devel cmake git pkg-config
|
|
||||||
mesa libx11 libxcursor libxrandr libxinerama libxi
|
|
||||||
libxkbcommon wayland libsodium curl"
|
|
||||||
|
|
||||||
pkgs_core_macos="cmake"
|
|
||||||
|
|
||||||
# Windows cross-compile (from Linux)
|
|
||||||
pkgs_win_debian="mingw-w64 zip"
|
|
||||||
pkgs_win_fedora="mingw64-gcc mingw64-gcc-c++ zip"
|
|
||||||
pkgs_win_arch="mingw-w64-gcc zip"
|
|
||||||
|
|
||||||
# macOS cross-compile helpers (osxcross is separate)
|
|
||||||
pkgs_mac_debian="genisoimage icnsutils"
|
|
||||||
pkgs_mac_fedora="genisoimage"
|
|
||||||
pkgs_mac_arch="cdrtools"
|
|
||||||
|
|
||||||
# ── Helpers ──────────────────────────────────────────────────────────────────
|
|
||||||
has_cmd() { command -v "$1" &>/dev/null; }
|
|
||||||
|
|
||||||
# Install packages for the detected distro
|
|
||||||
install_pkgs() {
|
|
||||||
local pkgs="$1"
|
|
||||||
local desc="$2"
|
|
||||||
|
|
||||||
if $CHECK_ONLY; then
|
|
||||||
warn "Would install ($desc): $pkgs"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
info "Installing $desc packages..."
|
|
||||||
case "$PKG" in
|
|
||||||
apt) sudo apt-get update -qq && sudo apt-get install -y $pkgs ;;
|
|
||||||
dnf) sudo dnf install -y $pkgs ;;
|
|
||||||
pacman) sudo pacman -S --needed --noconfirm $pkgs ;;
|
|
||||||
zypper) sudo zypper install -y $pkgs ;;
|
|
||||||
brew) brew install $pkgs ;;
|
|
||||||
*) err "No supported package manager found for $DISTRO"
|
|
||||||
echo "Please install manually: $pkgs"
|
|
||||||
return 1 ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
# Select the right package list variable
|
|
||||||
get_pkgs() {
|
|
||||||
local category="$1" # core, win, mac
|
|
||||||
local var="pkgs_${category}_${DISTRO}"
|
|
||||||
echo "${!var:-}"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── Check individual tools ──────────────────────────────────────────────────
|
|
||||||
MISSING=0
|
|
||||||
|
|
||||||
check_tool() {
|
|
||||||
local cmd="$1"
|
|
||||||
local label="${2:-$1}"
|
|
||||||
if has_cmd "$cmd"; then
|
|
||||||
ok "$label"
|
|
||||||
else
|
|
||||||
miss "$label (not found: $cmd)"
|
|
||||||
MISSING=$((MISSING + 1))
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
check_file() {
|
|
||||||
local path="$1"
|
|
||||||
local label="$2"
|
|
||||||
if [[ -f "$path" ]]; then
|
|
||||||
ok "$label"
|
|
||||||
else
|
|
||||||
miss "$label ($path)"
|
|
||||||
MISSING=$((MISSING + 1))
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
check_dir() {
|
|
||||||
local path="$1"
|
|
||||||
local label="$2"
|
|
||||||
if [[ -d "$path" ]]; then
|
|
||||||
ok "$label"
|
|
||||||
else
|
|
||||||
miss "$label ($path)"
|
|
||||||
MISSING=$((MISSING + 1))
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# ═════════════════════════════════════════════════════════════════════════════
|
|
||||||
# MAIN
|
|
||||||
# ═════════════════════════════════════════════════════════════════════════════
|
|
||||||
echo -e "${BOLD}DragonX Wallet — Development Setup${NC}"
|
|
||||||
echo "═══════════════════════════════════"
|
|
||||||
|
|
||||||
detect_os
|
|
||||||
info "Detected: $OS / $DISTRO (package manager: ${PKG:-none})"
|
|
||||||
|
|
||||||
# ── 1. Core build dependencies ──────────────────────────────────────────────
|
|
||||||
header "Core Build Dependencies"
|
|
||||||
|
|
||||||
core_pkgs="$(get_pkgs core)"
|
|
||||||
if [[ -z "$core_pkgs" ]]; then
|
|
||||||
warn "No package list for $DISTRO — check README for manual instructions"
|
|
||||||
else
|
|
||||||
# Check if key tools are already present
|
|
||||||
NEED_CORE=false
|
|
||||||
has_cmd cmake && has_cmd g++ && has_cmd pkg-config || NEED_CORE=true
|
|
||||||
|
|
||||||
if $NEED_CORE; then
|
|
||||||
install_pkgs "$core_pkgs" "core build"
|
|
||||||
else
|
|
||||||
ok "Core tools already installed (cmake, g++, pkg-config)"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
check_tool cmake "cmake"
|
|
||||||
check_tool g++ "g++ (C++ compiler)"
|
|
||||||
check_tool git "git"
|
|
||||||
check_tool make "make"
|
|
||||||
|
|
||||||
# ── 2. libsodium ────────────────────────────────────────────────────────────
|
|
||||||
header "libsodium"
|
|
||||||
|
|
||||||
SODIUM_OK=false
|
|
||||||
# Check system libsodium
|
|
||||||
if pkg-config --exists libsodium 2>/dev/null; then
|
|
||||||
ok "libsodium (system, $(pkg-config --modversion libsodium))"
|
|
||||||
SODIUM_OK=true
|
|
||||||
elif [[ -f "$PROJECT_DIR/libs/libsodium/lib/libsodium.a" ]]; then
|
|
||||||
ok "libsodium (local build)"
|
|
||||||
SODIUM_OK=true
|
|
||||||
else
|
|
||||||
miss "libsodium not found"
|
|
||||||
if ! $CHECK_ONLY; then
|
|
||||||
info "Building libsodium from source..."
|
|
||||||
"$SCRIPT_DIR/fetch-libsodium.sh" && SODIUM_OK=true
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── 3. Windows cross-compile (optional) ─────────────────────────────────────
|
|
||||||
header "Windows Cross-Compile"
|
|
||||||
|
|
||||||
if $SETUP_WIN; then
|
|
||||||
win_pkgs="$(get_pkgs win)"
|
|
||||||
if [[ -n "$win_pkgs" ]]; then
|
|
||||||
install_pkgs "$win_pkgs" "Windows cross-compile"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Set posix thread model if available
|
|
||||||
if has_cmd update-alternatives && [[ "$PKG" == "apt" ]]; then
|
|
||||||
if ! $CHECK_ONLY; then
|
|
||||||
sudo update-alternatives --set x86_64-w64-mingw32-gcc \
|
|
||||||
/usr/bin/x86_64-w64-mingw32-gcc-posix 2>/dev/null || true
|
|
||||||
sudo update-alternatives --set x86_64-w64-mingw32-g++ \
|
|
||||||
/usr/bin/x86_64-w64-mingw32-g++-posix 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Fetch libsodium for Windows
|
|
||||||
if [[ ! -f "$PROJECT_DIR/libs/libsodium-win/lib/libsodium.a" ]]; then
|
|
||||||
if ! $CHECK_ONLY; then
|
|
||||||
info "Building libsodium for Windows target..."
|
|
||||||
"$SCRIPT_DIR/fetch-libsodium.sh" --win
|
|
||||||
else
|
|
||||||
miss "libsodium-win (not built yet)"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
ok "libsodium-win"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if has_cmd x86_64-w64-mingw32-g++-posix || has_cmd x86_64-w64-mingw32-g++; then
|
|
||||||
ok "mingw-w64 ($(x86_64-w64-mingw32-g++-posix --version 2>/dev/null | head -1 || x86_64-w64-mingw32-g++ --version 2>/dev/null | head -1))"
|
|
||||||
else
|
|
||||||
if $SETUP_WIN; then
|
|
||||||
miss "mingw-w64"
|
|
||||||
else
|
|
||||||
skip "mingw-w64 (use --win to install)"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── 4. macOS cross-compile (optional) ───────────────────────────────────────
|
|
||||||
header "macOS Cross-Compile"
|
|
||||||
|
|
||||||
if $SETUP_MAC; then
|
|
||||||
mac_pkgs="$(get_pkgs mac)"
|
|
||||||
if [[ -n "$mac_pkgs" ]]; then
|
|
||||||
install_pkgs "$mac_pkgs" "macOS cross-compile helpers"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Fetch libsodium for macOS
|
|
||||||
if [[ ! -f "$PROJECT_DIR/libs/libsodium-mac/lib/libsodium.a" ]]; then
|
|
||||||
if ! $CHECK_ONLY; then
|
|
||||||
info "Building libsodium for macOS target..."
|
|
||||||
"$SCRIPT_DIR/fetch-libsodium.sh" --mac
|
|
||||||
else
|
|
||||||
miss "libsodium-mac (not built yet)"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
ok "libsodium-mac"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -d "$PROJECT_DIR/external/osxcross/target" ]] || [[ -d "${OSXCROSS:-}/target" ]]; then
|
|
||||||
ok "osxcross"
|
|
||||||
else
|
|
||||||
if $SETUP_MAC; then
|
|
||||||
miss "osxcross (must be set up manually — see README)"
|
|
||||||
else
|
|
||||||
skip "osxcross (use --mac to set up macOS deps)"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── 5. Sapling parameters ───────────────────────────────────────────────────
|
|
||||||
header "Sapling Parameters"
|
|
||||||
|
|
||||||
SAPLING_DIR=""
|
|
||||||
for d in "$HOME/.zcash-params" "$HOME/.hush-params"; do
|
|
||||||
if [[ -f "$d/sapling-spend.params" && -f "$d/sapling-output.params" ]]; then
|
|
||||||
SAPLING_DIR="$d"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# Also check project-local locations
|
|
||||||
if [[ -z "$SAPLING_DIR" ]]; then
|
|
||||||
for d in "$PROJECT_DIR/prebuilt-binaries/dragonxd-linux" "$PROJECT_DIR/prebuilt-binaries/dragonxd-win"; do
|
|
||||||
if [[ -f "$d/sapling-spend.params" && -f "$d/sapling-output.params" ]]; then
|
|
||||||
SAPLING_DIR="$d"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -n "$SAPLING_DIR" ]]; then
|
|
||||||
ok "sapling-spend.params ($(du -h "$SAPLING_DIR/sapling-spend.params" | cut -f1))"
|
|
||||||
ok "sapling-output.params ($(du -h "$SAPLING_DIR/sapling-output.params" | cut -f1))"
|
|
||||||
elif $SETUP_SAPLING; then
|
|
||||||
if ! $CHECK_ONLY; then
|
|
||||||
info "Downloading Sapling parameters (~51 MB)..."
|
|
||||||
PARAMS_DIR="$HOME/.zcash-params"
|
|
||||||
mkdir -p "$PARAMS_DIR"
|
|
||||||
|
|
||||||
SPEND_URL="https://z.cash/downloads/sapling-spend.params"
|
|
||||||
OUTPUT_URL="https://z.cash/downloads/sapling-output.params"
|
|
||||||
|
|
||||||
curl -fSL -o "$PARAMS_DIR/sapling-spend.params" "$SPEND_URL" && \
|
|
||||||
ok "Downloaded sapling-spend.params"
|
|
||||||
curl -fSL -o "$PARAMS_DIR/sapling-output.params" "$OUTPUT_URL" && \
|
|
||||||
ok "Downloaded sapling-output.params"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
skip "Sapling params not found (use --sapling to download, or they'll be extracted at runtime from embedded builds)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── 6. Binary directories ───────────────────────────────────────────────────
|
|
||||||
header "Binary Directories"
|
|
||||||
|
|
||||||
for platform in dragonxd-linux dragonxd-win dragonxd-mac xmrig; do
|
|
||||||
dir="$PROJECT_DIR/prebuilt-binaries/$platform"
|
|
||||||
if [[ -d "$dir" ]]; then
|
|
||||||
# Count actual files (not .gitkeep)
|
|
||||||
count=$(find "$dir" -maxdepth 1 -type f ! -name '.gitkeep' | wc -l)
|
|
||||||
if [[ $count -gt 0 ]]; then
|
|
||||||
ok "prebuilt-binaries/$platform/ ($count files)"
|
|
||||||
else
|
|
||||||
skip "prebuilt-binaries/$platform/ (empty — place binaries here)"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
if ! $CHECK_ONLY; then
|
|
||||||
mkdir -p "$dir"
|
|
||||||
touch "$dir/.gitkeep"
|
|
||||||
ok "Created prebuilt-binaries/$platform/"
|
|
||||||
else
|
|
||||||
miss "prebuilt-binaries/$platform/"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# ── Summary ──────────────────────────────────────────────────────────────────
|
|
||||||
echo ""
|
|
||||||
echo "═══════════════════════════════════════════"
|
|
||||||
if [[ $MISSING -eq 0 ]]; then
|
|
||||||
echo -e "${GREEN}${BOLD} Setup complete — ready to build!${NC}"
|
|
||||||
echo ""
|
|
||||||
echo " Quick start:"
|
|
||||||
echo " ./build.sh # Dev build"
|
|
||||||
echo " ./build.sh --linux-release # Linux release + AppImage"
|
|
||||||
echo " ./build.sh --win-release # Windows cross-compile"
|
|
||||||
else
|
|
||||||
echo -e "${YELLOW}${BOLD} $MISSING item(s) still need attention${NC}"
|
|
||||||
if $CHECK_ONLY; then
|
|
||||||
echo ""
|
|
||||||
echo " Run without --check to install automatically:"
|
|
||||||
echo " ./scripts/setup.sh"
|
|
||||||
echo " ./scripts/setup.sh --all # Include cross-compile + Sapling"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
echo "═══════════════════════════════════════════"
|
|
||||||
779
setup.sh
Executable file
@@ -0,0 +1,779 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ── setup.sh ────────────────────────────────────────────────────────────────
|
||||||
|
# DragonX Wallet — Development Environment Setup
|
||||||
|
# Copyright 2024-2026 The Hush Developers
|
||||||
|
# Released under the GPLv3
|
||||||
|
#
|
||||||
|
# Detects your OS/distro, installs build prerequisites, fetches libraries,
|
||||||
|
# and validates the environment so you can build immediately with:
|
||||||
|
#
|
||||||
|
# ./build.sh # dev build
|
||||||
|
# ./build.sh --win-release # Windows cross-compile
|
||||||
|
# ./build.sh --linux-release # Linux release + AppImage
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./setup.sh # Interactive — install everything needed
|
||||||
|
# ./setup.sh --check # Just report what's missing, don't install
|
||||||
|
# ./setup.sh --all # Install dev + all cross-compile targets
|
||||||
|
# ./setup.sh --win # Also install Windows cross-compile deps
|
||||||
|
# ./setup.sh --mac # Also install macOS cross-compile deps
|
||||||
|
# ./setup.sh --sapling # Also download Sapling params (~51 MB)
|
||||||
|
# ./setup.sh -j6 # Use 6 threads for building
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
|
# ── Parallel builds ─────────────────────────────────────────────────────────
|
||||||
|
# Default to 1 core; use -jN to increase (e.g. -j6)
|
||||||
|
NPROC=1
|
||||||
|
|
||||||
|
# ── Colours ──────────────────────────────────────────────────────────────────
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
BOLD='\033[1m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
ok() { echo -e " ${GREEN}✓${NC} $1"; }
|
||||||
|
miss() { echo -e " ${RED}✗${NC} $1"; }
|
||||||
|
skip() { echo -e " ${YELLOW}—${NC} $1"; }
|
||||||
|
info() { echo -e "${GREEN}[*]${NC} $1"; }
|
||||||
|
warn() { echo -e "${YELLOW}[!]${NC} $1"; }
|
||||||
|
err() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||||
|
header(){ echo -e "\n${CYAN}── $1 ──${NC}"; }
|
||||||
|
|
||||||
|
# ── Parse args ───────────────────────────────────────────────────────────────
|
||||||
|
CHECK_ONLY=false
|
||||||
|
SETUP_WIN=false
|
||||||
|
SETUP_MAC=false
|
||||||
|
SETUP_SAPLING=false
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case $1 in
|
||||||
|
--check) CHECK_ONLY=true; shift ;;
|
||||||
|
--win) SETUP_WIN=true; shift ;;
|
||||||
|
--mac) SETUP_MAC=true; shift ;;
|
||||||
|
--sapling) SETUP_SAPLING=true; shift ;;
|
||||||
|
--all) SETUP_WIN=true; SETUP_MAC=true; SETUP_SAPLING=true; shift ;;
|
||||||
|
-j*) NPROC="${1#-j}"; shift ;;
|
||||||
|
-h|--help)
|
||||||
|
sed -n '2,/^# ─\{10\}/{ /^# ─\{10\}/d; s/^# \?//p; }' "$0"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*) err "Unknown option: $1"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# Apply parallel build flag (after arg parsing so -jN override takes effect)
|
||||||
|
# NOTE: We do NOT export MAKEFLAGS globally — that interferes with autotools
|
||||||
|
# builds (dragonx daemon). Instead, -j is passed explicitly where needed.
|
||||||
|
|
||||||
|
# ── Detect OS / distro ──────────────────────────────────────────────────────
|
||||||
|
detect_os() {
|
||||||
|
OS="$(uname -s)"
|
||||||
|
DISTRO="unknown"
|
||||||
|
PKG=""
|
||||||
|
|
||||||
|
case "$OS" in
|
||||||
|
Linux)
|
||||||
|
if [[ -f /etc/os-release ]]; then
|
||||||
|
. /etc/os-release
|
||||||
|
case "${ID:-}" in
|
||||||
|
ubuntu|debian|linuxmint|pop|elementary|zorin|neon)
|
||||||
|
DISTRO="debian"; PKG="apt" ;;
|
||||||
|
fedora|rhel|centos|rocky|alma)
|
||||||
|
DISTRO="fedora"; PKG="dnf" ;;
|
||||||
|
arch|manjaro|endeavouros|garuda)
|
||||||
|
DISTRO="arch"; PKG="pacman" ;;
|
||||||
|
opensuse*|suse*)
|
||||||
|
DISTRO="suse"; PKG="zypper" ;;
|
||||||
|
void)
|
||||||
|
DISTRO="void"; PKG="xbps" ;;
|
||||||
|
gentoo)
|
||||||
|
DISTRO="gentoo"; PKG="emerge" ;;
|
||||||
|
*)
|
||||||
|
# Fallback: check for package managers
|
||||||
|
command -v apt &>/dev/null && { DISTRO="debian"; PKG="apt"; } ||
|
||||||
|
command -v dnf &>/dev/null && { DISTRO="fedora"; PKG="dnf"; } ||
|
||||||
|
command -v pacman &>/dev/null && { DISTRO="arch"; PKG="pacman"; } ||
|
||||||
|
true
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
Darwin)
|
||||||
|
DISTRO="macos"
|
||||||
|
if command -v brew &>/dev/null; then PKG="brew"; fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
err "Unsupported OS: $OS"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Package lists per distro ────────────────────────────────────────────────
|
||||||
|
# Core: minimum to do a dev build (Linux native)
|
||||||
|
pkgs_core_debian="build-essential cmake git pkg-config
|
||||||
|
libgl1-mesa-dev libx11-dev libxcursor-dev libxrandr-dev
|
||||||
|
libxinerama-dev libxi-dev libxkbcommon-dev libwayland-dev
|
||||||
|
libsodium-dev libcurl4-openssl-dev
|
||||||
|
autoconf automake libtool wget python3 xxd"
|
||||||
|
|
||||||
|
pkgs_core_fedora="gcc gcc-c++ cmake git pkg-config
|
||||||
|
mesa-libGL-devel libX11-devel libXcursor-devel libXrandr-devel
|
||||||
|
libXinerama-devel libXi-devel libxkbcommon-devel wayland-devel
|
||||||
|
libsodium-devel libcurl-devel
|
||||||
|
autoconf automake libtool wget python3 vim-common"
|
||||||
|
|
||||||
|
pkgs_core_arch="base-devel cmake git pkg-config
|
||||||
|
mesa libx11 libxcursor libxrandr libxinerama libxi
|
||||||
|
libxkbcommon wayland libsodium curl
|
||||||
|
autoconf automake libtool wget python xxd"
|
||||||
|
|
||||||
|
pkgs_core_macos="cmake python xxd"
|
||||||
|
|
||||||
|
# Windows cross-compile (from Linux)
|
||||||
|
pkgs_win_debian="mingw-w64 zip"
|
||||||
|
pkgs_win_fedora="mingw64-gcc mingw64-gcc-c++ zip"
|
||||||
|
pkgs_win_arch="mingw-w64-gcc zip"
|
||||||
|
|
||||||
|
# macOS cross-compile helpers (osxcross is separate)
|
||||||
|
pkgs_mac_debian="genisoimage icnsutils"
|
||||||
|
pkgs_mac_fedora="genisoimage"
|
||||||
|
pkgs_mac_arch="cdrtools"
|
||||||
|
|
||||||
|
# ── Helpers ──────────────────────────────────────────────────────────────────
|
||||||
|
has_cmd() { command -v "$1" &>/dev/null; }
|
||||||
|
|
||||||
|
# Install packages for the detected distro
|
||||||
|
install_pkgs() {
|
||||||
|
local pkgs="$1"
|
||||||
|
local desc="$2"
|
||||||
|
|
||||||
|
if $CHECK_ONLY; then
|
||||||
|
warn "Would install ($desc): $pkgs"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "Installing $desc packages..."
|
||||||
|
case "$PKG" in
|
||||||
|
apt) sudo apt-get update -qq && sudo apt-get install -y $pkgs ;;
|
||||||
|
dnf) sudo dnf install -y $pkgs ;;
|
||||||
|
pacman)
|
||||||
|
# Try pacman first; fall back to AUR helper for packages not in official repos
|
||||||
|
if ! sudo pacman -S --needed --noconfirm $pkgs 2>/dev/null; then
|
||||||
|
if command -v yay &>/dev/null; then
|
||||||
|
yay -S --needed --noconfirm $pkgs
|
||||||
|
elif command -v paru &>/dev/null; then
|
||||||
|
paru -S --needed --noconfirm $pkgs
|
||||||
|
else
|
||||||
|
err "pacman failed and no AUR helper (yay/paru) found"
|
||||||
|
echo "Some packages may be in the AUR. Install yay or paru, then retry."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
zypper) sudo zypper install -y $pkgs ;;
|
||||||
|
brew) brew install $pkgs ;;
|
||||||
|
*) err "No supported package manager found for $DISTRO"
|
||||||
|
echo "Please install manually: $pkgs"
|
||||||
|
return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# Select the right package list variable
|
||||||
|
get_pkgs() {
|
||||||
|
local category="$1" # core, win, mac
|
||||||
|
local var="pkgs_${category}_${DISTRO}"
|
||||||
|
echo "${!var:-}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Check individual tools ──────────────────────────────────────────────────
|
||||||
|
MISSING=0
|
||||||
|
|
||||||
|
check_tool() {
|
||||||
|
local cmd="$1"
|
||||||
|
local label="${2:-$1}"
|
||||||
|
if has_cmd "$cmd"; then
|
||||||
|
ok "$label"
|
||||||
|
else
|
||||||
|
miss "$label (not found: $cmd)"
|
||||||
|
MISSING=$((MISSING + 1))
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_file() {
|
||||||
|
local path="$1"
|
||||||
|
local label="$2"
|
||||||
|
if [[ -f "$path" ]]; then
|
||||||
|
ok "$label"
|
||||||
|
else
|
||||||
|
miss "$label ($path)"
|
||||||
|
MISSING=$((MISSING + 1))
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_dir() {
|
||||||
|
local path="$1"
|
||||||
|
local label="$2"
|
||||||
|
if [[ -d "$path" ]]; then
|
||||||
|
ok "$label"
|
||||||
|
else
|
||||||
|
miss "$label ($path)"
|
||||||
|
MISSING=$((MISSING + 1))
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ═════════════════════════════════════════════════════════════════════════════
|
||||||
|
# MAIN
|
||||||
|
# ═════════════════════════════════════════════════════════════════════════════
|
||||||
|
echo -e "${BOLD}DragonX Wallet — Development Setup${NC}"
|
||||||
|
echo "═══════════════════════════════════"
|
||||||
|
|
||||||
|
detect_os
|
||||||
|
info "Detected: $OS / $DISTRO (package manager: ${PKG:-none})"
|
||||||
|
|
||||||
|
# ── 1. Core build dependencies ──────────────────────────────────────────────
|
||||||
|
header "Core Build Dependencies"
|
||||||
|
|
||||||
|
core_pkgs="$(get_pkgs core)"
|
||||||
|
if [[ -z "$core_pkgs" ]]; then
|
||||||
|
warn "No package list for $DISTRO — check README for manual instructions"
|
||||||
|
else
|
||||||
|
# Check if key tools are already present
|
||||||
|
NEED_CORE=false
|
||||||
|
has_cmd cmake && has_cmd g++ && has_cmd pkg-config && has_cmd python3 && has_cmd xxd || NEED_CORE=true
|
||||||
|
|
||||||
|
if $NEED_CORE; then
|
||||||
|
install_pkgs "$core_pkgs" "core build"
|
||||||
|
else
|
||||||
|
ok "Core tools already installed (cmake, g++, pkg-config)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
check_tool cmake "cmake"
|
||||||
|
check_tool g++ "g++ (C++ compiler)"
|
||||||
|
check_tool git "git"
|
||||||
|
check_tool make "make"
|
||||||
|
check_tool python3 "python3 (theme expansion)"
|
||||||
|
check_tool xxd "xxd (embedded language headers)"
|
||||||
|
|
||||||
|
# ── 2. libsodium ────────────────────────────────────────────────────────────
|
||||||
|
header "libsodium"
|
||||||
|
|
||||||
|
SODIUM_OK=false
|
||||||
|
# Check system libsodium
|
||||||
|
if pkg-config --exists libsodium 2>/dev/null; then
|
||||||
|
ok "libsodium (system, $(pkg-config --modversion libsodium))"
|
||||||
|
SODIUM_OK=true
|
||||||
|
elif [[ -f "$PROJECT_DIR/libs/libsodium/lib/libsodium.a" ]]; then
|
||||||
|
ok "libsodium (local build)"
|
||||||
|
SODIUM_OK=true
|
||||||
|
else
|
||||||
|
miss "libsodium not found"
|
||||||
|
if ! $CHECK_ONLY; then
|
||||||
|
info "Building libsodium from source..."
|
||||||
|
"$PROJECT_DIR/scripts/fetch-libsodium.sh" && SODIUM_OK=true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 3. Windows cross-compile (optional) ─────────────────────────────────────
|
||||||
|
header "Windows Cross-Compile"
|
||||||
|
|
||||||
|
if $SETUP_WIN; then
|
||||||
|
win_pkgs="$(get_pkgs win)"
|
||||||
|
if [[ -n "$win_pkgs" ]]; then
|
||||||
|
install_pkgs "$win_pkgs" "Windows cross-compile"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Set posix thread model if available
|
||||||
|
if has_cmd update-alternatives && [[ "$PKG" == "apt" ]]; then
|
||||||
|
if ! $CHECK_ONLY; then
|
||||||
|
sudo update-alternatives --set x86_64-w64-mingw32-gcc \
|
||||||
|
/usr/bin/x86_64-w64-mingw32-gcc-posix 2>/dev/null || true
|
||||||
|
sudo update-alternatives --set x86_64-w64-mingw32-g++ \
|
||||||
|
/usr/bin/x86_64-w64-mingw32-g++-posix 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Fetch libsodium for Windows
|
||||||
|
if [[ ! -f "$PROJECT_DIR/libs/libsodium-win/lib/libsodium.a" ]]; then
|
||||||
|
if ! $CHECK_ONLY; then
|
||||||
|
info "Building libsodium for Windows target..."
|
||||||
|
"$PROJECT_DIR/scripts/fetch-libsodium.sh" --win
|
||||||
|
else
|
||||||
|
miss "libsodium-win (not built yet)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
ok "libsodium-win"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if has_cmd x86_64-w64-mingw32-g++-posix || has_cmd x86_64-w64-mingw32-g++; then
|
||||||
|
ok "mingw-w64 ($(x86_64-w64-mingw32-g++-posix --version 2>/dev/null | head -1 || x86_64-w64-mingw32-g++ --version 2>/dev/null | head -1))"
|
||||||
|
else
|
||||||
|
if $SETUP_WIN; then
|
||||||
|
miss "mingw-w64"
|
||||||
|
else
|
||||||
|
skip "mingw-w64 (use --win to install)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 4. macOS cross-compile (optional) ───────────────────────────────────────
|
||||||
|
header "macOS Cross-Compile"
|
||||||
|
|
||||||
|
if $SETUP_MAC; then
|
||||||
|
mac_pkgs="$(get_pkgs mac)"
|
||||||
|
if [[ -n "$mac_pkgs" ]]; then
|
||||||
|
install_pkgs "$mac_pkgs" "macOS cross-compile helpers"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Fetch libsodium for macOS
|
||||||
|
if [[ ! -f "$PROJECT_DIR/libs/libsodium-mac/lib/libsodium.a" ]]; then
|
||||||
|
if ! $CHECK_ONLY; then
|
||||||
|
# Requires osxcross — skip gracefully if not available
|
||||||
|
if [[ -d "$PROJECT_DIR/external/osxcross/target" ]] || [[ -d "${OSXCROSS:-}/target" ]]; then
|
||||||
|
info "Building libsodium for macOS target..."
|
||||||
|
"$PROJECT_DIR/scripts/fetch-libsodium.sh" --mac || warn "libsodium-mac build failed"
|
||||||
|
else
|
||||||
|
skip "libsodium-mac (requires osxcross — see README)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
miss "libsodium-mac (not built yet)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
ok "libsodium-mac"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -d "$PROJECT_DIR/external/osxcross/target" ]] || [[ -d "${OSXCROSS:-}/target" ]]; then
|
||||||
|
ok "osxcross"
|
||||||
|
else
|
||||||
|
if $SETUP_MAC; then
|
||||||
|
miss "osxcross (must be set up manually — see README)"
|
||||||
|
else
|
||||||
|
skip "osxcross (use --mac to set up macOS deps)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 5. Sapling parameters ───────────────────────────────────────────────────
|
||||||
|
header "Sapling Parameters"
|
||||||
|
|
||||||
|
SAPLING_DIR=""
|
||||||
|
for d in "$HOME/.zcash-params" "$HOME/.hush-params"; do
|
||||||
|
if [[ -f "$d/sapling-spend.params" && -f "$d/sapling-output.params" ]]; then
|
||||||
|
SAPLING_DIR="$d"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Also check project-local locations
|
||||||
|
if [[ -z "$SAPLING_DIR" ]]; then
|
||||||
|
for d in "$PROJECT_DIR/prebuilt-binaries/dragonxd-linux" "$PROJECT_DIR/prebuilt-binaries/dragonxd-win"; do
|
||||||
|
if [[ -f "$d/sapling-spend.params" && -f "$d/sapling-output.params" ]]; then
|
||||||
|
SAPLING_DIR="$d"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$SAPLING_DIR" ]]; then
|
||||||
|
ok "sapling-spend.params ($(du -h "$SAPLING_DIR/sapling-spend.params" | cut -f1))"
|
||||||
|
ok "sapling-output.params ($(du -h "$SAPLING_DIR/sapling-output.params" | cut -f1))"
|
||||||
|
elif $SETUP_SAPLING; then
|
||||||
|
if ! $CHECK_ONLY; then
|
||||||
|
info "Downloading Sapling parameters (~51 MB)..."
|
||||||
|
PARAMS_DIR="$HOME/.zcash-params"
|
||||||
|
mkdir -p "$PARAMS_DIR"
|
||||||
|
|
||||||
|
SPEND_URL="https://z.cash/downloads/sapling-spend.params"
|
||||||
|
OUTPUT_URL="https://z.cash/downloads/sapling-output.params"
|
||||||
|
|
||||||
|
curl -fSL -o "$PARAMS_DIR/sapling-spend.params" "$SPEND_URL" && \
|
||||||
|
ok "Downloaded sapling-spend.params"
|
||||||
|
curl -fSL -o "$PARAMS_DIR/sapling-output.params" "$OUTPUT_URL" && \
|
||||||
|
ok "Downloaded sapling-output.params"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
skip "Sapling params not found (use --sapling to download, or they'll be extracted at runtime from embedded builds)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 6. DragonX daemon ────────────────────────────────────────────────────────
|
||||||
|
header "DragonX Daemon"
|
||||||
|
|
||||||
|
DRAGONX_SRC="$PROJECT_DIR/external/dragonx"
|
||||||
|
DRAGONXD_LINUX="$PROJECT_DIR/prebuilt-binaries/dragonxd-linux"
|
||||||
|
DRAGONXD_WIN="$PROJECT_DIR/prebuilt-binaries/dragonxd-win"
|
||||||
|
DRAGONXD_MAC="$PROJECT_DIR/prebuilt-binaries/dragonxd-mac"
|
||||||
|
|
||||||
|
DAEMON_BINS="dragonxd dragonx-cli dragonx-tx hushd hush-arrakis-chain hush-cli hush-tx"
|
||||||
|
DAEMON_DATA="asmap.dat sapling-spend.params sapling-output.params"
|
||||||
|
|
||||||
|
# Helper: clone / update dragonx source
|
||||||
|
clone_dragonx_if_needed() {
|
||||||
|
if [[ ! -d "$DRAGONX_SRC" ]]; then
|
||||||
|
info "Cloning dragonx..."
|
||||||
|
git clone https://git.dragonx.is/DragonX/dragonx.git "$DRAGONX_SRC"
|
||||||
|
else
|
||||||
|
ok "dragonx source already present"
|
||||||
|
info "Switching to dragonx branch and pulling latest..."
|
||||||
|
(cd "$DRAGONX_SRC" && git checkout dragonx 2>/dev/null && git pull --ff-only 2>/dev/null || true)
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Helper: copy data files (asmap, sapling params) into a destination dir
|
||||||
|
copy_daemon_data() {
|
||||||
|
local dest="$1"
|
||||||
|
for p in "$DRAGONX_SRC/asmap.dat" "$DRAGONX_SRC/contrib/asmap/asmap.dat"; do
|
||||||
|
if [[ -f "$p" ]]; then
|
||||||
|
cp "$p" "$dest/asmap.dat"
|
||||||
|
ok " Installed asmap.dat"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
for p in sapling-spend.params sapling-output.params; do
|
||||||
|
if [[ -f "$DRAGONX_SRC/$p" ]]; then
|
||||||
|
cp "$DRAGONX_SRC/$p" "$dest/"
|
||||||
|
ok " Installed $p"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Linux daemon ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Skip Linux daemon build if only cross-compile targets were requested
|
||||||
|
# and we already have Linux binaries (avoids contaminating the build tree)
|
||||||
|
SKIP_LINUX_DAEMON=false
|
||||||
|
if ($SETUP_WIN || $SETUP_MAC) && [[ -f "$DRAGONXD_LINUX/dragonxd" || -f "$DRAGONXD_LINUX/hushd" ]]; then
|
||||||
|
SKIP_LINUX_DAEMON=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Clean previous prebuilt daemon binaries so we always rebuild
|
||||||
|
if ! $CHECK_ONLY && ! $SKIP_LINUX_DAEMON; then
|
||||||
|
for f in $DAEMON_BINS $DAEMON_DATA; do
|
||||||
|
rm -f "$DRAGONXD_LINUX/$f" 2>/dev/null || true
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if $CHECK_ONLY; then
|
||||||
|
if [[ -f "$DRAGONXD_LINUX/dragonxd" ]] || [[ -f "$DRAGONXD_LINUX/hushd" ]]; then
|
||||||
|
ok "dragonxd daemon (Linux) present"
|
||||||
|
else
|
||||||
|
miss "dragonxd daemon (Linux) not built"
|
||||||
|
fi
|
||||||
|
elif $SKIP_LINUX_DAEMON; then
|
||||||
|
skip "dragonxd (Linux) — skipped, binaries already present (cross-compile only)"
|
||||||
|
else
|
||||||
|
clone_dragonx_if_needed
|
||||||
|
|
||||||
|
info "Building dragonx daemon for Linux (this may take a while)..."
|
||||||
|
(
|
||||||
|
cd "$DRAGONX_SRC"
|
||||||
|
bash build.sh -j"$NPROC"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Copy binaries to prebuilt-binaries
|
||||||
|
mkdir -p "$DRAGONXD_LINUX"
|
||||||
|
local_found=0
|
||||||
|
for f in dragonxd dragonx-cli dragonx-tx hushd hush-arrakis-chain hush-cli hush-tx; do
|
||||||
|
if [[ -f "$DRAGONX_SRC/src/$f" ]]; then
|
||||||
|
cp "$DRAGONX_SRC/src/$f" "$DRAGONXD_LINUX/"
|
||||||
|
chmod +x "$DRAGONXD_LINUX/$f"
|
||||||
|
ok " Installed $f"
|
||||||
|
local_found=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
copy_daemon_data "$DRAGONXD_LINUX"
|
||||||
|
|
||||||
|
if [[ $local_found -eq 0 ]]; then
|
||||||
|
err "DragonX daemon (Linux) build failed — no binaries found"
|
||||||
|
MISSING=$((MISSING + 1))
|
||||||
|
else
|
||||||
|
ok "DragonX daemon built and installed to prebuilt-binaries/dragonxd-linux/"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Windows daemon (cross-compile, only with --win or --all) ─────────────────
|
||||||
|
|
||||||
|
if ! $SETUP_WIN; then
|
||||||
|
skip "dragonxd (Windows) — use --win to cross-compile"
|
||||||
|
elif $CHECK_ONLY; then
|
||||||
|
if [[ -f "$DRAGONXD_WIN/dragonxd.exe" ]] || [[ -f "$DRAGONXD_WIN/hushd.exe" ]]; then
|
||||||
|
ok "dragonxd daemon (Windows) present"
|
||||||
|
else
|
||||||
|
miss "dragonxd daemon (Windows) not built"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
clone_dragonx_if_needed
|
||||||
|
|
||||||
|
# Clean previous Windows prebuilt binaries
|
||||||
|
if ! $CHECK_ONLY; then
|
||||||
|
rm -f "$DRAGONXD_WIN"/*.exe "$DRAGONXD_WIN"/*.bat 2>/dev/null || true
|
||||||
|
for f in $DAEMON_DATA; do rm -f "$DRAGONXD_WIN/$f" 2>/dev/null || true; done
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "Building dragonx daemon for Windows (cross-compile, this may take a long time)..."
|
||||||
|
(
|
||||||
|
cd "$DRAGONX_SRC"
|
||||||
|
bash build.sh --win-release -j"$NPROC"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Copy binaries from release directory
|
||||||
|
mkdir -p "$DRAGONXD_WIN"
|
||||||
|
local_found=0
|
||||||
|
# Find the release subdirectory (e.g. release/dragonx-1.0.0-win64/)
|
||||||
|
WIN_RELEASE_DIR=$(find "$DRAGONX_SRC/release" -maxdepth 1 -type d -name '*win64*' 2>/dev/null | head -1)
|
||||||
|
for f in dragonxd.exe dragonx-cli.exe dragonx-tx.exe; do
|
||||||
|
if [[ -n "$WIN_RELEASE_DIR" ]] && [[ -f "$WIN_RELEASE_DIR/$f" ]]; then
|
||||||
|
cp "$WIN_RELEASE_DIR/$f" "$DRAGONXD_WIN/"
|
||||||
|
ok " Installed $f"
|
||||||
|
local_found=1
|
||||||
|
elif [[ -f "$DRAGONX_SRC/src/$f" ]]; then
|
||||||
|
cp "$DRAGONX_SRC/src/$f" "$DRAGONXD_WIN/"
|
||||||
|
ok " Installed $f (from src/)"
|
||||||
|
local_found=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
# .bat launchers
|
||||||
|
for f in dragonxd.bat dragonx-cli.bat bootstrap-dragonx.bat; do
|
||||||
|
if [[ -n "$WIN_RELEASE_DIR" ]] && [[ -f "$WIN_RELEASE_DIR/$f" ]]; then
|
||||||
|
cp "$WIN_RELEASE_DIR/$f" "$DRAGONXD_WIN/"
|
||||||
|
ok " Installed $f"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
copy_daemon_data "$DRAGONXD_WIN"
|
||||||
|
|
||||||
|
if [[ $local_found -eq 0 ]]; then
|
||||||
|
err "DragonX daemon (Windows) build failed — no binaries found"
|
||||||
|
MISSING=$((MISSING + 1))
|
||||||
|
else
|
||||||
|
ok "DragonX daemon (Windows) built and installed to prebuilt-binaries/dragonxd-win/"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── macOS daemon (only with --mac or --all) ──────────────────────────────────
|
||||||
|
|
||||||
|
if ! $SETUP_MAC; then
|
||||||
|
skip "dragonxd (macOS) — use --mac to cross-compile"
|
||||||
|
elif $CHECK_ONLY; then
|
||||||
|
if [[ -f "$DRAGONXD_MAC/dragonxd" ]] || [[ -f "$DRAGONXD_MAC/hushd" ]]; then
|
||||||
|
ok "dragonxd daemon (macOS) present"
|
||||||
|
else
|
||||||
|
miss "dragonxd daemon (macOS) not built"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
clone_dragonx_if_needed
|
||||||
|
|
||||||
|
# Clean previous macOS prebuilt binaries
|
||||||
|
if ! $CHECK_ONLY; then
|
||||||
|
for f in $DAEMON_BINS $DAEMON_DATA; do
|
||||||
|
rm -f "$DRAGONXD_MAC/$f" 2>/dev/null || true
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# macOS build requires either native macOS or osxcross
|
||||||
|
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||||
|
info "Building dragonx daemon for macOS (native)..."
|
||||||
|
(
|
||||||
|
cd "$DRAGONX_SRC"
|
||||||
|
bash build.sh -j"$NPROC"
|
||||||
|
)
|
||||||
|
else
|
||||||
|
info "Building dragonx daemon for macOS (requires osxcross)..."
|
||||||
|
OSXCROSS=""
|
||||||
|
for d in "$PROJECT_DIR/external/osxcross" "$HOME/osxcross" "/opt/osxcross"; do
|
||||||
|
if [[ -d "$d/target/bin" ]]; then
|
||||||
|
OSXCROSS="$d"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [[ -z "$OSXCROSS" ]]; then
|
||||||
|
warn "osxcross not found — skipping macOS daemon build"
|
||||||
|
warn "Install osxcross to external/osxcross to enable macOS cross-compilation"
|
||||||
|
else
|
||||||
|
info "Using osxcross at $OSXCROSS"
|
||||||
|
(
|
||||||
|
cd "$DRAGONX_SRC"
|
||||||
|
export PATH="$OSXCROSS/target/bin:$PATH"
|
||||||
|
bash util/build-mac-cross.sh 2>/dev/null || bash util/build-mac.sh
|
||||||
|
)
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Copy binaries
|
||||||
|
mkdir -p "$DRAGONXD_MAC"
|
||||||
|
local_found=0
|
||||||
|
for f in dragonxd dragonx-cli dragonx-tx hushd hush-arrakis-chain hush-cli hush-tx; do
|
||||||
|
if [[ -f "$DRAGONX_SRC/src/$f" ]]; then
|
||||||
|
cp "$DRAGONX_SRC/src/$f" "$DRAGONXD_MAC/"
|
||||||
|
chmod +x "$DRAGONXD_MAC/$f"
|
||||||
|
ok " Installed $f"
|
||||||
|
local_found=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
copy_daemon_data "$DRAGONXD_MAC"
|
||||||
|
|
||||||
|
if [[ $local_found -eq 0 ]]; then
|
||||||
|
warn "DragonX daemon (macOS) — no binaries found (osxcross may be missing)"
|
||||||
|
else
|
||||||
|
ok "DragonX daemon (macOS) built and installed to prebuilt-binaries/dragonxd-mac/"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 7. xmrig-hac (mining binary) ────────────────────────────────────────────
|
||||||
|
header "xmrig-hac Mining Binary"
|
||||||
|
|
||||||
|
XMRIG_SRC="$PROJECT_DIR/external/xmrig-hac"
|
||||||
|
XMRIG_PREBUILT="$PROJECT_DIR/prebuilt-binaries/xmrig-hac"
|
||||||
|
|
||||||
|
# Clean previous prebuilt xmrig binaries so we always rebuild
|
||||||
|
# Only clean the binary for the platform(s) we are actually building,
|
||||||
|
# otherwise a plain ./setup.sh deletes xmrig.exe without rebuilding it.
|
||||||
|
if ! $CHECK_ONLY; then
|
||||||
|
rm -f "$XMRIG_PREBUILT/xmrig" 2>/dev/null || true
|
||||||
|
if $SETUP_WIN; then
|
||||||
|
rm -f "$XMRIG_PREBUILT/xmrig.exe" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Helper: clone xmrig-hac if not present
|
||||||
|
clone_xmrig_if_needed() {
|
||||||
|
if [[ ! -d "$XMRIG_SRC" ]]; then
|
||||||
|
info "Cloning xmrig-hac..."
|
||||||
|
git clone https://git.dragonx.is/dragonx/xmrig-hac.git "$XMRIG_SRC"
|
||||||
|
else
|
||||||
|
ok "xmrig-hac source already present"
|
||||||
|
info "Pulling latest xmrig-hac..."
|
||||||
|
(cd "$XMRIG_SRC" && git pull --ff-only 2>/dev/null || true)
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Linux xmrig ─────────────────────────────────────────────────────────────
|
||||||
|
XMRIG_LINUX="$XMRIG_PREBUILT/xmrig"
|
||||||
|
|
||||||
|
if $CHECK_ONLY; then
|
||||||
|
if [[ -f "$XMRIG_LINUX" ]]; then
|
||||||
|
ok "xmrig (Linux) ($(du -h "$XMRIG_LINUX" | cut -f1))"
|
||||||
|
else
|
||||||
|
miss "xmrig (Linux) not built (run setup without --check to build)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
clone_xmrig_if_needed
|
||||||
|
|
||||||
|
# Clean previous build
|
||||||
|
rm -rf "$XMRIG_SRC/build"
|
||||||
|
|
||||||
|
# Build dependencies (libuv, hwloc, openssl)
|
||||||
|
info "Building xmrig-hac dependencies (libuv, hwloc, openssl)..."
|
||||||
|
(
|
||||||
|
cd "$XMRIG_SRC/scripts"
|
||||||
|
sh build_deps.sh
|
||||||
|
)
|
||||||
|
ok "xmrig-hac dependencies built"
|
||||||
|
|
||||||
|
# Build xmrig
|
||||||
|
info "Building xmrig-hac (Linux)..."
|
||||||
|
mkdir -p "$XMRIG_SRC/build"
|
||||||
|
(
|
||||||
|
cd "$XMRIG_SRC/build"
|
||||||
|
cmake .. \
|
||||||
|
-DCMAKE_BUILD_TYPE=Release \
|
||||||
|
-DWITH_OPENCL=OFF \
|
||||||
|
-DWITH_CUDA=OFF \
|
||||||
|
-DWITH_HWLOC=ON \
|
||||||
|
-DCMAKE_PREFIX_PATH="$XMRIG_SRC/scripts/deps"
|
||||||
|
make -j"$NPROC"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Copy binary to prebuilt-binaries
|
||||||
|
mkdir -p "$XMRIG_PREBUILT"
|
||||||
|
if [[ -f "$XMRIG_SRC/build/xmrig" ]]; then
|
||||||
|
cp "$XMRIG_SRC/build/xmrig" "$XMRIG_LINUX"
|
||||||
|
ok "xmrig (Linux) built and installed to prebuilt-binaries/xmrig-hac/"
|
||||||
|
else
|
||||||
|
err "xmrig (Linux) build failed — binary not found"
|
||||||
|
MISSING=$((MISSING + 1))
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Windows xmrig (cross-compile, only with --win or --all) ─────────────────
|
||||||
|
XMRIG_WIN="$XMRIG_PREBUILT/xmrig.exe"
|
||||||
|
|
||||||
|
if ! $SETUP_WIN; then
|
||||||
|
skip "xmrig.exe (Windows) — use --win to cross-compile"
|
||||||
|
elif $CHECK_ONLY; then
|
||||||
|
if [[ -f "$XMRIG_WIN" ]]; then
|
||||||
|
ok "xmrig.exe (Windows) ($(du -h "$XMRIG_WIN" | cut -f1))"
|
||||||
|
else
|
||||||
|
miss "xmrig.exe (Windows) not built (run setup --win without --check to build)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
clone_xmrig_if_needed
|
||||||
|
|
||||||
|
# Clean previous Windows build
|
||||||
|
rm -rf "$XMRIG_SRC/build-windows"
|
||||||
|
|
||||||
|
info "Building xmrig-hac (Windows cross-compile)..."
|
||||||
|
(
|
||||||
|
cd "$XMRIG_SRC/scripts"
|
||||||
|
bash build_windows.sh
|
||||||
|
)
|
||||||
|
|
||||||
|
# Copy binary to prebuilt-binaries
|
||||||
|
mkdir -p "$XMRIG_PREBUILT"
|
||||||
|
if [[ -f "$XMRIG_SRC/build-windows/xmrig.exe" ]]; then
|
||||||
|
cp "$XMRIG_SRC/build-windows/xmrig.exe" "$XMRIG_WIN"
|
||||||
|
ok "xmrig.exe (Windows) built and installed to prebuilt-binaries/xmrig-hac/"
|
||||||
|
else
|
||||||
|
err "xmrig.exe (Windows) build failed — binary not found"
|
||||||
|
MISSING=$((MISSING + 1))
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 8. Binary directories ───────────────────────────────────────────────────
|
||||||
|
header "Binary Directories"
|
||||||
|
|
||||||
|
for platform in dragonxd-linux dragonxd-win dragonxd-mac xmrig; do
|
||||||
|
dir="$PROJECT_DIR/prebuilt-binaries/$platform"
|
||||||
|
if [[ -d "$dir" ]]; then
|
||||||
|
# Count actual files (not .gitkeep)
|
||||||
|
count=$(find "$dir" -maxdepth 1 -type f ! -name '.gitkeep' | wc -l)
|
||||||
|
if [[ $count -gt 0 ]]; then
|
||||||
|
ok "prebuilt-binaries/$platform/ ($count files)"
|
||||||
|
else
|
||||||
|
skip "prebuilt-binaries/$platform/ (empty — place binaries here)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
if ! $CHECK_ONLY; then
|
||||||
|
mkdir -p "$dir"
|
||||||
|
touch "$dir/.gitkeep"
|
||||||
|
ok "Created prebuilt-binaries/$platform/"
|
||||||
|
else
|
||||||
|
miss "prebuilt-binaries/$platform/"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# ── Summary ──────────────────────────────────────────────────────────────────
|
||||||
|
echo ""
|
||||||
|
echo "═══════════════════════════════════════════"
|
||||||
|
if [[ $MISSING -eq 0 ]]; then
|
||||||
|
echo -e "${GREEN}${BOLD} Setup complete — ready to build!${NC}"
|
||||||
|
echo ""
|
||||||
|
echo " Quick start:"
|
||||||
|
echo " ./build.sh # Dev build"
|
||||||
|
echo " ./build.sh --linux-release # Linux release + AppImage"
|
||||||
|
echo " ./build.sh --win-release # Windows cross-compile"
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}${BOLD} $MISSING item(s) still need attention${NC}"
|
||||||
|
if $CHECK_ONLY; then
|
||||||
|
echo ""
|
||||||
|
echo " Run without --check to install automatically:"
|
||||||
|
echo " ./scripts/setup.sh"
|
||||||
|
echo " ./scripts/setup.sh --all # Include cross-compile + Sapling"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
echo "═══════════════════════════════════════════"
|
||||||
1658
src/app.cpp
182
src/app.h
@@ -10,8 +10,15 @@
|
|||||||
#include <thread>
|
#include <thread>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <unordered_set>
|
||||||
|
#include "data/transaction_history_cache.h"
|
||||||
#include "data/wallet_state.h"
|
#include "data/wallet_state.h"
|
||||||
#include "rpc/connection.h"
|
#include "rpc/connection.h"
|
||||||
|
#include "services/network_refresh_service.h"
|
||||||
|
#include "services/wallet_security_controller.h"
|
||||||
|
#include "services/wallet_security_workflow.h"
|
||||||
|
#include "util/async_task_manager.h"
|
||||||
#include "ui/sidebar.h"
|
#include "ui/sidebar.h"
|
||||||
#include "ui/windows/console_tab.h"
|
#include "ui/windows/console_tab.h"
|
||||||
#include "imgui.h"
|
#include "imgui.h"
|
||||||
@@ -23,7 +30,7 @@ namespace dragonx {
|
|||||||
class RPCWorker;
|
class RPCWorker;
|
||||||
}
|
}
|
||||||
namespace config { class Settings; }
|
namespace config { class Settings; }
|
||||||
namespace daemon { class EmbeddedDaemon; class XmrigManager; }
|
namespace daemon { class DaemonController; class EmbeddedDaemon; class XmrigManager; }
|
||||||
namespace util { class Bootstrap; class SecureVault; }
|
namespace util { class Bootstrap; class SecureVault; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,10 +178,19 @@ public:
|
|||||||
// Pool mining (xmrig)
|
// Pool mining (xmrig)
|
||||||
void startPoolMining(int threads);
|
void startPoolMining(int threads);
|
||||||
void stopPoolMining();
|
void stopPoolMining();
|
||||||
|
int getXmrigRequestedThreads() const {
|
||||||
|
return xmrig_manager_ ? xmrig_manager_->getRequestedThreads() : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mine-when-idle state query
|
||||||
|
bool isIdleMiningActive() const { return idle_mining_active_; }
|
||||||
|
|
||||||
// Peers
|
// Peers
|
||||||
const std::vector<PeerInfo>& getPeers() const { return state_.peers; }
|
const std::vector<PeerInfo>& getPeers() const { return state_.peers; }
|
||||||
const std::vector<BannedPeer>& getBannedPeers() const { return state_.bannedPeers; }
|
const std::vector<BannedPeer>& getBannedPeers() const { return state_.bannedPeers; }
|
||||||
|
bool isPeerRefreshInProgress() const {
|
||||||
|
return network_refresh_.jobInProgress(services::NetworkRefreshService::Job::Peers);
|
||||||
|
}
|
||||||
void banPeer(const std::string& ip, int duration_seconds = 86400);
|
void banPeer(const std::string& ip, int duration_seconds = 86400);
|
||||||
void unbanPeer(const std::string& ip);
|
void unbanPeer(const std::string& ip);
|
||||||
void clearBans();
|
void clearBans();
|
||||||
@@ -193,6 +209,19 @@ public:
|
|||||||
void unfavoriteAddress(const std::string& addr);
|
void unfavoriteAddress(const std::string& addr);
|
||||||
bool isAddressFavorite(const std::string& addr) const;
|
bool isAddressFavorite(const std::string& addr) const;
|
||||||
|
|
||||||
|
// Address metadata (labels, icons, custom ordering)
|
||||||
|
void setAddressLabel(const std::string& addr, const std::string& label);
|
||||||
|
void setAddressIcon(const std::string& addr, const std::string& icon);
|
||||||
|
std::string getAddressLabel(const std::string& addr) const;
|
||||||
|
std::string getAddressIcon(const std::string& addr) const;
|
||||||
|
int getAddressSortOrder(const std::string& addr) const;
|
||||||
|
void setAddressSortOrder(const std::string& addr, int order);
|
||||||
|
int getNextSortOrder() const;
|
||||||
|
void swapAddressOrder(const std::string& a, const std::string& b);
|
||||||
|
bool isMiningAddress(const std::string& addr) const;
|
||||||
|
void setMiningAddress(const std::string& addr, bool mining);
|
||||||
|
void invalidateAddressValidationCache();
|
||||||
|
|
||||||
// Key export/import
|
// Key export/import
|
||||||
void exportPrivateKey(const std::string& address, std::function<void(const std::string&)> callback);
|
void exportPrivateKey(const std::string& address, std::function<void(const std::string&)> callback);
|
||||||
void exportAllKeys(std::function<void(const std::string&)> callback);
|
void exportAllKeys(std::function<void(const std::string&)> callback);
|
||||||
@@ -212,8 +241,14 @@ public:
|
|||||||
void refreshPeerInfo();
|
void refreshPeerInfo();
|
||||||
void refreshMarketData();
|
void refreshMarketData();
|
||||||
|
|
||||||
|
/// @brief Per-category refresh intervals, adjusted by active tab
|
||||||
|
using RefreshIntervals = services::NetworkRefreshService::Intervals;
|
||||||
|
|
||||||
|
/// @brief Get recommended refresh intervals for a given page
|
||||||
|
static RefreshIntervals getIntervalsForPage(ui::NavPage page);
|
||||||
|
|
||||||
// UI navigation
|
// UI navigation
|
||||||
void setCurrentPage(ui::NavPage page) { current_page_ = page; }
|
void setCurrentPage(ui::NavPage page);
|
||||||
ui::NavPage getCurrentPage() const { return current_page_; }
|
ui::NavPage getCurrentPage() const { return current_page_; }
|
||||||
|
|
||||||
// Dialog triggers (used by settings page to open modal dialogs)
|
// Dialog triggers (used by settings page to open modal dialogs)
|
||||||
@@ -241,6 +276,11 @@ public:
|
|||||||
bool isEmbeddedDaemonRunning() const;
|
bool isEmbeddedDaemonRunning() const;
|
||||||
bool isUsingEmbeddedDaemon() const { return use_embedded_daemon_; }
|
bool isUsingEmbeddedDaemon() const { return use_embedded_daemon_; }
|
||||||
void setUseEmbeddedDaemon(bool use) { use_embedded_daemon_ = use; }
|
void setUseEmbeddedDaemon(bool use) { use_embedded_daemon_ = use; }
|
||||||
|
void rescanBlockchain(); // restart daemon with -rescan flag
|
||||||
|
void deleteBlockchainData(); // stop daemon, delete chain data, restart fresh
|
||||||
|
bool stopDaemonForBootstrap(); // stop daemon + disconnect for bootstrap, returns true if was running
|
||||||
|
bool isBootstrapDownloading() const { return bootstrap_downloading_; }
|
||||||
|
void setBootstrapDownloading(bool v) { bootstrap_downloading_ = v; }
|
||||||
|
|
||||||
// Get daemon memory usage in MB (uses embedded daemon handle if available,
|
// Get daemon memory usage in MB (uses embedded daemon handle if available,
|
||||||
// falls back to platform-level process scan for external daemons)
|
// falls back to platform-level process scan for external daemons)
|
||||||
@@ -255,6 +295,8 @@ public:
|
|||||||
|
|
||||||
// Logo texture accessor (wallet branding icon)
|
// Logo texture accessor (wallet branding icon)
|
||||||
ImTextureID getLogoTexture() const { return logo_tex_; }
|
ImTextureID getLogoTexture() const { return logo_tex_; }
|
||||||
|
int getLogoWidth() const { return logo_w_; }
|
||||||
|
int getLogoHeight() const { return logo_h_; }
|
||||||
|
|
||||||
// Coin logo texture accessor (DragonX currency icon for balance tab)
|
// Coin logo texture accessor (DragonX currency icon for balance tab)
|
||||||
ImTextureID getCoinLogoTexture() const { return coin_logo_tex_; }
|
ImTextureID getCoinLogoTexture() const { return coin_logo_tex_; }
|
||||||
@@ -303,9 +345,7 @@ public:
|
|||||||
void showChangePassphraseDialog() { show_change_passphrase_ = true; }
|
void showChangePassphraseDialog() { show_change_passphrase_ = true; }
|
||||||
void showDecryptDialog() {
|
void showDecryptDialog() {
|
||||||
show_decrypt_dialog_ = true;
|
show_decrypt_dialog_ = true;
|
||||||
decrypt_phase_ = 0; // passphrase entry
|
wallet_security_workflow_.reset();
|
||||||
decrypt_status_.clear();
|
|
||||||
decrypt_in_progress_ = false;
|
|
||||||
memset(decrypt_pass_buf_, 0, sizeof(decrypt_pass_buf_));
|
memset(decrypt_pass_buf_, 0, sizeof(decrypt_pass_buf_));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,8 +357,39 @@ public:
|
|||||||
|
|
||||||
/// @brief Check if RPC worker has queued results waiting to be processed
|
/// @brief Check if RPC worker has queued results waiting to be processed
|
||||||
bool hasPendingRPCResults() const;
|
bool hasPendingRPCResults() const;
|
||||||
|
bool hasTransactionSendProgress() const { return send_progress_active_ || send_submissions_in_flight_ > 0 || !pending_opids_.empty(); }
|
||||||
|
std::string transactionSendProgressText() const;
|
||||||
|
std::string transactionRefreshProgressText() const;
|
||||||
|
bool isTransactionRefreshInProgress() const {
|
||||||
|
return network_refresh_.jobInProgress(services::NetworkRefreshService::Job::Transactions);
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
friend class AppDaemonLifecycleRuntime;
|
||||||
|
friend class AppDaemonLifecycleTaskContext;
|
||||||
|
|
||||||
|
bool sendStopCommandSafely(rpc::RPCClient& client, const char* context);
|
||||||
|
void maybeFinishTransactionSendProgress();
|
||||||
|
void upsertPendingSendTransaction(const std::string& opid,
|
||||||
|
const std::string& from,
|
||||||
|
const std::string& to,
|
||||||
|
double amount,
|
||||||
|
const std::string& memo);
|
||||||
|
void markPendingSendTransactionSucceeded(const std::string& opid,
|
||||||
|
const std::string& txid);
|
||||||
|
void removePendingSendTransactions(const std::vector<std::string>& opids,
|
||||||
|
bool restoreBalances);
|
||||||
|
void applyPendingSendBalanceDeltas(bool includeAggregateBalances);
|
||||||
|
std::string transactionHistoryCacheWalletIdentity() const;
|
||||||
|
bool ensureTransactionHistoryCacheUnlockedFor(const std::string& walletIdentity);
|
||||||
|
void unlockTransactionHistoryCacheWithPassphrase(const std::string& passphrase);
|
||||||
|
void loadTransactionHistoryCacheIfAvailable();
|
||||||
|
void storeTransactionHistoryCacheIfAvailable();
|
||||||
|
void wipePendingTransactionHistoryCachePassphrase();
|
||||||
|
void resetTransactionHistoryCacheSession();
|
||||||
|
void pruneShieldedHistoryScanProgress();
|
||||||
|
void invalidateShieldedHistoryScanProgress(bool persistCache);
|
||||||
|
|
||||||
// Subsystems
|
// Subsystems
|
||||||
std::unique_ptr<rpc::RPCClient> rpc_;
|
std::unique_ptr<rpc::RPCClient> rpc_;
|
||||||
std::unique_ptr<rpc::RPCWorker> worker_;
|
std::unique_ptr<rpc::RPCWorker> worker_;
|
||||||
@@ -333,8 +404,9 @@ private:
|
|||||||
rpc::ConnectionConfig saved_config_;
|
rpc::ConnectionConfig saved_config_;
|
||||||
|
|
||||||
std::unique_ptr<config::Settings> settings_;
|
std::unique_ptr<config::Settings> settings_;
|
||||||
std::unique_ptr<daemon::EmbeddedDaemon> embedded_daemon_;
|
std::unique_ptr<daemon::DaemonController> daemon_controller_;
|
||||||
std::unique_ptr<daemon::XmrigManager> xmrig_manager_;
|
std::unique_ptr<daemon::XmrigManager> xmrig_manager_;
|
||||||
|
util::AsyncTaskManager async_tasks_;
|
||||||
bool pending_antivirus_dialog_ = false; // Show Windows Defender help dialog
|
bool pending_antivirus_dialog_ = false; // Show Windows Defender help dialog
|
||||||
|
|
||||||
// Wallet state
|
// Wallet state
|
||||||
@@ -343,16 +415,18 @@ private:
|
|||||||
// Shutdown state
|
// Shutdown state
|
||||||
std::atomic<bool> shutting_down_{false};
|
std::atomic<bool> shutting_down_{false};
|
||||||
std::atomic<bool> shutdown_complete_{false};
|
std::atomic<bool> shutdown_complete_{false};
|
||||||
std::atomic<bool> refresh_in_progress_{false};
|
|
||||||
bool address_list_dirty_ = false; // P8: dedup rebuildAddressList
|
bool address_list_dirty_ = false; // P8: dedup rebuildAddressList
|
||||||
std::string shutdown_status_;
|
std::string shutdown_status_;
|
||||||
std::thread shutdown_thread_;
|
std::thread shutdown_thread_;
|
||||||
float shutdown_timer_ = 0.0f;
|
float shutdown_timer_ = 0.0f;
|
||||||
|
bool force_quit_confirm_ = false;
|
||||||
std::chrono::steady_clock::time_point shutdown_start_time_;
|
std::chrono::steady_clock::time_point shutdown_start_time_;
|
||||||
|
|
||||||
// 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};
|
||||||
std::thread daemon_restart_thread_;
|
|
||||||
|
// Encryption state check timeout
|
||||||
|
float encryption_check_timer_ = 0.0f;
|
||||||
|
|
||||||
// UI State
|
// UI State
|
||||||
bool quit_requested_ = false;
|
bool quit_requested_ = false;
|
||||||
@@ -368,6 +442,7 @@ private:
|
|||||||
bool use_embedded_daemon_ = true;
|
bool use_embedded_daemon_ = true;
|
||||||
std::string daemon_status_;
|
std::string daemon_status_;
|
||||||
mutable std::string daemon_mem_diag_; // diagnostic info for daemon memory detection
|
mutable std::string daemon_mem_diag_; // diagnostic info for daemon memory detection
|
||||||
|
size_t daemon_output_offset_ = 0; // for incremental output parsing (rescan detection)
|
||||||
|
|
||||||
// Export/Import state
|
// Export/Import state
|
||||||
std::string export_result_;
|
std::string export_result_;
|
||||||
@@ -381,6 +456,7 @@ private:
|
|||||||
// Connection
|
// Connection
|
||||||
std::string connection_status_ = "Disconnected";
|
std::string connection_status_ = "Disconnected";
|
||||||
bool connection_in_progress_ = false;
|
bool connection_in_progress_ = false;
|
||||||
|
bool remote_rpc_plaintext_warning_shown_ = false;
|
||||||
float loading_timer_ = 0.0f; // spinner animation for loading overlay
|
float loading_timer_ = 0.0f; // spinner animation for loading overlay
|
||||||
|
|
||||||
// Current page (sidebar navigation)
|
// Current page (sidebar navigation)
|
||||||
@@ -418,18 +494,8 @@ private:
|
|||||||
std::string pending_memo_;
|
std::string pending_memo_;
|
||||||
std::string pending_label_;
|
std::string pending_label_;
|
||||||
|
|
||||||
// Timers (in seconds since last update)
|
// Per-category refresh timers, policy, and worker queue guards.
|
||||||
float refresh_timer_ = 0.0f;
|
services::NetworkRefreshService network_refresh_;
|
||||||
float price_timer_ = 0.0f;
|
|
||||||
float fast_refresh_timer_ = 0.0f; // For mining stats
|
|
||||||
|
|
||||||
// Refresh intervals (seconds)
|
|
||||||
static constexpr float REFRESH_INTERVAL = 5.0f;
|
|
||||||
static constexpr float PRICE_INTERVAL = 60.0f;
|
|
||||||
static constexpr float FAST_REFRESH_INTERVAL = 1.0f;
|
|
||||||
|
|
||||||
// Mining refresh guard (prevents worker queue pileup)
|
|
||||||
std::atomic<bool> mining_refresh_in_progress_{false};
|
|
||||||
int mining_slow_counter_ = 0; // counts fast ticks; fires slow refresh every N
|
int mining_slow_counter_ = 0; // counts fast ticks; fires slow refresh every N
|
||||||
|
|
||||||
// Mining toggle guard (prevents concurrent setgenerate calls)
|
// Mining toggle guard (prevents concurrent setgenerate calls)
|
||||||
@@ -440,29 +506,69 @@ private:
|
|||||||
|
|
||||||
// P4: Incremental transaction cache
|
// P4: Incremental transaction cache
|
||||||
int last_tx_block_height_ = -1; // block height at last full tx fetch
|
int last_tx_block_height_ = -1; // block height at last full tx fetch
|
||||||
|
static constexpr int MAX_VIEWTX_PER_CYCLE = 25; // cap z_viewtransaction calls per refresh
|
||||||
|
std::size_t shielded_history_scan_cursor_ = 0;
|
||||||
|
bool shielded_history_scan_pending_ = false;
|
||||||
|
std::unordered_map<std::string, int> shielded_history_scan_heights_;
|
||||||
|
|
||||||
|
// P4b: z_viewtransaction result cache — avoids re-calling the RPC for
|
||||||
|
// txids we've already enriched. Keyed by txid.
|
||||||
|
using ViewTxCacheEntry = services::NetworkRefreshService::TransactionViewCacheEntry;
|
||||||
|
services::NetworkRefreshService::TransactionViewCache viewtx_cache_;
|
||||||
|
|
||||||
|
// P4c: Confirmed transaction cache — deeply-confirmed txns (>= 10 confs)
|
||||||
|
// are accumulated here and reused across refresh cycles. Only
|
||||||
|
// recent/unconfirmed txns are re-fetched from the daemon each time.
|
||||||
|
std::vector<TransactionInfo> confirmed_tx_cache_;
|
||||||
|
std::unordered_set<std::string> confirmed_tx_ids_; // fast lookup
|
||||||
|
int confirmed_cache_block_ = -1; // block height when cache was last built
|
||||||
|
|
||||||
// Dirty flags for demand-driven refresh
|
// Dirty flags for demand-driven refresh
|
||||||
bool addresses_dirty_ = true; // true → refreshAddresses() will run
|
bool addresses_dirty_ = true; // true → refreshAddresses() will run
|
||||||
|
bool address_validation_cache_dirty_ = true;
|
||||||
|
bool transactions_dirty_ = false; // true → force tx refresh regardless of block height
|
||||||
bool encryption_state_prefetched_ = false; // suppress duplicate getwalletinfo on connect
|
bool encryption_state_prefetched_ = false; // suppress duplicate getwalletinfo on connect
|
||||||
|
bool rescan_status_poll_in_progress_ = false;
|
||||||
|
bool opid_poll_in_progress_ = false;
|
||||||
|
|
||||||
|
// Pending z_sendmany operation tracking
|
||||||
|
bool send_progress_active_ = false;
|
||||||
|
int send_submissions_in_flight_ = 0;
|
||||||
|
std::vector<std::string> pending_opids_; // opids to poll for completion
|
||||||
|
struct PendingSendInfo {
|
||||||
|
std::string from;
|
||||||
|
std::string to;
|
||||||
|
std::string memo;
|
||||||
|
double amount = 0.0;
|
||||||
|
std::int64_t timestamp = 0;
|
||||||
|
};
|
||||||
|
std::unordered_map<std::string, PendingSendInfo> pending_send_info_;
|
||||||
|
// Txids from completed z_sendmany operations.
|
||||||
|
// Ensures shielded sends are discoverable by z_viewtransaction
|
||||||
|
// even when they don't appear in listtransactions or
|
||||||
|
// z_listreceivedbyaddress.
|
||||||
|
std::unordered_set<std::string> send_txids_;
|
||||||
|
|
||||||
// First-run wizard state
|
// First-run wizard state
|
||||||
WizardPhase wizard_phase_ = WizardPhase::None;
|
WizardPhase wizard_phase_ = WizardPhase::None;
|
||||||
std::unique_ptr<util::Bootstrap> bootstrap_;
|
std::unique_ptr<util::Bootstrap> bootstrap_;
|
||||||
|
bool bootstrap_downloading_ = false; // true while settings bootstrap dialog is active
|
||||||
std::string wizard_pending_passphrase_; // held until daemon connects
|
std::string wizard_pending_passphrase_; // held until daemon connects
|
||||||
std::string wizard_saved_passphrase_; // held until PinSetup completes/skipped
|
std::string wizard_saved_passphrase_; // held until PinSetup completes/skipped
|
||||||
|
|
||||||
// Deferred encryption (wizard background task)
|
// Wallet security flow state shared by wizard/settings encryption paths.
|
||||||
std::string deferred_encrypt_passphrase_;
|
services::WalletSecurityController wallet_security_;
|
||||||
std::string deferred_encrypt_pin_;
|
services::WalletSecurityWorkflow wallet_security_workflow_;
|
||||||
bool deferred_encrypt_pending_ = false;
|
|
||||||
|
|
||||||
// Wizard: stopping an external daemon before bootstrap
|
// Wizard: stopping an external daemon before bootstrap
|
||||||
bool wizard_stopping_external_ = false;
|
bool wizard_stopping_external_ = false;
|
||||||
std::string wizard_stop_status_;
|
std::string wizard_stop_status_;
|
||||||
std::thread wizard_stop_thread_;
|
|
||||||
|
|
||||||
// PIN vault
|
// PIN vault
|
||||||
std::unique_ptr<util::SecureVault> vault_;
|
std::unique_ptr<util::SecureVault> vault_;
|
||||||
|
data::TransactionHistoryCache transaction_history_cache_;
|
||||||
|
std::string pending_transaction_history_cache_passphrase_;
|
||||||
|
bool transaction_history_cache_loaded_ = false;
|
||||||
|
|
||||||
// Lock screen state
|
// Lock screen state
|
||||||
bool lock_screen_was_visible_ = false; // tracks lock→unlock transitions for auto-focus
|
bool lock_screen_was_visible_ = false; // tracks lock→unlock transitions for auto-focus
|
||||||
@@ -504,10 +610,7 @@ private:
|
|||||||
|
|
||||||
// Decrypt wallet dialog state
|
// Decrypt wallet dialog state
|
||||||
bool show_decrypt_dialog_ = false;
|
bool show_decrypt_dialog_ = false;
|
||||||
int decrypt_phase_ = 0; // 0=passphrase, 1=working, 2=done, 3=error
|
|
||||||
char decrypt_pass_buf_[256] = {};
|
char decrypt_pass_buf_[256] = {};
|
||||||
std::string decrypt_status_;
|
|
||||||
bool decrypt_in_progress_ = false;
|
|
||||||
|
|
||||||
// Wizard PIN setup state
|
// Wizard PIN setup state
|
||||||
char wizard_pin_buf_[16] = {};
|
char wizard_pin_buf_[16] = {};
|
||||||
@@ -517,6 +620,10 @@ private:
|
|||||||
// Auto-lock on idle
|
// Auto-lock on idle
|
||||||
std::chrono::steady_clock::time_point last_interaction_ = std::chrono::steady_clock::now();
|
std::chrono::steady_clock::time_point last_interaction_ = std::chrono::steady_clock::now();
|
||||||
|
|
||||||
|
// Mine-when-idle: auto-start/stop mining based on system idle state
|
||||||
|
bool idle_mining_active_ = false; // true when mining was auto-started by idle detection
|
||||||
|
bool idle_scaled_to_idle_ = false; // true when threads have been scaled up for idle
|
||||||
|
|
||||||
// Private methods - rendering
|
// Private methods - rendering
|
||||||
void renderStatusBar();
|
void renderStatusBar();
|
||||||
void renderAboutDialog();
|
void renderAboutDialog();
|
||||||
@@ -535,15 +642,26 @@ private:
|
|||||||
void tryConnect();
|
void tryConnect();
|
||||||
void onConnected();
|
void onConnected();
|
||||||
void onDisconnected(const std::string& reason);
|
void onDisconnected(const std::string& reason);
|
||||||
|
void applyDefaultBanlist();
|
||||||
|
|
||||||
// Private methods - data refresh
|
// Private methods - data refresh
|
||||||
void refreshData();
|
void refreshData(); // Orchestrator: dispatches per-category refreshes
|
||||||
void refreshBalance();
|
void refreshCoreData(); // Balance + blockchain info (can use fast_worker_)
|
||||||
void refreshAddresses();
|
void refreshAddressData(); // Address lists + balances
|
||||||
void refreshTransactions();
|
void refreshTransactionData(); // Transaction list + z_viewtransaction enrichment
|
||||||
|
void refreshRecentTransactionData(); // Lightweight recent/unconfirmed tx poll
|
||||||
|
bool refreshEncryptionState(); // Wallet encryption/lock state
|
||||||
|
void refreshBalance(); // Legacy: balance-only refresh (used by specific callers)
|
||||||
|
void refreshAddresses(); // Legacy: standalone address refresh
|
||||||
void refreshPrice();
|
void refreshPrice();
|
||||||
void refreshWalletEncryptionState();
|
void refreshWalletEncryptionState();
|
||||||
|
void applyRefreshPolicy(ui::NavPage page);
|
||||||
|
bool currentPageNeedsWalletDataRefresh() const;
|
||||||
|
bool shouldRunWalletTransactionRefresh() const;
|
||||||
|
bool shouldRefreshTransactions() const;
|
||||||
|
bool shouldRefreshRecentTransactions() const;
|
||||||
void checkAutoLock();
|
void checkAutoLock();
|
||||||
|
void checkIdleMining();
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace dragonx
|
} // namespace dragonx
|
||||||
|
|||||||
2415
src/app_network.cpp
@@ -10,6 +10,7 @@
|
|||||||
#include "rpc/rpc_worker.h"
|
#include "rpc/rpc_worker.h"
|
||||||
#include "rpc/connection.h"
|
#include "rpc/connection.h"
|
||||||
#include "config/settings.h"
|
#include "config/settings.h"
|
||||||
|
#include "daemon/daemon_controller.h"
|
||||||
#include "daemon/embedded_daemon.h"
|
#include "daemon/embedded_daemon.h"
|
||||||
#include "ui/notifications.h"
|
#include "ui/notifications.h"
|
||||||
#include "ui/material/color_theme.h"
|
#include "ui/material/color_theme.h"
|
||||||
@@ -39,13 +40,43 @@ namespace dragonx {
|
|||||||
|
|
||||||
using json = nlohmann::json;
|
using json = nlohmann::json;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
struct WizardLowSpecSnapshot {
|
||||||
|
bool valid = false;
|
||||||
|
float blur = 0.0f;
|
||||||
|
float uiOp = 0.0f;
|
||||||
|
bool fx = false;
|
||||||
|
bool scanline = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct WizardUiState {
|
||||||
|
float blur_amount = 1.5f;
|
||||||
|
bool theme_effects = true;
|
||||||
|
float ui_opacity = 1.0f;
|
||||||
|
bool low_spec = false;
|
||||||
|
bool scanline = true;
|
||||||
|
std::string balance_layout = "classic";
|
||||||
|
int language_index = 0;
|
||||||
|
bool appearance_init = false;
|
||||||
|
WizardLowSpecSnapshot low_spec_snapshot;
|
||||||
|
float card0_max_h = 0.0f;
|
||||||
|
float card1_max_h = 0.0f;
|
||||||
|
double external_last_check = -10.0;
|
||||||
|
bool daemon_prestarted = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
WizardUiState s_wizardUi;
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
void App::restartWizard()
|
void App::restartWizard()
|
||||||
{
|
{
|
||||||
DEBUG_LOGF("[App] Restarting setup wizard — stopping daemon...\n");
|
DEBUG_LOGF("[App] Restarting setup wizard — stopping daemon...\n");
|
||||||
|
|
||||||
// Reset crash counter for fresh wizard attempt
|
// Reset crash counter for fresh wizard attempt
|
||||||
if (embedded_daemon_) {
|
if (daemon_controller_) {
|
||||||
embedded_daemon_->resetCrashCount();
|
daemon_controller_->resetCrashCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disconnect RPC
|
// Disconnect RPC
|
||||||
@@ -56,10 +87,11 @@ void App::restartWizard()
|
|||||||
|
|
||||||
// Stop the embedded daemon in a background thread to avoid
|
// Stop the embedded daemon in a background thread to avoid
|
||||||
// blocking the UI for up to 32 seconds (RPC stop + process wait).
|
// blocking the UI for up to 32 seconds (RPC stop + process wait).
|
||||||
if (embedded_daemon_ && isEmbeddedDaemonRunning()) {
|
if (daemon_controller_ && isEmbeddedDaemonRunning()) {
|
||||||
std::thread([this]() {
|
async_tasks_.submit("wizard-restart-stop-daemon", [this](const util::AsyncTaskManager::Token& token) {
|
||||||
|
if (token.cancelled()) return;
|
||||||
stopEmbeddedDaemon();
|
stopEmbeddedDaemon();
|
||||||
}).detach();
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enter wizard — the wizard completion handler already calls
|
// Enter wizard — the wizard completion handler already calls
|
||||||
@@ -73,6 +105,7 @@ void App::restartWizard()
|
|||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
|
|
||||||
void App::renderFirstRunWizard() {
|
void App::renderFirstRunWizard() {
|
||||||
|
auto& wizardUi = s_wizardUi;
|
||||||
ImGuiViewport* viewport = ImGui::GetMainViewport();
|
ImGuiViewport* viewport = ImGui::GetMainViewport();
|
||||||
ImGui::SetNextWindowPos(viewport->WorkPos);
|
ImGui::SetNextWindowPos(viewport->WorkPos);
|
||||||
ImGui::SetNextWindowSize(viewport->WorkSize);
|
ImGui::SetNextWindowSize(viewport->WorkSize);
|
||||||
@@ -94,21 +127,6 @@ void App::renderFirstRunWizard() {
|
|||||||
ImU32 bgCol = ui::material::Surface();
|
ImU32 bgCol = ui::material::Surface();
|
||||||
dl->AddRectFilled(winPos, ImVec2(winPos.x + winSize.x, winPos.y + winSize.y), bgCol);
|
dl->AddRectFilled(winPos, ImVec2(winPos.x + winSize.x, winPos.y + winSize.y), bgCol);
|
||||||
|
|
||||||
// Handle Done/None — wizard complete
|
|
||||||
if (wizard_phase_ == WizardPhase::Done || wizard_phase_ == WizardPhase::None) {
|
|
||||||
wizard_phase_ = WizardPhase::None;
|
|
||||||
if (!state_.connected) {
|
|
||||||
if (isUsingEmbeddedDaemon() && !isEmbeddedDaemonRunning()) {
|
|
||||||
startEmbeddedDaemon();
|
|
||||||
}
|
|
||||||
tryConnect();
|
|
||||||
}
|
|
||||||
settings_->setWizardCompleted(true);
|
|
||||||
settings_->save();
|
|
||||||
ImGui::End();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Determine which of the 3 masonry sections is focused ---
|
// --- Determine which of the 3 masonry sections is focused ---
|
||||||
// 0 = Appearance, 1 = Bootstrap, 2 = Encrypt + PIN
|
// 0 = Appearance, 1 = Bootstrap, 2 = Encrypt + PIN
|
||||||
int focusIdx = 0;
|
int focusIdx = 0;
|
||||||
@@ -258,15 +276,14 @@ void App::renderFirstRunWizard() {
|
|||||||
(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;
|
||||||
|
|
||||||
// Statics for appearance settings
|
float& wiz_blur_amount = wizardUi.blur_amount;
|
||||||
static float wiz_blur_amount = 1.5f;
|
bool& wiz_theme_effects = wizardUi.theme_effects;
|
||||||
static bool wiz_theme_effects = true;
|
float& wiz_ui_opacity = wizardUi.ui_opacity;
|
||||||
static float wiz_ui_opacity = 1.0f;
|
bool& wiz_low_spec = wizardUi.low_spec;
|
||||||
static bool wiz_low_spec = false;
|
bool& wiz_scanline = wizardUi.scanline;
|
||||||
static bool wiz_scanline = true;
|
std::string& wiz_balance_layout = wizardUi.balance_layout;
|
||||||
static std::string wiz_balance_layout = "classic";
|
int& wiz_language_index = wizardUi.language_index;
|
||||||
static int wiz_language_index = 0;
|
bool& wiz_appearance_init = wizardUi.appearance_init;
|
||||||
static bool wiz_appearance_init = false;
|
|
||||||
if (!wiz_appearance_init) {
|
if (!wiz_appearance_init) {
|
||||||
wiz_blur_amount = settings_->getBlurMultiplier();
|
wiz_blur_amount = settings_->getBlurMultiplier();
|
||||||
wiz_theme_effects = settings_->getThemeEffectsEnabled();
|
wiz_theme_effects = settings_->getThemeEffectsEnabled();
|
||||||
@@ -413,7 +430,7 @@ void App::renderFirstRunWizard() {
|
|||||||
|
|
||||||
// --- Low-spec mode checkbox ---
|
// --- Low-spec mode checkbox ---
|
||||||
// Snapshot for restoring settings when low-spec is turned off
|
// Snapshot for restoring settings when low-spec is turned off
|
||||||
static struct { bool valid; float blur; float uiOp; bool fx; bool scanline; } wiz_lsSnap = {};
|
WizardLowSpecSnapshot& wiz_lsSnap = wizardUi.low_spec_snapshot;
|
||||||
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(cx, cy));
|
ImGui::SetCursorScreenPos(ImVec2(cx, cy));
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 4.0f * dp);
|
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 4.0f * dp);
|
||||||
@@ -611,7 +628,7 @@ void App::renderFirstRunWizard() {
|
|||||||
|
|
||||||
cy += cardPad;
|
cy += cardPad;
|
||||||
// Lock card height to the tallest content ever seen
|
// Lock card height to the tallest content ever seen
|
||||||
static float card0MaxH = 0.0f;
|
float& card0MaxH = wizardUi.card0_max_h;
|
||||||
card0MaxH = std::max(card0MaxH, cy - card0Top);
|
card0MaxH = std::max(card0MaxH, cy - card0Top);
|
||||||
card0Bot = card0Top + card0MaxH;
|
card0Bot = card0Top + card0MaxH;
|
||||||
|
|
||||||
@@ -673,8 +690,13 @@ void App::renderFirstRunWizard() {
|
|||||||
} else {
|
} else {
|
||||||
auto prog = bootstrap_->getProgress();
|
auto prog = bootstrap_->getProgress();
|
||||||
|
|
||||||
const char* statusTitle = (prog.state == util::Bootstrap::State::Downloading)
|
const char* statusTitle;
|
||||||
? "Downloading bootstrap..." : "Extracting blockchain data...";
|
if (prog.state == util::Bootstrap::State::Downloading)
|
||||||
|
statusTitle = "Downloading bootstrap...";
|
||||||
|
else if (prog.state == util::Bootstrap::State::Verifying)
|
||||||
|
statusTitle = "Verifying checksums...";
|
||||||
|
else
|
||||||
|
statusTitle = "Extracting blockchain data...";
|
||||||
dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(cx, cy), textCol, statusTitle);
|
dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(cx, cy), textCol, statusTitle);
|
||||||
cy += bodyFont->LegacySize + 12.0f * dp;
|
cy += bodyFont->LegacySize + 12.0f * dp;
|
||||||
|
|
||||||
@@ -706,6 +728,28 @@ void App::renderFirstRunWizard() {
|
|||||||
dimCol, "(wallet.dat is protected)");
|
dimCol, "(wallet.dat is protected)");
|
||||||
cy += captionFont->LegacySize + 6.0f * dp;
|
cy += captionFont->LegacySize + 6.0f * dp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Daemon status indicator
|
||||||
|
{
|
||||||
|
bool daemonUp = isEmbeddedDaemonRunning();
|
||||||
|
const std::string& dStatus = getDaemonStatus();
|
||||||
|
ImU32 dotCol = daemonUp ? IM_COL32(76, 175, 80, 200) // green
|
||||||
|
: IM_COL32(120, 120, 120, 160); // gray
|
||||||
|
if (dStatus.find("Stopping") != std::string::npos)
|
||||||
|
dotCol = IM_COL32(255, 167, 38, 200); // orange
|
||||||
|
float dotR = 3.5f * dp;
|
||||||
|
dl->AddCircleFilled(ImVec2(cx + dotR, cy + captionFont->LegacySize * 0.5f),
|
||||||
|
dotR, dotCol);
|
||||||
|
const char* label = daemonUp ? (dStatus.find("Stopping") != std::string::npos
|
||||||
|
? "Daemon stopping..."
|
||||||
|
: "Daemon running")
|
||||||
|
: "Daemon stopped";
|
||||||
|
dl->AddText(captionFont, captionFont->LegacySize,
|
||||||
|
ImVec2(cx + dotR * 2.0f + 6.0f * dp, cy),
|
||||||
|
(dimCol & 0x00FFFFFF) | IM_COL32(0,0,0,140), label);
|
||||||
|
cy += captionFont->LegacySize + 6.0f * dp;
|
||||||
|
}
|
||||||
|
|
||||||
cy += 12.0f * dp;
|
cy += 12.0f * dp;
|
||||||
|
|
||||||
// Cancel button
|
// Cancel button
|
||||||
@@ -761,6 +805,8 @@ void App::renderFirstRunWizard() {
|
|||||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||||
if (ImGui::Button("Retry##bs", ImVec2(btnW2, btnH2))) {
|
if (ImGui::Button("Retry##bs", ImVec2(btnW2, btnH2))) {
|
||||||
|
// Stop embedded daemon before bootstrap to avoid chain data corruption
|
||||||
|
stopDaemonForBootstrap();
|
||||||
bootstrap_ = std::make_unique<util::Bootstrap>();
|
bootstrap_ = std::make_unique<util::Bootstrap>();
|
||||||
std::string dataDir = util::Platform::getDragonXDataDir();
|
std::string dataDir = util::Platform::getDragonXDataDir();
|
||||||
bootstrap_->start(dataDir);
|
bootstrap_->start(dataDir);
|
||||||
@@ -787,17 +833,23 @@ void App::renderFirstRunWizard() {
|
|||||||
if (isFocused) {
|
if (isFocused) {
|
||||||
static std::atomic<bool> s_extCached{false};
|
static std::atomic<bool> s_extCached{false};
|
||||||
static std::atomic<bool> s_checkInFlight{false};
|
static std::atomic<bool> s_checkInFlight{false};
|
||||||
static double s_extLastCheck = -10.0;
|
double& s_extLastCheck = wizardUi.external_last_check;
|
||||||
double now = ImGui::GetTime();
|
double now = ImGui::GetTime();
|
||||||
if (now - s_extLastCheck >= 2.0 && !s_checkInFlight.load()) {
|
if (now - s_extLastCheck >= 2.0 && !s_checkInFlight.load()) {
|
||||||
s_extLastCheck = now;
|
s_extLastCheck = now;
|
||||||
bool embeddedRunning = isEmbeddedDaemonRunning();
|
bool embeddedRunning = isEmbeddedDaemonRunning();
|
||||||
s_checkInFlight.store(true);
|
s_checkInFlight.store(true);
|
||||||
std::thread([embeddedRunning]() {
|
async_tasks_.submit("wizard-external-daemon-check", [embeddedRunning](const util::AsyncTaskManager::Token& token) {
|
||||||
|
if (token.cancelled()) {
|
||||||
|
s_checkInFlight.store(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
bool inUse = daemon::EmbeddedDaemon::isRpcPortInUse();
|
bool inUse = daemon::EmbeddedDaemon::isRpcPortInUse();
|
||||||
s_extCached.store(inUse && !embeddedRunning);
|
if (!token.cancelled()) {
|
||||||
|
s_extCached.store(inUse && !embeddedRunning);
|
||||||
|
}
|
||||||
s_checkInFlight.store(false);
|
s_checkInFlight.store(false);
|
||||||
}).detach();
|
});
|
||||||
}
|
}
|
||||||
externalRunning = s_extCached.load();
|
externalRunning = s_extCached.load();
|
||||||
}
|
}
|
||||||
@@ -838,19 +890,19 @@ void App::renderFirstRunWizard() {
|
|||||||
if (ImGui::Button("Stop Daemon##wiz", ImVec2(stopW, btnH2))) {
|
if (ImGui::Button("Stop Daemon##wiz", ImVec2(stopW, btnH2))) {
|
||||||
wizard_stopping_external_ = true;
|
wizard_stopping_external_ = true;
|
||||||
wizard_stop_status_ = "Sending stop command...";
|
wizard_stop_status_ = "Sending stop command...";
|
||||||
if (wizard_stop_thread_.joinable()) wizard_stop_thread_.join();
|
async_tasks_.submit("wizard-stop-external-daemon", [this](const util::AsyncTaskManager::Token& token) {
|
||||||
wizard_stop_thread_ = std::thread([this]() {
|
|
||||||
auto config = rpc::Connection::autoDetectConfig();
|
auto config = rpc::Connection::autoDetectConfig();
|
||||||
if (!config.rpcuser.empty() && !config.rpcpassword.empty()) {
|
if (!config.rpcuser.empty() && !config.rpcpassword.empty()) {
|
||||||
auto tmp_rpc = std::make_unique<rpc::RPCClient>();
|
auto tmp_rpc = std::make_unique<rpc::RPCClient>();
|
||||||
if (tmp_rpc->connect(config.host, config.port,
|
if (tmp_rpc->connect(config.host, config.port,
|
||||||
config.rpcuser, config.rpcpassword)) {
|
config.rpcuser, config.rpcpassword,
|
||||||
try { tmp_rpc->call("stop"); } catch (...) {}
|
config.use_tls)) {
|
||||||
|
sendStopCommandSafely(*tmp_rpc, "Wizard external daemon stop");
|
||||||
tmp_rpc->disconnect();
|
tmp_rpc->disconnect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
wizard_stop_status_ = "Waiting for daemon to shut down...";
|
wizard_stop_status_ = "Waiting for daemon to shut down...";
|
||||||
for (int i = 0; i < 60; i++) {
|
for (int i = 0; i < 60 && !token.cancelled(); i++) {
|
||||||
std::this_thread::sleep_for(std::chrono::seconds(1));
|
std::this_thread::sleep_for(std::chrono::seconds(1));
|
||||||
if (!daemon::EmbeddedDaemon::isRpcPortInUse()) {
|
if (!daemon::EmbeddedDaemon::isRpcPortInUse()) {
|
||||||
wizard_stop_status_ = "Daemon stopped.";
|
wizard_stop_status_ = "Daemon stopped.";
|
||||||
@@ -858,6 +910,7 @@ void App::renderFirstRunWizard() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (token.cancelled()) return;
|
||||||
wizard_stop_status_ = "Daemon did not stop — try manually.";
|
wizard_stop_status_ = "Daemon did not stop — try manually.";
|
||||||
wizard_stopping_external_ = false;
|
wizard_stopping_external_ = false;
|
||||||
});
|
});
|
||||||
@@ -889,27 +942,52 @@ void App::renderFirstRunWizard() {
|
|||||||
ImU32 warnCol = (textCol & 0x00FFFFFF) | ((ImU32)(255 * warnOpacity) << 24);
|
ImU32 warnCol = (textCol & 0x00FFFFFF) | ((ImU32)(255 * warnOpacity) << 24);
|
||||||
float iw = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, ICON_MD_WARNING).x;
|
float iw = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, ICON_MD_WARNING).x;
|
||||||
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx, cy), warnCol, ICON_MD_WARNING);
|
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx, cy), warnCol, ICON_MD_WARNING);
|
||||||
const char* twText = "Only use bootstrap.dragonx.is. Using files from untrusted sources could compromise your node.";
|
const char* twText = "Only use bootstrap.dragonx.is or bootstrap2.dragonx.is. Using files from untrusted sources could compromise your node.";
|
||||||
float twWrap = contentW - iw - 4.0f * dp;
|
float twWrap = contentW - iw - 4.0f * dp;
|
||||||
ImVec2 twSize = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, twWrap, twText);
|
ImVec2 twSize = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, twWrap, twText);
|
||||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx + iw + 4.0f * dp, cy), warnCol, twText, nullptr, twWrap);
|
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx + iw + 4.0f * dp, cy), warnCol, twText, nullptr, twWrap);
|
||||||
cy += twSize.y + 12.0f * dp;
|
cy += twSize.y + 12.0f * dp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Daemon status indicator (subtle, before buttons)
|
||||||
|
{
|
||||||
|
bool daemonUp = isEmbeddedDaemonRunning();
|
||||||
|
const std::string& dStatus = getDaemonStatus();
|
||||||
|
ImU32 dotCol = daemonUp ? IM_COL32(76, 175, 80, 200)
|
||||||
|
: IM_COL32(120, 120, 120, 160);
|
||||||
|
if (dStatus.find("Stopping") != std::string::npos)
|
||||||
|
dotCol = IM_COL32(255, 167, 38, 200);
|
||||||
|
float dotR = 3.5f * dp;
|
||||||
|
dl->AddCircleFilled(ImVec2(cx + dotR, cy + captionFont->LegacySize * 0.5f),
|
||||||
|
dotR, dotCol);
|
||||||
|
const char* label = daemonUp ? (dStatus.find("Stopping") != std::string::npos
|
||||||
|
? "Daemon stopping..."
|
||||||
|
: "Daemon running")
|
||||||
|
: "Daemon stopped";
|
||||||
|
dl->AddText(captionFont, captionFont->LegacySize,
|
||||||
|
ImVec2(cx + dotR * 2.0f + 6.0f * dp, cy),
|
||||||
|
(dimCol & 0x00FFFFFF) | IM_COL32(0,0,0,140), label);
|
||||||
|
cy += captionFont->LegacySize + 10.0f * dp;
|
||||||
|
}
|
||||||
|
|
||||||
// Buttons (only when focused)
|
// Buttons (only when focused)
|
||||||
if (isFocused) {
|
if (isFocused) {
|
||||||
float dlBtnW = 180.0f * dp;
|
float dlBtnW = 150.0f * dp;
|
||||||
|
float mirrorW = 150.0f * dp;
|
||||||
float skipW2 = 80.0f * dp;
|
float skipW2 = 80.0f * dp;
|
||||||
float btnH2 = 40.0f * dp;
|
float btnH2 = 40.0f * dp;
|
||||||
float totalBW = dlBtnW + 12.0f * dp + skipW2;
|
float totalBW = dlBtnW + 8.0f * dp + mirrorW + 8.0f * dp + skipW2;
|
||||||
float bx = rightX + (colW - totalBW) * 0.5f;
|
float bx = rightX + (colW - totalBW) * 0.5f;
|
||||||
|
|
||||||
|
// --- Download button (main / Cloudflare) ---
|
||||||
ImGui::SetCursorScreenPos(ImVec2(bx, cy));
|
ImGui::SetCursorScreenPos(ImVec2(bx, cy));
|
||||||
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(ui::material::Primary()));
|
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(ui::material::Primary()));
|
||||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
|
||||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||||
if (ImGui::Button("Download##bs", ImVec2(dlBtnW, btnH2))) {
|
if (ImGui::Button("Download##bs", ImVec2(dlBtnW, btnH2))) {
|
||||||
|
// Stop embedded daemon before bootstrap to avoid chain data corruption
|
||||||
|
stopDaemonForBootstrap();
|
||||||
bootstrap_ = std::make_unique<util::Bootstrap>();
|
bootstrap_ = std::make_unique<util::Bootstrap>();
|
||||||
std::string dataDir = util::Platform::getDragonXDataDir();
|
std::string dataDir = util::Platform::getDragonXDataDir();
|
||||||
bootstrap_->start(dataDir);
|
bootstrap_->start(dataDir);
|
||||||
@@ -918,7 +996,28 @@ void App::renderFirstRunWizard() {
|
|||||||
ImGui::PopStyleVar();
|
ImGui::PopStyleVar();
|
||||||
ImGui::PopStyleColor(3);
|
ImGui::PopStyleColor(3);
|
||||||
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(bx + dlBtnW + 12.0f * dp, cy));
|
// --- Mirror Download button ---
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(bx + dlBtnW + 8.0f * dp, cy));
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(ui::material::Surface()));
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnSurface()));
|
||||||
|
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||||
|
if (ImGui::Button("Mirror##bs_mirror", ImVec2(mirrorW, btnH2))) {
|
||||||
|
stopDaemonForBootstrap();
|
||||||
|
bootstrap_ = std::make_unique<util::Bootstrap>();
|
||||||
|
std::string dataDir = util::Platform::getDragonXDataDir();
|
||||||
|
std::string mirrorUrl = std::string(util::Bootstrap::kMirrorUrl) + "/" + util::Bootstrap::kZipName;
|
||||||
|
bootstrap_->start(dataDir, mirrorUrl);
|
||||||
|
wizard_phase_ = WizardPhase::BootstrapInProgress;
|
||||||
|
}
|
||||||
|
if (ImGui::IsItemHovered()) {
|
||||||
|
ImGui::SetTooltip("Download from mirror (bootstrap2.dragonx.is).\nUse this if the main download is slow or failing.");
|
||||||
|
}
|
||||||
|
ImGui::PopStyleVar();
|
||||||
|
ImGui::PopStyleColor(3);
|
||||||
|
|
||||||
|
// --- Skip button ---
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(bx + dlBtnW + 8.0f * dp + mirrorW + 8.0f * dp, cy));
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||||
if (ImGui::Button("Skip##bs", ImVec2(skipW2, btnH2))) {
|
if (ImGui::Button("Skip##bs", ImVec2(skipW2, btnH2))) {
|
||||||
wizard_phase_ = WizardPhase::EncryptOffer;
|
wizard_phase_ = WizardPhase::EncryptOffer;
|
||||||
@@ -931,7 +1030,7 @@ void App::renderFirstRunWizard() {
|
|||||||
|
|
||||||
cy += cardPad;
|
cy += cardPad;
|
||||||
// Lock card height to the tallest content ever seen (but not when collapsed)
|
// Lock card height to the tallest content ever seen (but not when collapsed)
|
||||||
static float card1MaxH = 0.0f;
|
float& card1MaxH = wizardUi.card1_max_h;
|
||||||
if (isCollapsed) {
|
if (isCollapsed) {
|
||||||
card1Bot = card1Top + (cy - card1Top);
|
card1Bot = card1Top + (cy - card1Top);
|
||||||
} else {
|
} else {
|
||||||
@@ -956,7 +1055,7 @@ void App::renderFirstRunWizard() {
|
|||||||
// Pre-start daemon when encrypt card becomes focused so it's ready
|
// Pre-start daemon when encrypt card becomes focused so it's ready
|
||||||
// by the time the user finishes typing their passphrase
|
// by the time the user finishes typing their passphrase
|
||||||
if (isFocused) {
|
if (isFocused) {
|
||||||
static bool wiz_daemon_prestarted = false;
|
bool& wiz_daemon_prestarted = wizardUi.daemon_prestarted;
|
||||||
if (!wiz_daemon_prestarted) {
|
if (!wiz_daemon_prestarted) {
|
||||||
wiz_daemon_prestarted = true;
|
wiz_daemon_prestarted = true;
|
||||||
if (!state_.connected && isUsingEmbeddedDaemon() && !isEmbeddedDaemonRunning()) {
|
if (!state_.connected && isUsingEmbeddedDaemon() && !isEmbeddedDaemonRunning()) {
|
||||||
@@ -1200,10 +1299,9 @@ void App::renderFirstRunWizard() {
|
|||||||
ImGui::BeginDisabled(!canEncrypt);
|
ImGui::BeginDisabled(!canEncrypt);
|
||||||
if (ImGui::Button("Encrypt & Continue##wiz", ImVec2(encBtnW, btnH2))) {
|
if (ImGui::Button("Encrypt & Continue##wiz", ImVec2(encBtnW, btnH2))) {
|
||||||
// Save passphrase + optional PIN for background processing
|
// Save passphrase + optional PIN for background processing
|
||||||
deferred_encrypt_passphrase_ = std::string(encrypt_pass_buf_);
|
wallet_security_.beginDeferredEncryption(
|
||||||
if (pinEntered && pinOk)
|
std::string(encrypt_pass_buf_),
|
||||||
deferred_encrypt_pin_ = pinStr;
|
(pinEntered && pinOk) ? pinStr : std::string());
|
||||||
deferred_encrypt_pending_ = 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_));
|
||||||
@@ -1213,7 +1311,10 @@ void App::renderFirstRunWizard() {
|
|||||||
|
|
||||||
// Start daemon + finish wizard immediately
|
// Start daemon + finish wizard immediately
|
||||||
if (!isEmbeddedDaemonRunning() && isUsingEmbeddedDaemon()) {
|
if (!isEmbeddedDaemonRunning() && isUsingEmbeddedDaemon()) {
|
||||||
startEmbeddedDaemon();
|
if (!startEmbeddedDaemon()) {
|
||||||
|
ui::Notifications::instance().warning(
|
||||||
|
TR("wizard_daemon_start_failed"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
tryConnect();
|
tryConnect();
|
||||||
wizard_phase_ = WizardPhase::Done;
|
wizard_phase_ = WizardPhase::Done;
|
||||||
@@ -1232,7 +1333,10 @@ void App::renderFirstRunWizard() {
|
|||||||
settings_->setWizardCompleted(true);
|
settings_->setWizardCompleted(true);
|
||||||
settings_->save();
|
settings_->save();
|
||||||
if (!isEmbeddedDaemonRunning() && isUsingEmbeddedDaemon()) {
|
if (!isEmbeddedDaemonRunning() && isUsingEmbeddedDaemon()) {
|
||||||
startEmbeddedDaemon();
|
if (!startEmbeddedDaemon()) {
|
||||||
|
ui::Notifications::instance().warning(
|
||||||
|
TR("wizard_daemon_start_failed"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
tryConnect();
|
tryConnect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
// DragonX Wallet - ImGui Edition
|
// DragonX Wallet - ImGui Edition
|
||||||
// Copyright 2024-2026 The Hush Developers
|
// Copyright 2024-2026 The Hush Developers
|
||||||
// Released under the GPLv3
|
// Released under the GPLv3
|
||||||
|
//
|
||||||
|
// settings.cpp — JSON settings persistence. Loads/saves user preferences
|
||||||
|
// to ~/.config/ObsidianDragon/settings.json (Linux/macOS) or %APPDATA% (Windows).
|
||||||
|
|
||||||
#include "settings.h"
|
#include "settings.h"
|
||||||
#include "version.h"
|
#include "version.h"
|
||||||
@@ -123,12 +126,29 @@ bool Settings::load(const std::string& path)
|
|||||||
for (const auto& a : j["favorite_addresses"])
|
for (const auto& a : j["favorite_addresses"])
|
||||||
if (a.is_string()) favorite_addresses_.insert(a.get<std::string>());
|
if (a.is_string()) favorite_addresses_.insert(a.get<std::string>());
|
||||||
}
|
}
|
||||||
|
if (j.contains("address_meta") && j["address_meta"].is_object()) {
|
||||||
|
address_meta_.clear();
|
||||||
|
for (auto& [addr, meta] : j["address_meta"].items()) {
|
||||||
|
AddressMeta m;
|
||||||
|
if (meta.contains("label") && meta["label"].is_string())
|
||||||
|
m.label = meta["label"].get<std::string>();
|
||||||
|
if (meta.contains("icon") && meta["icon"].is_string())
|
||||||
|
m.icon = meta["icon"].get<std::string>();
|
||||||
|
if (meta.contains("order") && meta["order"].is_number_integer())
|
||||||
|
m.sortOrder = meta["order"].get<int>();
|
||||||
|
if (meta.contains("mining") && meta["mining"].is_boolean())
|
||||||
|
m.mining = meta["mining"].get<bool>();
|
||||||
|
address_meta_[addr] = m;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (j.contains("wizard_completed")) wizard_completed_ = j["wizard_completed"].get<bool>();
|
if (j.contains("wizard_completed")) wizard_completed_ = j["wizard_completed"].get<bool>();
|
||||||
if (j.contains("auto_lock_timeout")) auto_lock_timeout_ = j["auto_lock_timeout"].get<int>();
|
if (j.contains("auto_lock_timeout")) auto_lock_timeout_ = j["auto_lock_timeout"].get<int>();
|
||||||
if (j.contains("unlock_duration")) unlock_duration_ = j["unlock_duration"].get<int>();
|
if (j.contains("unlock_duration")) unlock_duration_ = j["unlock_duration"].get<int>();
|
||||||
if (j.contains("pin_enabled")) pin_enabled_ = j["pin_enabled"].get<bool>();
|
if (j.contains("pin_enabled")) pin_enabled_ = j["pin_enabled"].get<bool>();
|
||||||
if (j.contains("keep_daemon_running")) keep_daemon_running_ = j["keep_daemon_running"].get<bool>();
|
if (j.contains("keep_daemon_running")) keep_daemon_running_ = j["keep_daemon_running"].get<bool>();
|
||||||
if (j.contains("stop_external_daemon")) stop_external_daemon_ = j["stop_external_daemon"].get<bool>();
|
if (j.contains("stop_external_daemon")) stop_external_daemon_ = j["stop_external_daemon"].get<bool>();
|
||||||
|
if (j.contains("max_connections")) max_connections_ = j["max_connections"].get<int>();
|
||||||
|
if (j.contains("verbose_logging")) verbose_logging_ = j["verbose_logging"].get<bool>();
|
||||||
if (j.contains("debug_categories") && j["debug_categories"].is_array()) {
|
if (j.contains("debug_categories") && j["debug_categories"].is_array()) {
|
||||||
debug_categories_.clear();
|
debug_categories_.clear();
|
||||||
for (const auto& c : j["debug_categories"])
|
for (const auto& c : j["debug_categories"])
|
||||||
@@ -136,22 +156,50 @@ bool Settings::load(const std::string& path)
|
|||||||
}
|
}
|
||||||
if (j.contains("theme_effects_enabled")) theme_effects_enabled_ = j["theme_effects_enabled"].get<bool>();
|
if (j.contains("theme_effects_enabled")) theme_effects_enabled_ = j["theme_effects_enabled"].get<bool>();
|
||||||
if (j.contains("low_spec_mode")) low_spec_mode_ = j["low_spec_mode"].get<bool>();
|
if (j.contains("low_spec_mode")) low_spec_mode_ = j["low_spec_mode"].get<bool>();
|
||||||
|
if (j.contains("reduce_motion")) reduce_motion_ = j["reduce_motion"].get<bool>();
|
||||||
if (j.contains("selected_exchange")) selected_exchange_ = j["selected_exchange"].get<std::string>();
|
if (j.contains("selected_exchange")) selected_exchange_ = j["selected_exchange"].get<std::string>();
|
||||||
if (j.contains("selected_pair")) selected_pair_ = j["selected_pair"].get<std::string>();
|
if (j.contains("selected_pair")) selected_pair_ = j["selected_pair"].get<std::string>();
|
||||||
if (j.contains("pool_url")) pool_url_ = j["pool_url"].get<std::string>();
|
if (j.contains("pool_url")) pool_url_ = j["pool_url"].get<std::string>();
|
||||||
|
// Migrate old default pool URL that was missing the stratum port
|
||||||
|
if (pool_url_ == "pool.dragonx.is") pool_url_ = "pool.dragonx.is:3433";
|
||||||
if (j.contains("pool_algo")) pool_algo_ = j["pool_algo"].get<std::string>();
|
if (j.contains("pool_algo")) pool_algo_ = j["pool_algo"].get<std::string>();
|
||||||
if (j.contains("pool_worker")) pool_worker_ = j["pool_worker"].get<std::string>();
|
if (j.contains("pool_worker")) pool_worker_ = j["pool_worker"].get<std::string>();
|
||||||
if (j.contains("pool_threads")) pool_threads_ = j["pool_threads"].get<int>();
|
if (j.contains("pool_threads")) pool_threads_ = j["pool_threads"].get<int>();
|
||||||
if (j.contains("pool_tls")) pool_tls_ = j["pool_tls"].get<bool>();
|
if (j.contains("pool_tls")) pool_tls_ = j["pool_tls"].get<bool>();
|
||||||
if (j.contains("pool_hugepages")) pool_hugepages_ = j["pool_hugepages"].get<bool>();
|
if (j.contains("pool_hugepages")) pool_hugepages_ = j["pool_hugepages"].get<bool>();
|
||||||
if (j.contains("pool_mode")) pool_mode_ = j["pool_mode"].get<bool>();
|
if (j.contains("pool_mode")) pool_mode_ = j["pool_mode"].get<bool>();
|
||||||
|
if (j.contains("mine_when_idle")) mine_when_idle_ = j["mine_when_idle"].get<bool>();
|
||||||
|
if (j.contains("mine_idle_delay")) mine_idle_delay_= std::max(30, j["mine_idle_delay"].get<int>());
|
||||||
|
if (j.contains("idle_thread_scaling")) idle_thread_scaling_ = j["idle_thread_scaling"].get<bool>();
|
||||||
|
if (j.contains("idle_threads_active")) idle_threads_active_ = j["idle_threads_active"].get<int>();
|
||||||
|
if (j.contains("idle_threads_idle")) idle_threads_idle_ = j["idle_threads_idle"].get<int>();
|
||||||
|
if (j.contains("idle_gpu_aware")) idle_gpu_aware_ = j["idle_gpu_aware"].get<bool>();
|
||||||
|
if (j.contains("saved_pool_urls") && j["saved_pool_urls"].is_array()) {
|
||||||
|
saved_pool_urls_.clear();
|
||||||
|
for (const auto& u : j["saved_pool_urls"])
|
||||||
|
if (u.is_string()) saved_pool_urls_.push_back(u.get<std::string>());
|
||||||
|
}
|
||||||
|
if (j.contains("saved_pool_workers") && j["saved_pool_workers"].is_array()) {
|
||||||
|
saved_pool_workers_.clear();
|
||||||
|
for (const auto& w : j["saved_pool_workers"])
|
||||||
|
if (w.is_string()) saved_pool_workers_.push_back(w.get<std::string>());
|
||||||
|
}
|
||||||
if (j.contains("font_scale") && j["font_scale"].is_number())
|
if (j.contains("font_scale") && j["font_scale"].is_number())
|
||||||
font_scale_ = std::max(1.0f, std::min(1.5f, j["font_scale"].get<float>()));
|
font_scale_ = std::max(1.0f, std::min(1.5f, j["font_scale"].get<float>()));
|
||||||
if (j.contains("window_width") && j["window_width"].is_number_integer())
|
if (j.contains("window_width") && j["window_width"].is_number_integer())
|
||||||
window_width_ = j["window_width"].get<int>();
|
window_width_ = j["window_width"].get<int>();
|
||||||
if (j.contains("window_height") && j["window_height"].is_number_integer())
|
if (j.contains("window_height") && j["window_height"].is_number_integer())
|
||||||
window_height_ = j["window_height"].get<int>();
|
window_height_ = j["window_height"].get<int>();
|
||||||
|
|
||||||
|
// Version tracking — detect upgrades so we can re-save with new defaults
|
||||||
|
if (j.contains("settings_version")) settings_version_ = j["settings_version"].get<std::string>();
|
||||||
|
if (settings_version_ != DRAGONX_VERSION) {
|
||||||
|
DEBUG_LOGF("Settings version %s differs from wallet %s — will re-save\n",
|
||||||
|
settings_version_.empty() ? "(none)" : settings_version_.c_str(),
|
||||||
|
DRAGONX_VERSION);
|
||||||
|
needs_upgrade_save_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
DEBUG_LOGF("Failed to parse settings: %s\n", e.what());
|
DEBUG_LOGF("Failed to parse settings: %s\n", e.what());
|
||||||
@@ -197,17 +245,33 @@ bool Settings::save(const std::string& path)
|
|||||||
j["favorite_addresses"] = json::array();
|
j["favorite_addresses"] = json::array();
|
||||||
for (const auto& addr : favorite_addresses_)
|
for (const auto& addr : favorite_addresses_)
|
||||||
j["favorite_addresses"].push_back(addr);
|
j["favorite_addresses"].push_back(addr);
|
||||||
|
{
|
||||||
|
json meta_obj = json::object();
|
||||||
|
for (const auto& [addr, m] : address_meta_) {
|
||||||
|
if (m.label.empty() && m.icon.empty() && m.sortOrder < 0 && !m.mining) continue;
|
||||||
|
json entry = json::object();
|
||||||
|
if (!m.label.empty()) entry["label"] = m.label;
|
||||||
|
if (!m.icon.empty()) entry["icon"] = m.icon;
|
||||||
|
if (m.sortOrder >= 0) entry["order"] = m.sortOrder;
|
||||||
|
if (m.mining) entry["mining"] = true;
|
||||||
|
meta_obj[addr] = entry;
|
||||||
|
}
|
||||||
|
j["address_meta"] = meta_obj;
|
||||||
|
}
|
||||||
j["wizard_completed"] = wizard_completed_;
|
j["wizard_completed"] = wizard_completed_;
|
||||||
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_;
|
||||||
j["keep_daemon_running"] = keep_daemon_running_;
|
j["keep_daemon_running"] = keep_daemon_running_;
|
||||||
j["stop_external_daemon"] = stop_external_daemon_;
|
j["stop_external_daemon"] = stop_external_daemon_;
|
||||||
|
j["max_connections"] = max_connections_;
|
||||||
|
j["verbose_logging"] = verbose_logging_;
|
||||||
j["debug_categories"] = json::array();
|
j["debug_categories"] = json::array();
|
||||||
for (const auto& cat : debug_categories_)
|
for (const auto& cat : debug_categories_)
|
||||||
j["debug_categories"].push_back(cat);
|
j["debug_categories"].push_back(cat);
|
||||||
j["theme_effects_enabled"] = theme_effects_enabled_;
|
j["theme_effects_enabled"] = theme_effects_enabled_;
|
||||||
j["low_spec_mode"] = low_spec_mode_;
|
j["low_spec_mode"] = low_spec_mode_;
|
||||||
|
j["reduce_motion"] = reduce_motion_;
|
||||||
j["selected_exchange"] = selected_exchange_;
|
j["selected_exchange"] = selected_exchange_;
|
||||||
j["selected_pair"] = selected_pair_;
|
j["selected_pair"] = selected_pair_;
|
||||||
j["pool_url"] = pool_url_;
|
j["pool_url"] = pool_url_;
|
||||||
@@ -217,7 +281,20 @@ bool Settings::save(const std::string& path)
|
|||||||
j["pool_tls"] = pool_tls_;
|
j["pool_tls"] = pool_tls_;
|
||||||
j["pool_hugepages"] = pool_hugepages_;
|
j["pool_hugepages"] = pool_hugepages_;
|
||||||
j["pool_mode"] = pool_mode_;
|
j["pool_mode"] = pool_mode_;
|
||||||
|
j["mine_when_idle"] = mine_when_idle_;
|
||||||
|
j["mine_idle_delay"]= mine_idle_delay_;
|
||||||
|
j["idle_thread_scaling"] = idle_thread_scaling_;
|
||||||
|
j["idle_threads_active"] = idle_threads_active_;
|
||||||
|
j["idle_threads_idle"] = idle_threads_idle_;
|
||||||
|
j["idle_gpu_aware"] = idle_gpu_aware_;
|
||||||
|
j["saved_pool_urls"] = json::array();
|
||||||
|
for (const auto& u : saved_pool_urls_)
|
||||||
|
j["saved_pool_urls"].push_back(u);
|
||||||
|
j["saved_pool_workers"] = json::array();
|
||||||
|
for (const auto& w : saved_pool_workers_)
|
||||||
|
j["saved_pool_workers"].push_back(w);
|
||||||
j["font_scale"] = font_scale_;
|
j["font_scale"] = font_scale_;
|
||||||
|
j["settings_version"] = std::string(DRAGONX_VERSION);
|
||||||
if (window_width_ > 0 && window_height_ > 0) {
|
if (window_width_ > 0 && window_height_ > 0) {
|
||||||
j["window_width"] = window_width_;
|
j["window_width"] = window_width_;
|
||||||
j["window_height"] = window_height_;
|
j["window_height"] = window_height_;
|
||||||
|
|||||||
@@ -4,8 +4,11 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <map>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <set>
|
#include <set>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace dragonx {
|
namespace dragonx {
|
||||||
namespace config {
|
namespace config {
|
||||||
@@ -139,6 +142,54 @@ public:
|
|||||||
void unfavoriteAddress(const std::string& addr) { favorite_addresses_.erase(addr); }
|
void unfavoriteAddress(const std::string& addr) { favorite_addresses_.erase(addr); }
|
||||||
int getFavoriteAddressCount() const { return (int)favorite_addresses_.size(); }
|
int getFavoriteAddressCount() const { return (int)favorite_addresses_.size(); }
|
||||||
|
|
||||||
|
// Address metadata (labels, icons, custom ordering)
|
||||||
|
struct AddressMeta {
|
||||||
|
std::string label;
|
||||||
|
std::string icon; // material icon name, e.g. "savings"
|
||||||
|
int sortOrder = -1; // -1 = auto (use default sort)
|
||||||
|
bool mining = false;
|
||||||
|
};
|
||||||
|
const AddressMeta& getAddressMeta(const std::string& addr) const {
|
||||||
|
static const AddressMeta empty{};
|
||||||
|
auto it = address_meta_.find(addr);
|
||||||
|
return it != address_meta_.end() ? it->second : empty;
|
||||||
|
}
|
||||||
|
void setAddressLabel(const std::string& addr, const std::string& label) {
|
||||||
|
address_meta_[addr].label = label;
|
||||||
|
}
|
||||||
|
void setAddressIcon(const std::string& addr, const std::string& icon) {
|
||||||
|
address_meta_[addr].icon = icon;
|
||||||
|
}
|
||||||
|
void setAddressSortOrder(const std::string& addr, int order) {
|
||||||
|
address_meta_[addr].sortOrder = order;
|
||||||
|
}
|
||||||
|
bool isMiningAddress(const std::string& addr) const {
|
||||||
|
auto it = address_meta_.find(addr);
|
||||||
|
return it != address_meta_.end() && it->second.mining;
|
||||||
|
}
|
||||||
|
void setMiningAddress(const std::string& addr, bool mining) {
|
||||||
|
address_meta_[addr].mining = mining;
|
||||||
|
}
|
||||||
|
std::set<std::string> getMiningAddresses() const {
|
||||||
|
std::set<std::string> addresses;
|
||||||
|
for (const auto& [addr, meta] : address_meta_) {
|
||||||
|
if (meta.mining) addresses.insert(addr);
|
||||||
|
}
|
||||||
|
return addresses;
|
||||||
|
}
|
||||||
|
int getNextSortOrder() const {
|
||||||
|
int mx = -1;
|
||||||
|
for (const auto& [k, v] : address_meta_)
|
||||||
|
if (v.sortOrder > mx) mx = v.sortOrder;
|
||||||
|
return mx + 1;
|
||||||
|
}
|
||||||
|
void swapAddressOrder(const std::string& a, const std::string& b) {
|
||||||
|
int oa = address_meta_[a].sortOrder;
|
||||||
|
int ob = address_meta_[b].sortOrder;
|
||||||
|
address_meta_[a].sortOrder = ob;
|
||||||
|
address_meta_[b].sortOrder = oa;
|
||||||
|
}
|
||||||
|
|
||||||
// First-run wizard
|
// First-run wizard
|
||||||
bool getWizardCompleted() const { return wizard_completed_; }
|
bool getWizardCompleted() const { return wizard_completed_; }
|
||||||
void setWizardCompleted(bool v) { wizard_completed_ = v; }
|
void setWizardCompleted(bool v) { wizard_completed_ = v; }
|
||||||
@@ -163,6 +214,14 @@ public:
|
|||||||
bool getStopExternalDaemon() const { return stop_external_daemon_; }
|
bool getStopExternalDaemon() const { return stop_external_daemon_; }
|
||||||
void setStopExternalDaemon(bool v) { stop_external_daemon_ = v; }
|
void setStopExternalDaemon(bool v) { stop_external_daemon_ = v; }
|
||||||
|
|
||||||
|
// Daemon — maximum peer connections (0 = daemon default)
|
||||||
|
int getMaxConnections() const { return max_connections_; }
|
||||||
|
void setMaxConnections(int v) { max_connections_ = std::max(0, v); }
|
||||||
|
|
||||||
|
// Verbose diagnostic logging (connection attempts, daemon state, port owner, etc.)
|
||||||
|
bool getVerboseLogging() const { return verbose_logging_; }
|
||||||
|
void setVerboseLogging(bool v) { verbose_logging_ = v; }
|
||||||
|
|
||||||
// Daemon — debug logging categories
|
// Daemon — debug logging categories
|
||||||
const std::set<std::string>& getDebugCategories() const { return debug_categories_; }
|
const std::set<std::string>& getDebugCategories() const { return debug_categories_; }
|
||||||
void setDebugCategories(const std::set<std::string>& cats) { debug_categories_ = cats; }
|
void setDebugCategories(const std::set<std::string>& cats) { debug_categories_ = cats; }
|
||||||
@@ -180,6 +239,10 @@ public:
|
|||||||
bool getLowSpecMode() const { return low_spec_mode_; }
|
bool getLowSpecMode() const { return low_spec_mode_; }
|
||||||
void setLowSpecMode(bool v) { low_spec_mode_ = v; }
|
void setLowSpecMode(bool v) { low_spec_mode_ = v; }
|
||||||
|
|
||||||
|
// Reduce motion — disables animated transitions for accessibility
|
||||||
|
bool getReduceMotion() const { return reduce_motion_; }
|
||||||
|
void setReduceMotion(bool v) { reduce_motion_ = v; }
|
||||||
|
|
||||||
// Market — last selected exchange + pair
|
// Market — last selected exchange + pair
|
||||||
std::string getSelectedExchange() const { return selected_exchange_; }
|
std::string getSelectedExchange() const { return selected_exchange_; }
|
||||||
void setSelectedExchange(const std::string& v) { selected_exchange_ = v; }
|
void setSelectedExchange(const std::string& v) { selected_exchange_ = v; }
|
||||||
@@ -202,6 +265,47 @@ public:
|
|||||||
bool getPoolMode() const { return pool_mode_; }
|
bool getPoolMode() const { return pool_mode_; }
|
||||||
void setPoolMode(bool v) { pool_mode_ = v; }
|
void setPoolMode(bool v) { pool_mode_ = v; }
|
||||||
|
|
||||||
|
// Mine when idle (auto-start mining when system is idle)
|
||||||
|
bool getMineWhenIdle() const { return mine_when_idle_; }
|
||||||
|
void setMineWhenIdle(bool v) { mine_when_idle_ = v; }
|
||||||
|
int getMineIdleDelay() const { return mine_idle_delay_; }
|
||||||
|
void setMineIdleDelay(int seconds) { mine_idle_delay_ = std::max(30, seconds); }
|
||||||
|
|
||||||
|
// Idle thread scaling — scale thread count instead of start/stop
|
||||||
|
bool getIdleThreadScaling() const { return idle_thread_scaling_; }
|
||||||
|
void setIdleThreadScaling(bool v) { idle_thread_scaling_ = v; }
|
||||||
|
int getIdleThreadsActive() const { return idle_threads_active_; }
|
||||||
|
void setIdleThreadsActive(int v) { idle_threads_active_ = std::max(0, v); }
|
||||||
|
int getIdleThreadsIdle() const { return idle_threads_idle_; }
|
||||||
|
void setIdleThreadsIdle(int v) { idle_threads_idle_ = std::max(0, v); }
|
||||||
|
bool getIdleGpuAware() const { return idle_gpu_aware_; }
|
||||||
|
void setIdleGpuAware(bool v) { idle_gpu_aware_ = v; }
|
||||||
|
|
||||||
|
// Saved pool URLs (user-managed favorites dropdown)
|
||||||
|
const std::vector<std::string>& getSavedPoolUrls() const { return saved_pool_urls_; }
|
||||||
|
void addSavedPoolUrl(const std::string& url) {
|
||||||
|
// Don't add duplicates
|
||||||
|
for (const auto& u : saved_pool_urls_) if (u == url) return;
|
||||||
|
saved_pool_urls_.push_back(url);
|
||||||
|
}
|
||||||
|
void removeSavedPoolUrl(const std::string& url) {
|
||||||
|
saved_pool_urls_.erase(
|
||||||
|
std::remove(saved_pool_urls_.begin(), saved_pool_urls_.end(), url),
|
||||||
|
saved_pool_urls_.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Saved pool worker addresses (user-managed favorites dropdown)
|
||||||
|
const std::vector<std::string>& getSavedPoolWorkers() const { return saved_pool_workers_; }
|
||||||
|
void addSavedPoolWorker(const std::string& addr) {
|
||||||
|
for (const auto& a : saved_pool_workers_) if (a == addr) return;
|
||||||
|
saved_pool_workers_.push_back(addr);
|
||||||
|
}
|
||||||
|
void removeSavedPoolWorker(const std::string& addr) {
|
||||||
|
saved_pool_workers_.erase(
|
||||||
|
std::remove(saved_pool_workers_.begin(), saved_pool_workers_.end(), addr),
|
||||||
|
saved_pool_workers_.end());
|
||||||
|
}
|
||||||
|
|
||||||
// Font scale (user accessibility setting, 1.0–1.5)
|
// Font scale (user accessibility setting, 1.0–1.5)
|
||||||
float getFontScale() const { return font_scale_; }
|
float getFontScale() const { return font_scale_; }
|
||||||
void setFontScale(float v) { font_scale_ = std::max(1.0f, std::min(1.5f, v)); }
|
void setFontScale(float v) { font_scale_ = std::max(1.0f, std::min(1.5f, v)); }
|
||||||
@@ -211,6 +315,10 @@ public:
|
|||||||
int getWindowHeight() const { return window_height_; }
|
int getWindowHeight() const { return window_height_; }
|
||||||
void setWindowSize(int w, int h) { window_width_ = w; window_height_ = h; }
|
void setWindowSize(int w, int h) { window_width_ = w; window_height_ = h; }
|
||||||
|
|
||||||
|
// Returns true once after an upgrade (version mismatch detected on load)
|
||||||
|
bool needsUpgradeSave() const { return needs_upgrade_save_; }
|
||||||
|
void clearUpgradeSave() { needs_upgrade_save_ = false; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::string settings_path_;
|
std::string settings_path_;
|
||||||
|
|
||||||
@@ -231,32 +339,49 @@ private:
|
|||||||
float blur_multiplier_ = 0.10f;
|
float blur_multiplier_ = 0.10f;
|
||||||
float noise_opacity_ = 0.5f;
|
float noise_opacity_ = 0.5f;
|
||||||
bool gradient_background_ = false;
|
bool gradient_background_ = false;
|
||||||
|
#ifdef _WIN32
|
||||||
float ui_opacity_ = 0.50f; // Card/sidebar opacity (0.3–1.0, 1.0 = opaque)
|
float ui_opacity_ = 0.50f; // Card/sidebar opacity (0.3–1.0, 1.0 = opaque)
|
||||||
float window_opacity_ = 0.75f; // Background alpha (0.3–1.0, <1 = desktop visible)
|
float window_opacity_ = 0.75f; // Background alpha (0.3–1.0, <1 = desktop visible)
|
||||||
|
#else
|
||||||
|
float ui_opacity_ = 1.0f; // Mac/Linux: default fully opaque
|
||||||
|
float window_opacity_ = 1.0f; // Mac/Linux: default fully opaque
|
||||||
|
#endif
|
||||||
std::string balance_layout_ = "classic";
|
std::string balance_layout_ = "classic";
|
||||||
bool scanline_enabled_ = true;
|
bool scanline_enabled_ = true;
|
||||||
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_;
|
||||||
bool wizard_completed_ = false;
|
bool wizard_completed_ = false;
|
||||||
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;
|
||||||
bool keep_daemon_running_ = false;
|
bool keep_daemon_running_ = false;
|
||||||
bool stop_external_daemon_ = false;
|
bool stop_external_daemon_ = false;
|
||||||
|
int max_connections_ = 0; // 0 = daemon default
|
||||||
|
bool verbose_logging_ = false;
|
||||||
std::set<std::string> debug_categories_;
|
std::set<std::string> debug_categories_;
|
||||||
bool theme_effects_enabled_ = true;
|
bool theme_effects_enabled_ = true;
|
||||||
bool low_spec_mode_ = false;
|
bool low_spec_mode_ = false;
|
||||||
|
bool reduce_motion_ = false;
|
||||||
std::string selected_exchange_ = "TradeOgre";
|
std::string selected_exchange_ = "TradeOgre";
|
||||||
std::string selected_pair_ = "DRGX/BTC";
|
std::string selected_pair_ = "DRGX/BTC";
|
||||||
|
|
||||||
// Pool mining
|
// Pool mining
|
||||||
std::string pool_url_ = "pool.dragonx.is";
|
std::string pool_url_ = "pool.dragonx.is:3433";
|
||||||
std::string pool_algo_ = "rx/hush";
|
std::string pool_algo_ = "rx/hush";
|
||||||
std::string pool_worker_ = "x";
|
std::string pool_worker_ = "";
|
||||||
int pool_threads_ = 0;
|
int pool_threads_ = 0;
|
||||||
bool pool_tls_ = false;
|
bool pool_tls_ = false;
|
||||||
bool pool_hugepages_ = true;
|
bool pool_hugepages_ = true;
|
||||||
bool pool_mode_ = false; // false=solo, true=pool
|
bool pool_mode_ = false; // false=solo, true=pool
|
||||||
|
bool mine_when_idle_ = false; // auto-start mining when system idle
|
||||||
|
int mine_idle_delay_= 120; // seconds of idle before mining starts
|
||||||
|
bool idle_thread_scaling_ = false; // scale threads instead of start/stop
|
||||||
|
int idle_threads_active_ = 0; // threads when user active (0 = auto)
|
||||||
|
int idle_threads_idle_ = 0; // threads when idle (0 = auto = all)
|
||||||
|
bool idle_gpu_aware_ = true; // treat GPU activity as non-idle
|
||||||
|
std::vector<std::string> saved_pool_urls_; // user-saved pool URL favorites
|
||||||
|
std::vector<std::string> saved_pool_workers_; // user-saved worker address favorites
|
||||||
|
|
||||||
// Font scale (user accessibility, 1.0–3.0; 1.0 = default)
|
// Font scale (user accessibility, 1.0–3.0; 1.0 = default)
|
||||||
float font_scale_ = 1.0f;
|
float font_scale_ = 1.0f;
|
||||||
@@ -264,6 +389,10 @@ private:
|
|||||||
// Window size (logical pixels at 1x scale; 0 = use default 1200×775)
|
// Window size (logical pixels at 1x scale; 0 = use default 1200×775)
|
||||||
int window_width_ = 0;
|
int window_width_ = 0;
|
||||||
int window_height_ = 0;
|
int window_height_ = 0;
|
||||||
|
|
||||||
|
// Wallet version that last wrote this settings file (for upgrade detection)
|
||||||
|
std::string settings_version_;
|
||||||
|
bool needs_upgrade_save_ = false; // true when version changed
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace config
|
} // namespace config
|
||||||
|
|||||||
@@ -4,9 +4,12 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#define DRAGONX_VERSION "1.0.0"
|
// !! DO NOT EDIT version.h — it is generated from version.h.in by CMake.
|
||||||
|
// !! Change the version in CMakeLists.txt: project(... VERSION x.y.z ...)
|
||||||
|
|
||||||
|
#define DRAGONX_VERSION "1.2.0-rc1"
|
||||||
#define DRAGONX_VERSION_MAJOR 1
|
#define DRAGONX_VERSION_MAJOR 1
|
||||||
#define DRAGONX_VERSION_MINOR 0
|
#define DRAGONX_VERSION_MINOR 2
|
||||||
#define DRAGONX_VERSION_PATCH 0
|
#define DRAGONX_VERSION_PATCH 0
|
||||||
|
|
||||||
#define DRAGONX_APP_NAME "ObsidianDragon"
|
#define DRAGONX_APP_NAME "ObsidianDragon"
|
||||||
|
|||||||
31
src/config/version.h.in
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
// DragonX Wallet - ImGui Edition
|
||||||
|
// Copyright 2024-2026 The Hush Developers
|
||||||
|
// Released under the GPLv3
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
// !! DO NOT EDIT version.h — it is generated from version.h.in by CMake.
|
||||||
|
// !! Change the version in CMakeLists.txt: project(... VERSION x.y.z ...)
|
||||||
|
|
||||||
|
#define DRAGONX_VERSION "@PROJECT_VERSION@@DRAGONX_VERSION_SUFFIX@"
|
||||||
|
#define DRAGONX_VERSION_MAJOR @PROJECT_VERSION_MAJOR@
|
||||||
|
#define DRAGONX_VERSION_MINOR @PROJECT_VERSION_MINOR@
|
||||||
|
#define DRAGONX_VERSION_PATCH @PROJECT_VERSION_PATCH@
|
||||||
|
|
||||||
|
#define DRAGONX_APP_NAME "ObsidianDragon"
|
||||||
|
#define DRAGONX_ORG_NAME "Hush"
|
||||||
|
|
||||||
|
// Default RPC settings
|
||||||
|
#define DRAGONX_DEFAULT_RPC_HOST "127.0.0.1"
|
||||||
|
#define DRAGONX_DEFAULT_RPC_PORT "21769"
|
||||||
|
|
||||||
|
// Coin parameters
|
||||||
|
#define DRAGONX_TICKER "DRGX"
|
||||||
|
#define DRAGONX_COIN_NAME "DragonX"
|
||||||
|
#define DRAGONX_URI_SCHEME "drgx"
|
||||||
|
#define DRAGONX_ZATOSHI_PER_COIN 100000000
|
||||||
|
#define DRAGONX_DEFAULT_FEE 0.0001
|
||||||
|
|
||||||
|
// Config file names
|
||||||
|
#define DRAGONX_CONF_FILENAME "DRAGONX.conf"
|
||||||
|
#define DRAGONX_WALLET_FILENAME "wallet.dat"
|
||||||
117
src/daemon/daemon_controller.cpp
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
#include "daemon_controller.h"
|
||||||
|
#include "../config/settings.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace dragonx {
|
||||||
|
namespace daemon {
|
||||||
|
|
||||||
|
DaemonController::DaemonController()
|
||||||
|
: daemon_(std::make_unique<EmbeddedDaemon>())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
DaemonController::~DaemonController() = default;
|
||||||
|
|
||||||
|
void DaemonController::setStateCallback(StateCallback callback)
|
||||||
|
{
|
||||||
|
daemon_->setStateCallback(std::move(callback));
|
||||||
|
}
|
||||||
|
|
||||||
|
void DaemonController::syncSettings(const config::Settings* settings)
|
||||||
|
{
|
||||||
|
if (!settings) return;
|
||||||
|
daemon_->setDebugCategories(settings->getDebugCategories());
|
||||||
|
daemon_->setMaxConnections(settings->getMaxConnections());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DaemonController::start(const config::Settings* settings)
|
||||||
|
{
|
||||||
|
syncSettings(settings);
|
||||||
|
return daemon_->start();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DaemonController::stop(int waitMs)
|
||||||
|
{
|
||||||
|
daemon_->stop(waitMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DaemonController::isRunning() const
|
||||||
|
{
|
||||||
|
return daemon_->isRunning();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DaemonController::externalDaemonDetected() const
|
||||||
|
{
|
||||||
|
return daemon_->externalDaemonDetected();
|
||||||
|
}
|
||||||
|
|
||||||
|
DaemonController::State DaemonController::state() const
|
||||||
|
{
|
||||||
|
return daemon_->getState();
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string& DaemonController::lastError() const
|
||||||
|
{
|
||||||
|
return daemon_->getLastError();
|
||||||
|
}
|
||||||
|
|
||||||
|
int DaemonController::crashCount() const
|
||||||
|
{
|
||||||
|
return daemon_->getCrashCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
int DaemonController::lastBlockHeight() const
|
||||||
|
{
|
||||||
|
return daemon_ ? daemon_->getLastBlockHeight() : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
double DaemonController::memoryUsageMB() const
|
||||||
|
{
|
||||||
|
return daemon_ ? daemon_->getMemoryUsageMB() : 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> DaemonController::recentLines(std::size_t count) const
|
||||||
|
{
|
||||||
|
return daemon_ ? daemon_->getRecentLines(count) : std::vector<std::string>{};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string DaemonController::outputSince(std::size_t& offset) const
|
||||||
|
{
|
||||||
|
return daemon_ ? daemon_->getOutputSince(offset) : std::string{};
|
||||||
|
}
|
||||||
|
|
||||||
|
void DaemonController::resetCrashCount()
|
||||||
|
{
|
||||||
|
daemon_->resetCrashCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DaemonController::setRescanOnNextStart(bool enabled)
|
||||||
|
{
|
||||||
|
daemon_->setRescanOnNextStart(enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DaemonController::rescanOnNextStart() const
|
||||||
|
{
|
||||||
|
return daemon_->rescanOnNextStart();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DaemonController::prepareLifecycleOperation(const LifecycleDecision& decision,
|
||||||
|
const config::Settings* settings)
|
||||||
|
{
|
||||||
|
if (settings) syncSettings(settings);
|
||||||
|
if (decision.resetCrashCount) resetCrashCount();
|
||||||
|
if (decision.setRescanOnNextStart) setRescanOnNextStart(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
DaemonController::ShutdownDecision DaemonController::shutdownDecision(
|
||||||
|
bool keepDaemonRunning, bool stopExternalDaemon) const
|
||||||
|
{
|
||||||
|
return evaluateShutdownPolicy(static_cast<bool>(daemon_),
|
||||||
|
daemon_ && daemon_->externalDaemonDetected(),
|
||||||
|
keepDaemonRunning,
|
||||||
|
stopExternalDaemon);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace daemon
|
||||||
|
} // namespace dragonx
|
||||||
225
src/daemon/daemon_controller.h
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "embedded_daemon.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace dragonx {
|
||||||
|
namespace config { class Settings; }
|
||||||
|
namespace daemon {
|
||||||
|
|
||||||
|
class DaemonController {
|
||||||
|
public:
|
||||||
|
using State = EmbeddedDaemon::State;
|
||||||
|
using StateCallback = EmbeddedDaemon::StateCallback;
|
||||||
|
|
||||||
|
enum class ShutdownAction {
|
||||||
|
DisconnectOnly,
|
||||||
|
StopDaemon
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ShutdownDecision {
|
||||||
|
ShutdownAction action = ShutdownAction::DisconnectOnly;
|
||||||
|
const char* logReason = "no embedded daemon";
|
||||||
|
const char* status = "Disconnecting...";
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class LifecycleOperation {
|
||||||
|
ManualRestart,
|
||||||
|
Rescan,
|
||||||
|
DeleteBlockchainData,
|
||||||
|
BootstrapStop
|
||||||
|
};
|
||||||
|
|
||||||
|
struct LifecycleDecision {
|
||||||
|
LifecycleOperation operation = LifecycleOperation::ManualRestart;
|
||||||
|
bool allowed = false;
|
||||||
|
bool wasRunning = false;
|
||||||
|
const char* taskName = "";
|
||||||
|
const char* status = "";
|
||||||
|
const char* warning = "";
|
||||||
|
bool resetCrashCount = false;
|
||||||
|
bool setRescanOnNextStart = false;
|
||||||
|
bool disconnectRpc = false;
|
||||||
|
int restartDelayMs = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class LifecycleTaskContext {
|
||||||
|
public:
|
||||||
|
virtual ~LifecycleTaskContext() = default;
|
||||||
|
virtual bool cancelled() const = 0;
|
||||||
|
virtual bool shuttingDown() const = 0;
|
||||||
|
virtual void sleepForMs(int milliseconds) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class LifecycleRuntime {
|
||||||
|
public:
|
||||||
|
virtual ~LifecycleRuntime() = default;
|
||||||
|
virtual void stopDaemonWithPolicy() = 0;
|
||||||
|
virtual bool startDaemon() = 0;
|
||||||
|
virtual int deleteBlockchainData() = 0;
|
||||||
|
virtual void resetOutputOffset() = 0;
|
||||||
|
virtual void requestRpcStopAndDisconnect(const char* context, const char* reason) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct LifecycleExecutionResult {
|
||||||
|
bool completed = false;
|
||||||
|
bool cancelled = false;
|
||||||
|
bool stopped = false;
|
||||||
|
bool started = false;
|
||||||
|
int deletedItems = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
DaemonController();
|
||||||
|
~DaemonController();
|
||||||
|
|
||||||
|
DaemonController(const DaemonController&) = delete;
|
||||||
|
DaemonController& operator=(const DaemonController&) = delete;
|
||||||
|
|
||||||
|
EmbeddedDaemon* daemon() { return daemon_.get(); }
|
||||||
|
const EmbeddedDaemon* daemon() const { return daemon_.get(); }
|
||||||
|
|
||||||
|
void setStateCallback(StateCallback callback);
|
||||||
|
void syncSettings(const config::Settings* settings);
|
||||||
|
|
||||||
|
bool start(const config::Settings* settings);
|
||||||
|
void stop(int waitMs);
|
||||||
|
|
||||||
|
bool isRunning() const;
|
||||||
|
bool externalDaemonDetected() const;
|
||||||
|
State state() const;
|
||||||
|
const std::string& lastError() const;
|
||||||
|
int crashCount() const;
|
||||||
|
int lastBlockHeight() const;
|
||||||
|
double memoryUsageMB() const;
|
||||||
|
std::vector<std::string> recentLines(std::size_t count) const;
|
||||||
|
std::string outputSince(std::size_t& offset) const;
|
||||||
|
|
||||||
|
void resetCrashCount();
|
||||||
|
void setRescanOnNextStart(bool enabled);
|
||||||
|
bool rescanOnNextStart() const;
|
||||||
|
|
||||||
|
static ShutdownDecision evaluateShutdownPolicy(bool hasDaemon,
|
||||||
|
bool externalDaemonDetected,
|
||||||
|
bool keepDaemonRunning,
|
||||||
|
bool stopExternalDaemon) {
|
||||||
|
if (!hasDaemon) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (keepDaemonRunning) {
|
||||||
|
return {ShutdownAction::DisconnectOnly,
|
||||||
|
"keep_daemon_running enabled",
|
||||||
|
"Disconnecting (daemon stays running)..."};
|
||||||
|
}
|
||||||
|
if (externalDaemonDetected && !stopExternalDaemon) {
|
||||||
|
return {ShutdownAction::DisconnectOnly,
|
||||||
|
"external daemon (not ours to stop)",
|
||||||
|
"Disconnecting (daemon stays running)..."};
|
||||||
|
}
|
||||||
|
return {ShutdownAction::StopDaemon,
|
||||||
|
"stopping managed daemon",
|
||||||
|
"Sending stop command to daemon..."};
|
||||||
|
}
|
||||||
|
static LifecycleDecision evaluateLifecycleOperation(LifecycleOperation operation,
|
||||||
|
bool usingEmbeddedDaemon,
|
||||||
|
bool hasDaemon,
|
||||||
|
bool daemonRunning,
|
||||||
|
bool restartInProgress = false) {
|
||||||
|
switch (operation) {
|
||||||
|
case LifecycleOperation::ManualRestart:
|
||||||
|
if (!usingEmbeddedDaemon || restartInProgress) return {};
|
||||||
|
return {operation, true, daemonRunning, "daemon-restart", "Restarting daemon...", "",
|
||||||
|
true, false, true, 500};
|
||||||
|
case LifecycleOperation::Rescan:
|
||||||
|
if (!usingEmbeddedDaemon || !hasDaemon) {
|
||||||
|
return {operation, false, daemonRunning, "", "",
|
||||||
|
"Rescan requires embedded daemon. Restart your daemon with -rescan manually."};
|
||||||
|
}
|
||||||
|
return {operation, true, daemonRunning, "rescan-blockchain", "Starting rescan...", "",
|
||||||
|
false, true, false, 3000};
|
||||||
|
case LifecycleOperation::DeleteBlockchainData:
|
||||||
|
if (!usingEmbeddedDaemon || !hasDaemon) {
|
||||||
|
return {operation, false, daemonRunning, "", "",
|
||||||
|
"Delete blockchain requires embedded daemon. Stop your daemon manually and delete the data directory."};
|
||||||
|
}
|
||||||
|
return {operation, true, daemonRunning, "delete-blockchain-data", "Deleting blockchain data...", "",
|
||||||
|
false, false, false, 3000};
|
||||||
|
case LifecycleOperation::BootstrapStop:
|
||||||
|
return {operation, true, daemonRunning, "bootstrap-stop-daemon", "Stopping daemon for bootstrap...", "",
|
||||||
|
false, false, true, 0};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
void prepareLifecycleOperation(const LifecycleDecision& decision,
|
||||||
|
const config::Settings* settings = nullptr);
|
||||||
|
static inline LifecycleExecutionResult executeLifecycleOperation(const LifecycleDecision& decision,
|
||||||
|
LifecycleRuntime& runtime,
|
||||||
|
LifecycleTaskContext& task)
|
||||||
|
{
|
||||||
|
LifecycleExecutionResult result;
|
||||||
|
if (!decision.allowed) return result;
|
||||||
|
|
||||||
|
auto waitForDelay = [&]() {
|
||||||
|
int waitTicks = std::max(0, decision.restartDelayMs / 100);
|
||||||
|
for (int i = 0; i < waitTicks && !task.cancelled() && !task.shuttingDown(); ++i) {
|
||||||
|
task.sleepForMs(100);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
auto cancelled = [&]() {
|
||||||
|
result.cancelled = task.cancelled() || task.shuttingDown();
|
||||||
|
return result.cancelled;
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (decision.operation) {
|
||||||
|
case LifecycleOperation::BootstrapStop:
|
||||||
|
if (decision.wasRunning) {
|
||||||
|
runtime.requestRpcStopAndDisconnect("Bootstrap daemon stop", "Bootstrap");
|
||||||
|
result.stopped = true;
|
||||||
|
}
|
||||||
|
result.completed = true;
|
||||||
|
return result;
|
||||||
|
case LifecycleOperation::ManualRestart:
|
||||||
|
if (decision.wasRunning) {
|
||||||
|
runtime.stopDaemonWithPolicy();
|
||||||
|
result.stopped = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case LifecycleOperation::Rescan:
|
||||||
|
case LifecycleOperation::DeleteBlockchainData:
|
||||||
|
runtime.stopDaemonWithPolicy();
|
||||||
|
result.stopped = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cancelled()) return result;
|
||||||
|
waitForDelay();
|
||||||
|
if (cancelled()) return result;
|
||||||
|
|
||||||
|
if (decision.operation == LifecycleOperation::DeleteBlockchainData) {
|
||||||
|
result.deletedItems = runtime.deleteBlockchainData();
|
||||||
|
if (cancelled()) return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decision.operation == LifecycleOperation::Rescan ||
|
||||||
|
decision.operation == LifecycleOperation::DeleteBlockchainData) {
|
||||||
|
runtime.resetOutputOffset();
|
||||||
|
}
|
||||||
|
|
||||||
|
result.started = runtime.startDaemon();
|
||||||
|
result.completed = !cancelled();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
ShutdownDecision shutdownDecision(bool keepDaemonRunning,
|
||||||
|
bool stopExternalDaemon) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::unique_ptr<EmbeddedDaemon> daemon_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace daemon
|
||||||
|
} // namespace dragonx
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
// DragonX Wallet - ImGui Edition
|
// DragonX Wallet - ImGui Edition
|
||||||
// Copyright 2024-2026 The Hush Developers
|
// Copyright 2024-2026 The Hush Developers
|
||||||
// Released under the GPLv3
|
// Released under the GPLv3
|
||||||
|
//
|
||||||
|
// embedded_daemon.cpp — Manages the dragonxd child process lifecycle:
|
||||||
|
// binary discovery, process spawn, stdout/stderr monitoring, crash recovery.
|
||||||
|
|
||||||
#include "embedded_daemon.h"
|
#include "embedded_daemon.h"
|
||||||
#include "../config/version.h"
|
#include "../config/version.h"
|
||||||
#include "../resources/embedded_resources.h"
|
#include "../resources/embedded_resources.h"
|
||||||
|
#include "../util/platform.h"
|
||||||
|
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
@@ -17,12 +21,13 @@
|
|||||||
#include "../util/logger.h"
|
#include "../util/logger.h"
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
|
#include <winsock2.h>
|
||||||
|
#include <ws2tcpip.h>
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
#include <psapi.h>
|
#include <psapi.h>
|
||||||
#include <tlhelp32.h>
|
#include <tlhelp32.h>
|
||||||
#include <shlobj.h>
|
#include <shlobj.h>
|
||||||
#include <winsock2.h>
|
#include <iphlpapi.h>
|
||||||
#include <ws2tcpip.h>
|
|
||||||
#else
|
#else
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include <signal.h>
|
#include <signal.h>
|
||||||
@@ -32,6 +37,9 @@
|
|||||||
#include <sys/socket.h>
|
#include <sys/socket.h>
|
||||||
#include <netinet/in.h>
|
#include <netinet/in.h>
|
||||||
#include <arpa/inet.h>
|
#include <arpa/inet.h>
|
||||||
|
#ifdef __APPLE__
|
||||||
|
#include <sys/sysctl.h>
|
||||||
|
#endif
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
@@ -54,6 +62,8 @@ std::string EmbeddedDaemon::findDaemonBinary()
|
|||||||
char exe_path[MAX_PATH];
|
char exe_path[MAX_PATH];
|
||||||
GetModuleFileNameA(NULL, exe_path, MAX_PATH);
|
GetModuleFileNameA(NULL, exe_path, MAX_PATH);
|
||||||
exe_dir = fs::path(exe_path).parent_path().string();
|
exe_dir = fs::path(exe_path).parent_path().string();
|
||||||
|
#elif defined(__APPLE__)
|
||||||
|
exe_dir = util::Platform::getExecutableDirectory();
|
||||||
#else
|
#else
|
||||||
char exe_path[4096];
|
char exe_path[4096];
|
||||||
ssize_t len = readlink("/proc/self/exe", exe_path, sizeof(exe_path) - 1);
|
ssize_t len = readlink("/proc/self/exe", exe_path, sizeof(exe_path) - 1);
|
||||||
@@ -71,15 +81,10 @@ std::string EmbeddedDaemon::findDaemonBinary()
|
|||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
// Check wallet's own directory for manually placed binaries
|
// Check wallet's own directory for manually placed binaries
|
||||||
std::vector<std::string> localPaths = {
|
std::vector<std::string> localPaths = {
|
||||||
exe_dir + "\\hushd.exe",
|
|
||||||
exe_dir + "\\hush-arrakis-chain.exe",
|
|
||||||
exe_dir + "\\dragonxd.exe",
|
exe_dir + "\\dragonxd.exe",
|
||||||
exe_dir + "\\dragonxd.bat",
|
|
||||||
};
|
};
|
||||||
#else
|
#else
|
||||||
std::vector<std::string> localPaths = {
|
std::vector<std::string> localPaths = {
|
||||||
exe_dir + "/hush-arrakis-chain",
|
|
||||||
exe_dir + "/hushd",
|
|
||||||
exe_dir + "/dragonxd",
|
exe_dir + "/dragonxd",
|
||||||
};
|
};
|
||||||
#endif
|
#endif
|
||||||
@@ -103,41 +108,36 @@ std::string EmbeddedDaemon::findDaemonBinary()
|
|||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
// 3. Search additional well-known locations
|
// 3. Search additional well-known locations
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
// IMPORTANT: Always prefer hushd.exe directly over dragonxd.bat
|
// IMPORTANT: Always prefer dragonxd.exe directly over .bat wrappers.
|
||||||
// Using .bat files causes issues because cmd.exe exits immediately
|
// Using .bat files causes issues because cmd.exe exits immediately
|
||||||
// while hushd.exe continues running, making process monitoring fail.
|
// while dragonxd.exe continues running, making process monitoring fail.
|
||||||
std::vector<std::string> search_paths;
|
std::vector<std::string> search_paths;
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
// Parent directory
|
// Parent directory
|
||||||
if (!exe_dir.empty()) {
|
if (!exe_dir.empty()) {
|
||||||
search_paths.push_back(exe_dir + "\\..\\hushd.exe");
|
|
||||||
search_paths.push_back(exe_dir + "\\..\\dragonxd.bat");
|
|
||||||
search_paths.push_back(exe_dir + "\\..\\dragonxd.exe");
|
search_paths.push_back(exe_dir + "\\..\\dragonxd.exe");
|
||||||
}
|
}
|
||||||
search_paths.push_back("C:\\Program Files\\DragonX\\hushd.exe");
|
search_paths.push_back("C:\\Program Files\\DragonX\\dragonxd.exe");
|
||||||
#else
|
#else
|
||||||
if (!exe_dir.empty()) {
|
if (!exe_dir.empty()) {
|
||||||
search_paths.push_back(exe_dir + "/../hush-arrakis-chain");
|
|
||||||
search_paths.push_back(exe_dir + "/../bin/hush-arrakis-chain");
|
|
||||||
search_paths.push_back(exe_dir + "/../dragonxd");
|
search_paths.push_back(exe_dir + "/../dragonxd");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Standard Linux locations
|
// Standard Linux locations
|
||||||
search_paths.push_back("/usr/local/bin/hush-arrakis-chain");
|
search_paths.push_back("/usr/local/bin/dragonxd");
|
||||||
search_paths.push_back("/usr/bin/hush-arrakis-chain");
|
search_paths.push_back("/usr/bin/dragonxd");
|
||||||
|
|
||||||
// Home directory
|
// Home directory
|
||||||
const char* home = getenv("HOME");
|
const char* home = getenv("HOME");
|
||||||
if (home) {
|
if (home) {
|
||||||
search_paths.push_back(std::string(home) + "/hush3/src/hush-arrakis-chain");
|
search_paths.push_back(std::string(home) + "/dragonx/src/dragonxd");
|
||||||
search_paths.push_back(std::string(home) + "/hush3/src/dragonxd");
|
search_paths.push_back(std::string(home) + "/bin/dragonxd");
|
||||||
search_paths.push_back(std::string(home) + "/bin/hush-arrakis-chain");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef __APPLE__
|
#ifdef __APPLE__
|
||||||
// macOS app bundle
|
// macOS app bundle
|
||||||
search_paths.push_back("/Applications/DragonX.app/Contents/MacOS/hush-arrakis-chain");
|
search_paths.push_back("/Applications/DragonX.app/Contents/MacOS/dragonxd");
|
||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -158,21 +158,61 @@ std::vector<std::string> EmbeddedDaemon::getChainParams()
|
|||||||
// DragonX chain parameters.
|
// DragonX chain parameters.
|
||||||
// On Windows, omit -printtoconsole: we tail debug.log instead of piping stdout.
|
// On Windows, omit -printtoconsole: we tail debug.log instead of piping stdout.
|
||||||
// On Linux, -printtoconsole is used for pipe-based output capture.
|
// On Linux, -printtoconsole is used for pipe-based output capture.
|
||||||
|
// Auto-detect a reasonable -dbcache based on available physical RAM.
|
||||||
|
// Default LevelDB cache is small (~450MB); larger caches improve sync
|
||||||
|
// performance and reduce disk I/O — especially on macOS with APFS.
|
||||||
|
std::string dbcache_arg = "-dbcache=450";
|
||||||
|
{
|
||||||
|
#ifdef __APPLE__
|
||||||
|
// sysctl hw.memsize returns total physical RAM in bytes
|
||||||
|
int64_t memsize = 0;
|
||||||
|
size_t len = sizeof(memsize);
|
||||||
|
if (sysctlbyname("hw.memsize", &memsize, &len, nullptr, 0) == 0 && memsize > 0) {
|
||||||
|
int totalMB = static_cast<int>(memsize / (1024 * 1024));
|
||||||
|
// Use ~12.5% of RAM for dbcache, clamped to [450, 4096]
|
||||||
|
int cache = std::max(450, std::min(4096, totalMB / 8));
|
||||||
|
dbcache_arg = "-dbcache=" + std::to_string(cache);
|
||||||
|
}
|
||||||
|
#elif defined(__linux__)
|
||||||
|
long pages = sysconf(_SC_PHYS_PAGES);
|
||||||
|
long page_size = sysconf(_SC_PAGE_SIZE);
|
||||||
|
if (pages > 0 && page_size > 0) {
|
||||||
|
int totalMB = static_cast<int>((static_cast<int64_t>(pages) * page_size) / (1024 * 1024));
|
||||||
|
int cache = std::max(450, std::min(4096, totalMB / 8));
|
||||||
|
dbcache_arg = "-dbcache=" + std::to_string(cache);
|
||||||
|
}
|
||||||
|
#elif defined(_WIN32)
|
||||||
|
MEMORYSTATUSEX memInfo;
|
||||||
|
memInfo.dwLength = sizeof(memInfo);
|
||||||
|
if (GlobalMemoryStatusEx(&memInfo)) {
|
||||||
|
int totalMB = static_cast<int>(memInfo.ullTotalPhys / (1024 * 1024));
|
||||||
|
int cache = std::max(450, std::min(4096, totalMB / 8));
|
||||||
|
dbcache_arg = "-dbcache=" + std::to_string(cache);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
DEBUG_LOGF("[INFO] Using %s\n", dbcache_arg.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"-tls=only",
|
"-tls=only",
|
||||||
#ifndef _WIN32
|
#ifndef _WIN32
|
||||||
"-printtoconsole",
|
"-printtoconsole",
|
||||||
#endif
|
#endif
|
||||||
"-clientname=DragonXImGui",
|
"-clientname=ObsidianDragon",
|
||||||
"-ac_name=DRAGONX",
|
"-ac_name=DRAGONX",
|
||||||
"-ac_algo=randomx",
|
"-ac_algo=randomx",
|
||||||
"-ac_halving=3500000",
|
"-ac_halving=3500000",
|
||||||
"-ac_reward=300000000",
|
"-ac_reward=300000000",
|
||||||
"-ac_blocktime=36",
|
"-ac_blocktime=36",
|
||||||
"-ac_private=1",
|
"-ac_private=1",
|
||||||
"-addnode=176.126.87.241",
|
"-addnode=node.dragonx.is",
|
||||||
|
"-addnode=node1.dragonx.is",
|
||||||
|
"-addnode=node2.dragonx.is",
|
||||||
|
"-addnode=node3.dragonx.is",
|
||||||
|
"-addnode=node4.dragonx.is",
|
||||||
"-experimentalfeatures",
|
"-experimentalfeatures",
|
||||||
"-developerencryptwallet"
|
"-developerencryptwallet",
|
||||||
|
dbcache_arg
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,6 +274,113 @@ std::vector<std::string> EmbeddedDaemon::getRecentLines(int maxLines) const
|
|||||||
return lines;
|
return lines;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Identify what process owns a given TCP port.
|
||||||
|
// Returns a string like "PID 1234 (dragonxd.exe)" or "unknown" on failure.
|
||||||
|
static std::string getPortOwnerInfo(int port)
|
||||||
|
{
|
||||||
|
#ifdef _WIN32
|
||||||
|
DWORD size = 0;
|
||||||
|
// First call to get required buffer size
|
||||||
|
GetExtendedTcpTable(nullptr, &size, FALSE, AF_INET, TCP_TABLE_OWNER_PID_ALL, 0);
|
||||||
|
if (size == 0) return "unknown (GetExtendedTcpTable size query failed)";
|
||||||
|
std::vector<BYTE> buf(size);
|
||||||
|
DWORD ret = GetExtendedTcpTable(buf.data(), &size, FALSE, AF_INET, TCP_TABLE_OWNER_PID_ALL, 0);
|
||||||
|
if (ret != NO_ERROR) return "unknown (GetExtendedTcpTable failed, error " + std::to_string(ret) + ")";
|
||||||
|
auto* table = reinterpret_cast<MIB_TCPTABLE_OWNER_PID*>(buf.data());
|
||||||
|
DWORD ownerPid = 0;
|
||||||
|
for (DWORD i = 0; i < table->dwNumEntries; i++) {
|
||||||
|
auto& row = table->table[i];
|
||||||
|
int rowPort = ntohs(static_cast<u_short>(row.dwLocalPort));
|
||||||
|
// Match port in LISTEN state (MIB_TCP_STATE_LISTEN = 2)
|
||||||
|
if (rowPort == port && row.dwState == MIB_TCP_STATE_LISTEN) {
|
||||||
|
ownerPid = row.dwOwningPid;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ownerPid == 0) {
|
||||||
|
// Maybe it's in ESTABLISHED or another state from our connect probe — try any state
|
||||||
|
for (DWORD i = 0; i < table->dwNumEntries; i++) {
|
||||||
|
auto& row = table->table[i];
|
||||||
|
int rowPort = ntohs(static_cast<u_short>(row.dwLocalPort));
|
||||||
|
if (rowPort == port && row.dwOwningPid != 0) {
|
||||||
|
ownerPid = row.dwOwningPid;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ownerPid == 0) return "unknown (no PID found for port " + std::to_string(port) + ")";
|
||||||
|
// Resolve PID to process name
|
||||||
|
std::string procName = "<unknown>";
|
||||||
|
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
||||||
|
if (snap != INVALID_HANDLE_VALUE) {
|
||||||
|
PROCESSENTRY32 pe;
|
||||||
|
pe.dwSize = sizeof(pe);
|
||||||
|
if (Process32First(snap, &pe)) {
|
||||||
|
do {
|
||||||
|
if (pe.th32ProcessID == ownerPid) {
|
||||||
|
procName = pe.szExeFile;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} while (Process32Next(snap, &pe));
|
||||||
|
}
|
||||||
|
CloseHandle(snap);
|
||||||
|
}
|
||||||
|
return "PID " + std::to_string(ownerPid) + " (" + procName + ")";
|
||||||
|
#else
|
||||||
|
// Linux: parse /proc/net/tcp to find the inode, then scan /proc/*/fd
|
||||||
|
FILE* fp = fopen("/proc/net/tcp", "r");
|
||||||
|
if (!fp) return "unknown (cannot read /proc/net/tcp)";
|
||||||
|
char line[512];
|
||||||
|
unsigned long inode = 0;
|
||||||
|
while (fgets(line, sizeof(line), fp)) {
|
||||||
|
unsigned int localPort, state;
|
||||||
|
unsigned long lineInode;
|
||||||
|
if (sscanf(line, " %*d: %*X:%X %*X:%*X %X %*X:%*X %*X:%*X %*X %*u %*u %lu",
|
||||||
|
&localPort, &state, &lineInode) == 3) {
|
||||||
|
if (static_cast<int>(localPort) == port && state == 0x0A) { // 0x0A = LISTEN
|
||||||
|
inode = lineInode;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fclose(fp);
|
||||||
|
if (inode == 0) return "unknown (no listener found for port " + std::to_string(port) + ")";
|
||||||
|
// Scan /proc/*/fd/* for the matching inode
|
||||||
|
for (const auto& entry : fs::directory_iterator("/proc")) {
|
||||||
|
if (!entry.is_directory()) continue;
|
||||||
|
std::string pidStr = entry.path().filename().string();
|
||||||
|
if (pidStr.empty() || !std::isdigit(pidStr[0])) continue;
|
||||||
|
std::string fdDir = "/proc/" + pidStr + "/fd";
|
||||||
|
try {
|
||||||
|
for (const auto& fdEntry : fs::directory_iterator(fdDir)) {
|
||||||
|
char target[512];
|
||||||
|
ssize_t len = readlink(fdEntry.path().c_str(), target, sizeof(target) - 1);
|
||||||
|
if (len > 0) {
|
||||||
|
target[len] = '\0';
|
||||||
|
std::string t(target);
|
||||||
|
if (t.find("socket:[" + std::to_string(inode) + "]") != std::string::npos) {
|
||||||
|
// Found the PID, now get the process name
|
||||||
|
std::string commPath = "/proc/" + pidStr + "/comm";
|
||||||
|
FILE* cf = fopen(commPath.c_str(), "r");
|
||||||
|
std::string procName = "<unknown>";
|
||||||
|
if (cf) {
|
||||||
|
char name[256];
|
||||||
|
if (fgets(name, sizeof(name), cf)) {
|
||||||
|
procName = name;
|
||||||
|
while (!procName.empty() && procName.back() == '\n') procName.pop_back();
|
||||||
|
}
|
||||||
|
fclose(cf);
|
||||||
|
}
|
||||||
|
return "PID " + pidStr + " (" + procName + ")";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (...) { /* permission denied — skip */ }
|
||||||
|
}
|
||||||
|
return "unknown (inode " + std::to_string(inode) + " found but no matching PID)";
|
||||||
|
#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)
|
||||||
static bool isPortInUse(int port)
|
static bool isPortInUse(int port)
|
||||||
{
|
{
|
||||||
@@ -251,25 +398,35 @@ static bool isPortInUse(int port)
|
|||||||
WSACleanup();
|
WSACleanup();
|
||||||
return (result == 0);
|
return (result == 0);
|
||||||
#else
|
#else
|
||||||
// Read /proc/net/tcp to check for listeners — avoids creating sockets
|
// On macOS /proc doesn't exist; on Linux prefer /proc/net/tcp to avoid
|
||||||
// which would add conntrack entries and consume ephemeral ports.
|
// creating sockets. Fall back to connect() if /proc is unavailable.
|
||||||
FILE* fp = fopen("/proc/net/tcp", "r");
|
FILE* fp = fopen("/proc/net/tcp", "r");
|
||||||
if (!fp) return false;
|
if (fp) {
|
||||||
char line[256];
|
char line[256];
|
||||||
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)) {
|
||||||
// Format: sl local_address rem_address st ...
|
if (sscanf(line, " %*d: %*X:%X %*X:%*X %X", &localPort, &state) == 2) {
|
||||||
// local_address is HEXIP:HEXPORT, state 0x0A = TCP_LISTEN
|
if (localPort == static_cast<unsigned int>(port) && state == 0x0A) {
|
||||||
if (sscanf(line, " %*d: %*X:%X %*X:%*X %X", &localPort, &state) == 2) {
|
found = true;
|
||||||
if (localPort == static_cast<unsigned int>(port) && state == 0x0A) {
|
break;
|
||||||
found = true;
|
}
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fclose(fp);
|
||||||
|
return found;
|
||||||
}
|
}
|
||||||
fclose(fp);
|
// Fallback (macOS): try to connect
|
||||||
return found;
|
int sock = socket(AF_INET, SOCK_STREAM, 0);
|
||||||
|
if (sock < 0) return false;
|
||||||
|
struct sockaddr_in addr;
|
||||||
|
memset(&addr, 0, sizeof(addr));
|
||||||
|
addr.sin_family = AF_INET;
|
||||||
|
addr.sin_port = htons(static_cast<uint16_t>(port));
|
||||||
|
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
||||||
|
int result = connect(sock, (struct sockaddr*)&addr, sizeof(addr));
|
||||||
|
close(sock);
|
||||||
|
return (result == 0);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,10 +446,11 @@ bool EmbeddedDaemon::start(const std::string& binary_path)
|
|||||||
// Check if something is already listening on the RPC port
|
// Check if something is already listening on the RPC port
|
||||||
int rpc_port = std::atoi(DRAGONX_DEFAULT_RPC_PORT);
|
int rpc_port = std::atoi(DRAGONX_DEFAULT_RPC_PORT);
|
||||||
if (isPortInUse(rpc_port)) {
|
if (isPortInUse(rpc_port)) {
|
||||||
DEBUG_LOGF("[INFO] Port %d is already in use — external daemon detected, will connect to it.\\n", rpc_port);
|
std::string owner = getPortOwnerInfo(rpc_port);
|
||||||
|
VERBOSE_LOGF("[INFO] Port %d is already in use by %s — external daemon detected, will connect to it.\\n", rpc_port, owner.c_str());
|
||||||
external_daemon_detected_ = true;
|
external_daemon_detected_ = true;
|
||||||
// Don't set Error — the wallet will connect to the running daemon.
|
// Don't set Error — the wallet will connect to the running daemon.
|
||||||
setState(State::Stopped, "External daemon detected on port " + std::string(DRAGONX_DEFAULT_RPC_PORT));
|
setState(State::Stopped, "External daemon detected on port " + std::string(DRAGONX_DEFAULT_RPC_PORT) + " (owned by " + owner + ")");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
external_daemon_detected_ = false;
|
external_daemon_detected_ = false;
|
||||||
@@ -318,6 +476,17 @@ bool EmbeddedDaemon::start(const std::string& binary_path)
|
|||||||
for (const auto& cat : debug_categories_) {
|
for (const auto& cat : debug_categories_) {
|
||||||
args.push_back("-debug=" + cat);
|
args.push_back("-debug=" + cat);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Append max connections if configured (0 = daemon default)
|
||||||
|
if (max_connections_ > 0) {
|
||||||
|
args.push_back("-maxconnections=" + std::to_string(max_connections_));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add -rescan flag if requested (one-shot)
|
||||||
|
if (rescan_on_next_start_.exchange(false)) {
|
||||||
|
DEBUG_LOGF("[INFO] Adding -rescan flag for blockchain rescan\n");
|
||||||
|
args.push_back("-rescan");
|
||||||
|
}
|
||||||
|
|
||||||
if (!startProcess(daemon_path, args)) {
|
if (!startProcess(daemon_path, args)) {
|
||||||
DEBUG_LOGF("[ERROR] Failed to start dragonxd process: %s\\n", last_error_.c_str());
|
DEBUG_LOGF("[ERROR] Failed to start dragonxd process: %s\\n", last_error_.c_str());
|
||||||
@@ -397,7 +566,7 @@ bool EmbeddedDaemon::startProcess(const std::string& binary_path, const std::vec
|
|||||||
|
|
||||||
// Launch daemon with CREATE_NEW_CONSOLE (hidden via SW_HIDE).
|
// Launch daemon with CREATE_NEW_CONSOLE (hidden via SW_HIDE).
|
||||||
// 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>/hush3/ 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;
|
||||||
PROCESS_INFORMATION pi;
|
PROCESS_INFORMATION pi;
|
||||||
ZeroMemory(&si, sizeof(si));
|
ZeroMemory(&si, sizeof(si));
|
||||||
@@ -523,7 +692,7 @@ void EmbeddedDaemon::drainOutput()
|
|||||||
std::lock_guard<std::mutex> lk(output_mutex_);
|
std::lock_guard<std::mutex> lk(output_mutex_);
|
||||||
appendOutput(buffer, bytes_read);
|
appendOutput(buffer, bytes_read);
|
||||||
}
|
}
|
||||||
DEBUG_LOGF("[dragonxd] %s", buffer);
|
VERBOSE_LOGF("[dragonxd] %s", buffer);
|
||||||
debug_log_offset_ += bytes_read;
|
debug_log_offset_ += bytes_read;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -558,7 +727,7 @@ void EmbeddedDaemon::stop(int wait_ms)
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (tracked_alive) {
|
if (tracked_alive) {
|
||||||
// Our tracked process (hushd.exe launched directly) is still alive.
|
// Our tracked process (dragonxd.exe launched directly) is still alive.
|
||||||
// The RPC "stop" was already sent by the caller — wait for it to exit.
|
// The RPC "stop" was already sent by the caller — wait for it to exit.
|
||||||
DEBUG_LOGF("Waiting up to %d ms for tracked daemon process to exit...\n", wait_ms);
|
DEBUG_LOGF("Waiting up to %d ms for tracked daemon process to exit...\n", wait_ms);
|
||||||
if (!pollWait(process_handle_, wait_ms)) {
|
if (!pollWait(process_handle_, wait_ms)) {
|
||||||
@@ -571,33 +740,33 @@ void EmbeddedDaemon::stop(int wait_ms)
|
|||||||
process_handle_ = nullptr;
|
process_handle_ = nullptr;
|
||||||
} else {
|
} else {
|
||||||
// Tracked handle is dead (batch file case: cmd.exe already exited).
|
// Tracked handle is dead (batch file case: cmd.exe already exited).
|
||||||
// The real hushd.exe may still be running as an orphan.
|
// The real dragonxd.exe may still be running as an orphan.
|
||||||
if (process_handle_ != nullptr) {
|
if (process_handle_ != nullptr) {
|
||||||
CloseHandle(process_handle_);
|
CloseHandle(process_handle_);
|
||||||
process_handle_ = nullptr;
|
process_handle_ = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the real hushd.exe process by name
|
// Find the real dragonxd.exe process by name
|
||||||
DWORD hushd_pid = findProcessByName("hushd.exe");
|
DWORD dragonxd_pid = findProcessByName("dragonxd.exe");
|
||||||
if (hushd_pid != 0) {
|
if (dragonxd_pid != 0) {
|
||||||
DEBUG_LOGF("Found orphaned hushd.exe (PID %lu) — waiting for RPC stop to take effect...\n", hushd_pid);
|
DEBUG_LOGF("Found orphaned dragonxd.exe (PID %lu) — waiting for RPC stop to take effect...\n", dragonxd_pid);
|
||||||
HANDLE hProc = OpenProcess(PROCESS_TERMINATE | SYNCHRONIZE, FALSE, hushd_pid);
|
HANDLE hProc = OpenProcess(PROCESS_TERMINATE | SYNCHRONIZE, FALSE, dragonxd_pid);
|
||||||
if (hProc) {
|
if (hProc) {
|
||||||
// RPC stop was already sent — wait for graceful exit
|
// RPC stop was already sent — wait for graceful exit
|
||||||
if (!pollWait(hProc, wait_ms)) {
|
if (!pollWait(hProc, wait_ms)) {
|
||||||
DEBUG_LOGF("Timeout — forcing hushd.exe (PID %lu) termination...\n", hushd_pid);
|
DEBUG_LOGF("Timeout — forcing dragonxd.exe (PID %lu) termination...\n", dragonxd_pid);
|
||||||
TerminateProcess(hProc, 1);
|
TerminateProcess(hProc, 1);
|
||||||
WaitForSingleObject(hProc, 2000);
|
WaitForSingleObject(hProc, 2000);
|
||||||
}
|
}
|
||||||
drainOutput();
|
drainOutput();
|
||||||
CloseHandle(hProc);
|
CloseHandle(hProc);
|
||||||
DEBUG_LOGF("hushd.exe stopped\n");
|
DEBUG_LOGF("dragonxd.exe stopped\n");
|
||||||
} else {
|
} else {
|
||||||
DEBUG_LOGF("Could not open hushd.exe process (PID %lu), error %lu\n",
|
DEBUG_LOGF("Could not open dragonxd.exe process (PID %lu), error %lu\n",
|
||||||
hushd_pid, GetLastError());
|
dragonxd_pid, GetLastError());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
DEBUG_LOGF("No hushd.exe process found — daemon may have already exited\n");
|
DEBUG_LOGF("No dragonxd.exe process found — daemon may have already exited\n");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -732,11 +901,11 @@ bool EmbeddedDaemon::startProcess(const std::string& binary_path, const std::vec
|
|||||||
close(pipefd[0]); // Close read end
|
close(pipefd[0]); // Close read end
|
||||||
|
|
||||||
// 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 hushd spawned by a wrapper script).
|
// group later (including dragonxd spawned by a wrapper script).
|
||||||
// Without this, SIGTERM only kills the shell, leaving hushd orphaned.
|
// Without this, SIGTERM only kills the shell, leaving dragonxd orphaned.
|
||||||
setpgid(0, 0);
|
setpgid(0, 0);
|
||||||
|
|
||||||
// Change to the daemon binary's directory so hushd can find
|
// Change to the daemon binary's directory so dragonxd can find
|
||||||
// sapling params via its PWD search path (same as CreateProcessA
|
// sapling params via its PWD search path (same as CreateProcessA
|
||||||
// lpCurrentDirectory on Windows).
|
// lpCurrentDirectory on Windows).
|
||||||
{
|
{
|
||||||
@@ -763,8 +932,7 @@ bool EmbeddedDaemon::startProcess(const std::string& binary_path, const std::vec
|
|||||||
bool is_script = false;
|
bool is_script = false;
|
||||||
if (binary_path.size() >= 3) {
|
if (binary_path.size() >= 3) {
|
||||||
std::string ext = binary_path.substr(binary_path.size() - 3);
|
std::string ext = binary_path.substr(binary_path.size() - 3);
|
||||||
if (ext == ".sh" || binary_path.find("hush-arrakis-chain") != std::string::npos ||
|
if (ext == ".sh" || binary_path.find("dragonxd") != std::string::npos) {
|
||||||
binary_path.find("dragonxd") != std::string::npos) {
|
|
||||||
// Check if it's a script by looking at first bytes
|
// Check if it's a script by looking at first bytes
|
||||||
FILE* f = fopen(binary_path.c_str(), "r");
|
FILE* f = fopen(binary_path.c_str(), "r");
|
||||||
if (f) {
|
if (f) {
|
||||||
@@ -821,8 +989,24 @@ double EmbeddedDaemon::getMemoryUsageMB() const
|
|||||||
{
|
{
|
||||||
if (process_pid_ <= 0) return 0.0;
|
if (process_pid_ <= 0) return 0.0;
|
||||||
|
|
||||||
// The tracked PID is often a bash wrapper script; the real daemon
|
#ifdef __APPLE__
|
||||||
// (hushd) is a child in the same process group. Sum VmRSS for every
|
// macOS: use ps to read RSS for the daemon process and its children
|
||||||
|
// in the same process group.
|
||||||
|
char cmd[128];
|
||||||
|
snprintf(cmd, sizeof(cmd), "ps -o rss= -g %d 2>/dev/null", process_pid_);
|
||||||
|
FILE* fp = popen(cmd, "r");
|
||||||
|
if (!fp) return 0.0;
|
||||||
|
double total_rss_mb = 0.0;
|
||||||
|
char line[64];
|
||||||
|
while (fgets(line, sizeof(line), fp)) {
|
||||||
|
long rss_kb = atol(line);
|
||||||
|
if (rss_kb > 0) total_rss_mb += static_cast<double>(rss_kb) / 1024.0;
|
||||||
|
}
|
||||||
|
pclose(fp);
|
||||||
|
return total_rss_mb;
|
||||||
|
#else
|
||||||
|
// Linux: The tracked PID is often a bash wrapper script; the real daemon
|
||||||
|
// (dragonxd) is a child in the same process group. Sum VmRSS for every
|
||||||
// process whose PGID matches our tracked PID.
|
// process whose PGID matches our tracked PID.
|
||||||
double total_rss_mb = 0.0;
|
double total_rss_mb = 0.0;
|
||||||
|
|
||||||
@@ -871,6 +1055,7 @@ double EmbeddedDaemon::getMemoryUsageMB() const
|
|||||||
}
|
}
|
||||||
|
|
||||||
return total_rss_mb;
|
return total_rss_mb;
|
||||||
|
#endif // __APPLE__ / Linux
|
||||||
}
|
}
|
||||||
|
|
||||||
bool EmbeddedDaemon::isRunning() const
|
bool EmbeddedDaemon::isRunning() const
|
||||||
@@ -900,7 +1085,7 @@ void EmbeddedDaemon::drainOutput()
|
|||||||
std::lock_guard<std::mutex> lk(output_mutex_);
|
std::lock_guard<std::mutex> lk(output_mutex_);
|
||||||
appendOutput(buffer, static_cast<size_t>(n));
|
appendOutput(buffer, static_cast<size_t>(n));
|
||||||
}
|
}
|
||||||
DEBUG_LOGF("[dragonxd] %s", buffer);
|
VERBOSE_LOGF("[dragonxd] %s", buffer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -911,14 +1096,12 @@ void EmbeddedDaemon::stop(int wait_ms)
|
|||||||
if (process_pid_ > 0) {
|
if (process_pid_ > 0) {
|
||||||
setState(State::Stopping, "Stopping dragonxd...");
|
setState(State::Stopping, "Stopping dragonxd...");
|
||||||
|
|
||||||
// Send SIGTERM to the entire process group (negative PID).
|
// Phase 1: Wait for the daemon to exit naturally.
|
||||||
// This ensures that if dragonxd is a shell script wrapper,
|
// The caller (stopEmbeddedDaemon) already sent an RPC "stop" which
|
||||||
// both bash AND the actual hushd child receive the signal.
|
// tells the daemon to flush LevelDB, close sockets, and exit cleanly.
|
||||||
// Without this, only bash is killed and hushd is orphaned.
|
// On macOS/APFS the LevelDB flush can take several seconds — we must
|
||||||
DEBUG_LOGF("Sending SIGTERM to process group -%d\n", process_pid_);
|
// NOT send SIGTERM until the daemon has had enough time to finish.
|
||||||
kill(-process_pid_, SIGTERM);
|
DEBUG_LOGF("Waiting up to %d ms for daemon to exit after RPC stop...\n", wait_ms);
|
||||||
|
|
||||||
// Wait for process to exit, draining stdout each iteration
|
|
||||||
auto start = std::chrono::steady_clock::now();
|
auto start = std::chrono::steady_clock::now();
|
||||||
while (isRunning()) {
|
while (isRunning()) {
|
||||||
drainOutput();
|
drainOutput();
|
||||||
@@ -927,15 +1110,34 @@ void EmbeddedDaemon::stop(int wait_ms)
|
|||||||
std::chrono::steady_clock::now() - start).count();
|
std::chrono::steady_clock::now() - start).count();
|
||||||
|
|
||||||
if (elapsed >= wait_ms) {
|
if (elapsed >= wait_ms) {
|
||||||
// Force kill the entire process group
|
|
||||||
DEBUG_LOGF("Forcing dragonxd termination with SIGKILL (group -%d)...\n", process_pid_);
|
|
||||||
kill(-process_pid_, SIGKILL);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Phase 2: If still running, send SIGTERM and wait a further 10s.
|
||||||
|
if (isRunning()) {
|
||||||
|
DEBUG_LOGF("Daemon did not exit gracefully — sending SIGTERM to process group -%d\n", process_pid_);
|
||||||
|
kill(-process_pid_, SIGTERM);
|
||||||
|
|
||||||
|
auto sigterm_start = std::chrono::steady_clock::now();
|
||||||
|
while (isRunning()) {
|
||||||
|
drainOutput();
|
||||||
|
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||||
|
std::chrono::steady_clock::now() - sigterm_start).count();
|
||||||
|
if (elapsed >= 10000) {
|
||||||
|
// Phase 3: Force kill
|
||||||
|
DEBUG_LOGF("Forcing dragonxd termination with SIGKILL (group -%d)...\n", process_pid_);
|
||||||
|
kill(-process_pid_, SIGKILL);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
DEBUG_LOGF("Daemon exited cleanly after RPC stop\n");
|
||||||
|
}
|
||||||
|
|
||||||
drainOutput(); // read any remaining output
|
drainOutput(); // read any remaining output
|
||||||
|
|
||||||
// Reap the child process
|
// Reap the child process
|
||||||
@@ -992,7 +1194,7 @@ void EmbeddedDaemon::monitorProcess()
|
|||||||
std::lock_guard<std::mutex> lk(output_mutex_);
|
std::lock_guard<std::mutex> lk(output_mutex_);
|
||||||
appendOutput(buffer, static_cast<size_t>(bytes_read));
|
appendOutput(buffer, static_cast<size_t>(bytes_read));
|
||||||
}
|
}
|
||||||
DEBUG_LOGF("[dragonxd] %s", buffer);
|
VERBOSE_LOGF("[dragonxd] %s", buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||||
|
|||||||
@@ -116,6 +116,26 @@ public:
|
|||||||
* @brief Get last N lines of daemon output (thread-safe snapshot)
|
* @brief Get last N lines of daemon output (thread-safe snapshot)
|
||||||
*/
|
*/
|
||||||
std::vector<std::string> getRecentLines(int maxLines = 8) const;
|
std::vector<std::string> getRecentLines(int maxLines = 8) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Extract the latest block height from daemon output (thread-safe).
|
||||||
|
* Parses the last "height=N" from UpdateTip lines without copying
|
||||||
|
* the entire output buffer. Returns -1 if no UpdateTip found.
|
||||||
|
*/
|
||||||
|
int getLastBlockHeight() const {
|
||||||
|
std::lock_guard<std::mutex> lk(output_mutex_);
|
||||||
|
// Search backwards from the end for "height="
|
||||||
|
size_t pos = process_output_.rfind("height=");
|
||||||
|
if (pos == std::string::npos) return -1;
|
||||||
|
pos += 7; // skip "height="
|
||||||
|
int h = 0;
|
||||||
|
for (size_t i = pos; i < process_output_.size(); ++i) {
|
||||||
|
char c = process_output_[i];
|
||||||
|
if (c >= '0' && c <= '9') h = h * 10 + (c - '0');
|
||||||
|
else break;
|
||||||
|
}
|
||||||
|
return h > 0 ? h : -1;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Whether start() detected an existing daemon on the RPC port.
|
* @brief Whether start() detected an existing daemon on the RPC port.
|
||||||
@@ -152,6 +172,17 @@ public:
|
|||||||
void setDebugCategories(const std::set<std::string>& cats) { debug_categories_ = cats; }
|
void setDebugCategories(const std::set<std::string>& cats) { debug_categories_ = cats; }
|
||||||
const std::set<std::string>& getDebugCategories() const { return debug_categories_; }
|
const std::set<std::string>& getDebugCategories() const { return debug_categories_; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set maximum peer connections (0 = use daemon default)
|
||||||
|
*/
|
||||||
|
void setMaxConnections(int v) { max_connections_ = v; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Request a blockchain rescan on the next daemon start
|
||||||
|
*/
|
||||||
|
void setRescanOnNextStart(bool v) { rescan_on_next_start_ = v; }
|
||||||
|
bool rescanOnNextStart() const { return rescan_on_next_start_.load(); }
|
||||||
|
|
||||||
/** Get number of consecutive daemon crashes (resets on successful start or manual reset) */
|
/** Get number of consecutive daemon crashes (resets on successful start or manual reset) */
|
||||||
int getCrashCount() const { return crash_count_.load(); }
|
int getCrashCount() const { return crash_count_.load(); }
|
||||||
/** Reset crash counter (call on successful connection or manual restart) */
|
/** Reset crash counter (call on successful connection or manual restart) */
|
||||||
@@ -188,7 +219,9 @@ private:
|
|||||||
std::thread monitor_thread_;
|
std::thread monitor_thread_;
|
||||||
std::atomic<bool> should_stop_{false};
|
std::atomic<bool> should_stop_{false};
|
||||||
std::set<std::string> debug_categories_;
|
std::set<std::string> debug_categories_;
|
||||||
|
int max_connections_ = 0; // 0 = daemon default
|
||||||
std::atomic<int> crash_count_{0}; // consecutive crash counter
|
std::atomic<int> crash_count_{0}; // consecutive crash counter
|
||||||
|
std::atomic<bool> rescan_on_next_start_{false}; // -rescan flag for next start
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace daemon
|
} // namespace daemon
|
||||||
|
|||||||
93
src/daemon/lifecycle_adapters.cpp
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
#include "lifecycle_adapters.h"
|
||||||
|
#include "../util/logger.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <chrono>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
namespace dragonx {
|
||||||
|
namespace daemon {
|
||||||
|
|
||||||
|
AsyncLifecycleTaskContext::AsyncLifecycleTaskContext(
|
||||||
|
const util::AsyncTaskManager::Token& token,
|
||||||
|
const std::atomic<bool>& shuttingDown)
|
||||||
|
: token_(token), shuttingDown_(shuttingDown)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AsyncLifecycleTaskContext::cancelled() const
|
||||||
|
{
|
||||||
|
return token_.cancelled();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AsyncLifecycleTaskContext::shuttingDown() const
|
||||||
|
{
|
||||||
|
return shuttingDown_.load(std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AsyncLifecycleTaskContext::sleepForMs(int milliseconds)
|
||||||
|
{
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(std::max(0, milliseconds)));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ImmediateLifecycleTaskContext::cancelled() const
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ImmediateLifecycleTaskContext::shuttingDown() const
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImmediateLifecycleTaskContext::sleepForMs(int)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
int BlockchainDataCleaner::removeBlockchainData(const std::filesystem::path& dataDir)
|
||||||
|
{
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
static constexpr std::array<const char*, 4> directories = {
|
||||||
|
"blocks", "chainstate", "database", "notarizations"
|
||||||
|
};
|
||||||
|
static constexpr std::array<const char*, 5> files = {
|
||||||
|
"peers.dat", "fee_estimates.dat", "banlist.dat", "db.log", ".lock"
|
||||||
|
};
|
||||||
|
|
||||||
|
int removed = 0;
|
||||||
|
for (const char* directoryName : directories) {
|
||||||
|
fs::path path = dataDir / directoryName;
|
||||||
|
std::error_code existsError;
|
||||||
|
if (!fs::exists(path, existsError)) continue;
|
||||||
|
|
||||||
|
std::error_code removeError;
|
||||||
|
auto count = fs::remove_all(path, removeError);
|
||||||
|
if (!removeError) {
|
||||||
|
removed += static_cast<int>(count);
|
||||||
|
DEBUG_LOGF("[DaemonLifecycle] Removed %s (%d entries)\n",
|
||||||
|
directoryName, static_cast<int>(count));
|
||||||
|
} else {
|
||||||
|
DEBUG_LOGF("[DaemonLifecycle] Failed to remove %s: %s\n",
|
||||||
|
directoryName, removeError.message().c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const char* fileName : files) {
|
||||||
|
fs::path path = dataDir / fileName;
|
||||||
|
std::error_code removeError;
|
||||||
|
if (fs::remove(path, removeError)) {
|
||||||
|
++removed;
|
||||||
|
DEBUG_LOGF("[DaemonLifecycle] Removed %s\n", fileName);
|
||||||
|
} else if (removeError) {
|
||||||
|
DEBUG_LOGF("[DaemonLifecycle] Failed to remove %s: %s\n",
|
||||||
|
fileName, removeError.message().c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace daemon
|
||||||
|
} // namespace dragonx
|
||||||
39
src/daemon/lifecycle_adapters.h
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "daemon_controller.h"
|
||||||
|
#include "../util/async_task_manager.h"
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <filesystem>
|
||||||
|
|
||||||
|
namespace dragonx {
|
||||||
|
namespace daemon {
|
||||||
|
|
||||||
|
class AsyncLifecycleTaskContext final : public DaemonController::LifecycleTaskContext {
|
||||||
|
public:
|
||||||
|
AsyncLifecycleTaskContext(const util::AsyncTaskManager::Token& token,
|
||||||
|
const std::atomic<bool>& shuttingDown);
|
||||||
|
|
||||||
|
bool cancelled() const override;
|
||||||
|
bool shuttingDown() const override;
|
||||||
|
void sleepForMs(int milliseconds) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
const util::AsyncTaskManager::Token& token_;
|
||||||
|
const std::atomic<bool>& shuttingDown_;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ImmediateLifecycleTaskContext final : public DaemonController::LifecycleTaskContext {
|
||||||
|
public:
|
||||||
|
bool cancelled() const override;
|
||||||
|
bool shuttingDown() const override;
|
||||||
|
void sleepForMs(int milliseconds) override;
|
||||||
|
};
|
||||||
|
|
||||||
|
class BlockchainDataCleaner final {
|
||||||
|
public:
|
||||||
|
static int removeBlockchainData(const std::filesystem::path& dataDir);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace daemon
|
||||||
|
} // namespace dragonx
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
// DragonX Wallet - ImGui Edition
|
// DragonX Wallet - ImGui Edition
|
||||||
// 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.
|
||||||
|
// Spawns xmrig, monitors via HTTP API, tracks hashrate and shares.
|
||||||
|
|
||||||
#include "xmrig_manager.h"
|
#include "xmrig_manager.h"
|
||||||
#include "../resources/embedded_resources.h"
|
#include "../resources/embedded_resources.h"
|
||||||
@@ -244,6 +247,18 @@ bool XmrigManager::start(const Config& cfg) {
|
|||||||
}
|
}
|
||||||
stats_ = PoolStats{};
|
stats_ = PoolStats{};
|
||||||
|
|
||||||
|
// Extract pool hostname for stats API queries
|
||||||
|
{
|
||||||
|
std::string url = cfg.pool_url;
|
||||||
|
// Strip protocol prefix if present
|
||||||
|
auto pos = url.find("://");
|
||||||
|
if (pos != std::string::npos) url = url.substr(pos + 3);
|
||||||
|
// Strip port suffix
|
||||||
|
pos = url.find(':');
|
||||||
|
if (pos != std::string::npos) url = url.substr(0, pos);
|
||||||
|
pool_host_ = url;
|
||||||
|
}
|
||||||
|
|
||||||
// Find binary
|
// Find binary
|
||||||
std::string binary = findXmrigBinary();
|
std::string binary = findXmrigBinary();
|
||||||
if (binary.empty()) {
|
if (binary.empty()) {
|
||||||
@@ -454,6 +469,18 @@ bool XmrigManager::startProcess(const std::string& xmrigPath, const std::string&
|
|||||||
dup2(pipefd[1], STDERR_FILENO);
|
dup2(pipefd[1], STDERR_FILENO);
|
||||||
close(pipefd[1]);
|
close(pipefd[1]);
|
||||||
|
|
||||||
|
// Detach from controlling terminal's stdin to prevent SIGTTIN/SIGTTOU
|
||||||
|
// when running in a new process group (setpgid below).
|
||||||
|
int devnull = open("/dev/null", O_RDONLY);
|
||||||
|
if (devnull >= 0) {
|
||||||
|
dup2(devnull, STDIN_FILENO);
|
||||||
|
close(devnull);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore job-control signals that a background process group may receive
|
||||||
|
signal(SIGTTIN, SIG_IGN);
|
||||||
|
signal(SIGTTOU, SIG_IGN);
|
||||||
|
|
||||||
// New process group so we can kill the whole group
|
// New process group so we can kill the whole group
|
||||||
setpgid(0, 0);
|
setpgid(0, 0);
|
||||||
|
|
||||||
@@ -488,6 +515,21 @@ bool XmrigManager::isRunning() const {
|
|||||||
|
|
||||||
double XmrigManager::getMemoryUsageMB() const {
|
double XmrigManager::getMemoryUsageMB() const {
|
||||||
if (process_pid_ <= 0) return 0.0;
|
if (process_pid_ <= 0) return 0.0;
|
||||||
|
#ifdef __APPLE__
|
||||||
|
// macOS: use ps to read RSS for xmrig process
|
||||||
|
char cmd[128];
|
||||||
|
snprintf(cmd, sizeof(cmd), "ps -o rss= -p %d 2>/dev/null", process_pid_);
|
||||||
|
FILE* fp = popen(cmd, "r");
|
||||||
|
if (!fp) return 0.0;
|
||||||
|
char line[64];
|
||||||
|
double mb = 0.0;
|
||||||
|
if (fgets(line, sizeof(line), fp)) {
|
||||||
|
long rss_kb = atol(line);
|
||||||
|
if (rss_kb > 0) mb = static_cast<double>(rss_kb) / 1024.0;
|
||||||
|
}
|
||||||
|
pclose(fp);
|
||||||
|
return mb;
|
||||||
|
#else
|
||||||
char path[64];
|
char path[64];
|
||||||
snprintf(path, sizeof(path), "/proc/%d/statm", process_pid_);
|
snprintf(path, sizeof(path), "/proc/%d/statm", process_pid_);
|
||||||
FILE* fp = fopen(path, "r");
|
FILE* fp = fopen(path, "r");
|
||||||
@@ -499,6 +541,7 @@ double XmrigManager::getMemoryUsageMB() const {
|
|||||||
fclose(fp);
|
fclose(fp);
|
||||||
long pageSize = sysconf(_SC_PAGESIZE);
|
long pageSize = sysconf(_SC_PAGESIZE);
|
||||||
return static_cast<double>(pages * pageSize) / (1024.0 * 1024.0);
|
return static_cast<double>(pages * pageSize) / (1024.0 * 1024.0);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void XmrigManager::drainOutput() {
|
void XmrigManager::drainOutput() {
|
||||||
@@ -572,6 +615,7 @@ void XmrigManager::monitorProcess() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int poll_counter = 0;
|
int poll_counter = 0;
|
||||||
|
int pool_api_counter = 0;
|
||||||
while (!should_stop_) {
|
while (!should_stop_) {
|
||||||
drainOutput();
|
drainOutput();
|
||||||
|
|
||||||
@@ -605,6 +649,12 @@ void XmrigManager::monitorProcess() {
|
|||||||
fetchStatsHttp();
|
fetchStatsHttp();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Poll pool-side stats every ~30 seconds (300 * 100ms)
|
||||||
|
if (++pool_api_counter >= 300) {
|
||||||
|
pool_api_counter = 0;
|
||||||
|
fetchPoolApiStats();
|
||||||
|
}
|
||||||
|
|
||||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||||
}
|
}
|
||||||
drainOutput(); // Final drain
|
drainOutput(); // Final drain
|
||||||
@@ -711,5 +761,53 @@ void XmrigManager::fetchStatsHttp() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Pool-side stats (hashrate reported by the pool)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
void XmrigManager::fetchPoolApiStats() {
|
||||||
|
if (state_ != State::Running || pool_host_.empty()) return;
|
||||||
|
|
||||||
|
// Query the pool's public stats API
|
||||||
|
std::string url = "https://" + pool_host_ + "/api/stats";
|
||||||
|
std::string responseData;
|
||||||
|
|
||||||
|
CURL* curl = curl_easy_init();
|
||||||
|
if (!curl) return;
|
||||||
|
|
||||||
|
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
|
||||||
|
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curlWriteCb);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &responseData);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, 5000L);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, 3000L);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
|
||||||
|
|
||||||
|
CURLcode res = curl_easy_perform(curl);
|
||||||
|
curl_easy_cleanup(curl);
|
||||||
|
|
||||||
|
if (res != CURLE_OK) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
json resp = json::parse(responseData);
|
||||||
|
|
||||||
|
// Pool stats API format: { "pools": { "<name>": { "hashrate": ... } } }
|
||||||
|
double poolHR = 0;
|
||||||
|
if (resp.contains("pools") && resp["pools"].is_object()) {
|
||||||
|
for (auto& [key, pool] : resp["pools"].items()) {
|
||||||
|
if (pool.contains("hashrate") && pool["hashrate"].is_number()) {
|
||||||
|
poolHR = pool["hashrate"].get<double>();
|
||||||
|
break; // Use the first pool entry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> lk(stats_mutex_);
|
||||||
|
stats_.pool_hashrate = poolHR;
|
||||||
|
} catch (...) {
|
||||||
|
// Malformed response — ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace daemon
|
} // namespace daemon
|
||||||
} // namespace dragonx
|
} // namespace dragonx
|
||||||
|
|||||||
@@ -49,11 +49,13 @@ public:
|
|||||||
int64_t memory_total = 0; // bytes
|
int64_t memory_total = 0; // bytes
|
||||||
int64_t memory_used = 0; // bytes (resident set size)
|
int64_t memory_used = 0; // bytes (resident set size)
|
||||||
int threads_active = 0; // actual mining threads
|
int threads_active = 0; // actual mining threads
|
||||||
|
// Pool-side hashrate (from pool stats API, not xmrig)
|
||||||
|
double pool_hashrate = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
/// User-facing config (maps 1:1 to UI fields / Settings)
|
/// User-facing config (maps 1:1 to UI fields / Settings)
|
||||||
struct Config {
|
struct Config {
|
||||||
std::string pool_url = "pool.dragonx.is";
|
std::string pool_url = "pool.dragonx.is:3433";
|
||||||
std::string wallet_address;
|
std::string wallet_address;
|
||||||
std::string worker_name = "x";
|
std::string worker_name = "x";
|
||||||
std::string algo = "rx/hush";
|
std::string algo = "rx/hush";
|
||||||
@@ -86,6 +88,10 @@ public:
|
|||||||
const PoolStats& getStats() const { return stats_; }
|
const PoolStats& getStats() const { return stats_; }
|
||||||
const std::string& getLastError() const { return last_error_; }
|
const std::string& getLastError() const { return last_error_; }
|
||||||
|
|
||||||
|
/// Thread count requested at start() — available immediately, unlike
|
||||||
|
/// PoolStats::threads_active which requires an API response.
|
||||||
|
int getRequestedThreads() const { return threads_; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Get last N lines of xmrig stdout (thread-safe snapshot).
|
* @brief Get last N lines of xmrig stdout (thread-safe snapshot).
|
||||||
*/
|
*/
|
||||||
@@ -138,6 +144,7 @@ private:
|
|||||||
void drainOutput();
|
void drainOutput();
|
||||||
void appendOutput(const char* data, size_t len);
|
void appendOutput(const char* data, size_t len);
|
||||||
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
|
||||||
|
|
||||||
std::atomic<State> state_{State::Stopped};
|
std::atomic<State> state_{State::Stopped};
|
||||||
std::string last_error_;
|
std::string last_error_;
|
||||||
@@ -149,6 +156,7 @@ private:
|
|||||||
int api_port_ = 0;
|
int api_port_ = 0;
|
||||||
std::string api_token_;
|
std::string api_token_;
|
||||||
int threads_ = 0; // Thread count for mining
|
int threads_ = 0; // Thread count for mining
|
||||||
|
std::string pool_host_; // Pool hostname for stats API
|
||||||
PoolStats stats_;
|
PoolStats stats_;
|
||||||
mutable std::mutex stats_mutex_;
|
mutable std::mutex stats_mutex_;
|
||||||
|
|
||||||
|
|||||||
@@ -11,19 +11,10 @@ const std::vector<ExchangeInfo>& getExchangeRegistry()
|
|||||||
{
|
{
|
||||||
static const std::vector<ExchangeInfo> registry = {
|
static const std::vector<ExchangeInfo> registry = {
|
||||||
{
|
{
|
||||||
"TradeOgre",
|
"Nonkyc.io",
|
||||||
"https://tradeogre.com",
|
"https://nonkyc.io",
|
||||||
{
|
{
|
||||||
{"DRGX", "BTC", "DRGX/BTC", "https://tradeogre.com/exchange/DRGX-BTC"},
|
{"DRGX", "USDT", "DRGX/USDT", "https://nonkyc.io/market/DRGX_USDT"},
|
||||||
{"DRGX", "LTC", "DRGX/LTC", "https://tradeogre.com/exchange/DRGX-LTC"},
|
|
||||||
{"DRGX", "USDT", "DRGX/USDT", "https://tradeogre.com/exchange/DRGX-USDT"},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Exbitron",
|
|
||||||
"https://www.exbitron.com",
|
|
||||||
{
|
|
||||||
{"DRGX", "USDT", "DRGX/USDT", "https://www.exbitron.com/trading/drgxusdt"},
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
510
src/data/transaction_history_cache.cpp
Normal file
@@ -0,0 +1,510 @@
|
|||||||
|
#include "transaction_history_cache.h"
|
||||||
|
|
||||||
|
#include "../util/logger.h"
|
||||||
|
#include "../util/platform.h"
|
||||||
|
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
#include <sqlite3.h>
|
||||||
|
#include <sodium.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
using json = nlohmann::json;
|
||||||
|
|
||||||
|
namespace dragonx {
|
||||||
|
namespace data {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr int kSchemaVersion = 1;
|
||||||
|
constexpr std::size_t kKeyBytes = 32;
|
||||||
|
|
||||||
|
struct Statement {
|
||||||
|
sqlite3_stmt* handle = nullptr;
|
||||||
|
|
||||||
|
Statement(sqlite3* db, const char* sql)
|
||||||
|
{
|
||||||
|
if (sqlite3_prepare_v2(db, sql, -1, &handle, nullptr) != SQLITE_OK) {
|
||||||
|
handle = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
~Statement()
|
||||||
|
{
|
||||||
|
if (handle) sqlite3_finalize(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
Statement(const Statement&) = delete;
|
||||||
|
Statement& operator=(const Statement&) = delete;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool bindText(sqlite3_stmt* statement, int index, const std::string& value)
|
||||||
|
{
|
||||||
|
return sqlite3_bind_text(statement, index, value.c_str(), -1, SQLITE_TRANSIENT) == SQLITE_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool bindBlob(sqlite3_stmt* statement, int index, const std::vector<unsigned char>& value)
|
||||||
|
{
|
||||||
|
return sqlite3_bind_blob(statement, index, value.data(), static_cast<int>(value.size()), SQLITE_TRANSIENT) == SQLITE_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<unsigned char> readBlob(sqlite3_stmt* statement, int index)
|
||||||
|
{
|
||||||
|
const void* data = sqlite3_column_blob(statement, index);
|
||||||
|
int bytes = sqlite3_column_bytes(statement, index);
|
||||||
|
if (!data || bytes <= 0) return {};
|
||||||
|
const auto* begin = static_cast<const unsigned char*>(data);
|
||||||
|
return std::vector<unsigned char>(begin, begin + bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string hexEncode(const unsigned char* bytes, std::size_t length)
|
||||||
|
{
|
||||||
|
static constexpr char kHex[] = "0123456789abcdef";
|
||||||
|
std::string output;
|
||||||
|
output.resize(length * 2);
|
||||||
|
for (std::size_t index = 0; index < length; ++index) {
|
||||||
|
output[index * 2] = kHex[(bytes[index] >> 4) & 0x0F];
|
||||||
|
output[index * 2 + 1] = kHex[bytes[index] & 0x0F];
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
json transactionToJson(const TransactionInfo& transaction)
|
||||||
|
{
|
||||||
|
return json{
|
||||||
|
{"txid", transaction.txid},
|
||||||
|
{"type", transaction.type},
|
||||||
|
{"amount", transaction.amount},
|
||||||
|
{"timestamp", transaction.timestamp},
|
||||||
|
{"confirmations", transaction.confirmations},
|
||||||
|
{"address", transaction.address},
|
||||||
|
{"from_address", transaction.from_address},
|
||||||
|
{"memo", transaction.memo}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
TransactionInfo transactionFromJson(const json& source)
|
||||||
|
{
|
||||||
|
TransactionInfo transaction;
|
||||||
|
transaction.txid = source.value("txid", std::string());
|
||||||
|
transaction.type = source.value("type", std::string());
|
||||||
|
transaction.amount = source.value("amount", 0.0);
|
||||||
|
transaction.timestamp = source.value("timestamp", static_cast<std::int64_t>(0));
|
||||||
|
transaction.confirmations = source.value("confirmations", 0);
|
||||||
|
transaction.address = source.value("address", std::string());
|
||||||
|
transaction.from_address = source.value("from_address", std::string());
|
||||||
|
transaction.memo = source.value("memo", std::string());
|
||||||
|
return transaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string associatedDataForWallet(const std::string& walletHash)
|
||||||
|
{
|
||||||
|
return std::string("obsidian-dragon-tx-history-v1:") + walletHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TransactionHistoryCache::TransactionHistoryCache()
|
||||||
|
: TransactionHistoryCache(defaultDatabasePath())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
TransactionHistoryCache::TransactionHistoryCache(std::string databasePath)
|
||||||
|
: database_path_(std::move(databasePath))
|
||||||
|
{
|
||||||
|
if (sodium_init() < 0) {
|
||||||
|
DEBUG_LOGF("Failed to initialize libsodium for transaction history cache\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TransactionHistoryCache::~TransactionHistoryCache()
|
||||||
|
{
|
||||||
|
lockKey();
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string TransactionHistoryCache::defaultDatabasePath()
|
||||||
|
{
|
||||||
|
return (fs::path(util::Platform::getConfigDir()) / "transaction_history.sqlite").string();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string TransactionHistoryCache::walletIdentityFromAddresses(
|
||||||
|
const std::vector<std::string>& shieldedAddresses,
|
||||||
|
const std::vector<std::string>& transparentAddresses)
|
||||||
|
{
|
||||||
|
std::vector<std::string> addresses;
|
||||||
|
addresses.reserve(shieldedAddresses.size() + transparentAddresses.size());
|
||||||
|
for (const auto& address : shieldedAddresses) {
|
||||||
|
if (!address.empty()) addresses.push_back("z:" + address);
|
||||||
|
}
|
||||||
|
for (const auto& address : transparentAddresses) {
|
||||||
|
if (!address.empty()) addresses.push_back("t:" + address);
|
||||||
|
}
|
||||||
|
if (addresses.empty()) return {};
|
||||||
|
|
||||||
|
std::sort(addresses.begin(), addresses.end());
|
||||||
|
std::string identity = "wallet-addresses-v1\n";
|
||||||
|
for (const auto& address : addresses) {
|
||||||
|
identity += address;
|
||||||
|
identity += '\n';
|
||||||
|
}
|
||||||
|
return identity;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string TransactionHistoryCache::walletIdentityHash(const std::string& walletIdentity)
|
||||||
|
{
|
||||||
|
unsigned char digest[crypto_generichash_BYTES];
|
||||||
|
crypto_generichash(digest, sizeof(digest),
|
||||||
|
reinterpret_cast<const unsigned char*>(walletIdentity.data()),
|
||||||
|
walletIdentity.size(), nullptr, 0);
|
||||||
|
return hexEncode(digest, sizeof(digest));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TransactionHistoryCache::ensureOpen()
|
||||||
|
{
|
||||||
|
if (db_) return true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs::path path(database_path_);
|
||||||
|
if (!path.parent_path().empty()) fs::create_directories(path.parent_path());
|
||||||
|
} catch (const std::exception& exception) {
|
||||||
|
DEBUG_LOGF("Failed to create transaction history cache directory: %s\n", exception.what());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlite3* openedDb = nullptr;
|
||||||
|
if (sqlite3_open(database_path_.c_str(), &openedDb) != SQLITE_OK) {
|
||||||
|
DEBUG_LOGF("Failed to open transaction history cache: %s\n",
|
||||||
|
openedDb ? sqlite3_errmsg(openedDb) : "unknown error");
|
||||||
|
if (openedDb) sqlite3_close(openedDb);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
db_ = openedDb;
|
||||||
|
sqlite3_busy_timeout(db_, 2000);
|
||||||
|
exec("PRAGMA journal_mode=WAL");
|
||||||
|
exec("PRAGMA synchronous=NORMAL");
|
||||||
|
|
||||||
|
if (!createSchema()) {
|
||||||
|
close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TransactionHistoryCache::unlockWithPassphrase(const std::string& walletIdentity,
|
||||||
|
const std::string& passphrase)
|
||||||
|
{
|
||||||
|
if (walletIdentity.empty() || passphrase.empty() || !ensureOpen()) return false;
|
||||||
|
|
||||||
|
std::string walletHash = walletIdentityHash(walletIdentity);
|
||||||
|
std::vector<unsigned char> salt = getOrCreateSalt(walletHash);
|
||||||
|
if (salt.empty()) return false;
|
||||||
|
|
||||||
|
if (!deriveKey(passphrase, salt)) return false;
|
||||||
|
unlocked_wallet_hash_ = std::move(walletHash);
|
||||||
|
key_ready_ = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TransactionHistoryCache::lockKey()
|
||||||
|
{
|
||||||
|
if (key_ready_) sodium_memzero(key_.data(), key_.size());
|
||||||
|
key_ready_ = false;
|
||||||
|
unlocked_wallet_hash_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TransactionHistoryCache::isUnlockedFor(const std::string& walletIdentity) const
|
||||||
|
{
|
||||||
|
return key_ready_ && !walletIdentity.empty() &&
|
||||||
|
unlocked_wallet_hash_ == walletIdentityHash(walletIdentity);
|
||||||
|
}
|
||||||
|
|
||||||
|
TransactionHistoryCache::LoadResult TransactionHistoryCache::load(
|
||||||
|
const std::string& walletIdentity,
|
||||||
|
int currentTipHeight,
|
||||||
|
const std::string& currentTipHash)
|
||||||
|
{
|
||||||
|
LoadResult result;
|
||||||
|
if (!isUnlockedFor(walletIdentity) || !ensureOpen()) return result;
|
||||||
|
|
||||||
|
std::string walletHash = walletIdentityHash(walletIdentity);
|
||||||
|
int tipHeight = 0;
|
||||||
|
std::string tipHash;
|
||||||
|
std::time_t updatedAt = 0;
|
||||||
|
std::vector<unsigned char> nonce;
|
||||||
|
std::vector<unsigned char> cipherText;
|
||||||
|
if (!readSnapshot(walletHash, tipHeight, tipHash, updatedAt, nonce, cipherText)) return result;
|
||||||
|
|
||||||
|
if ((currentTipHeight > 0 && tipHeight > currentTipHeight) ||
|
||||||
|
(currentTipHeight > 0 && tipHeight == currentTipHeight &&
|
||||||
|
!currentTipHash.empty() && !tipHash.empty() && tipHash != currentTipHash)) {
|
||||||
|
clearWalletByHash(walletHash);
|
||||||
|
result.invalidated = true;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string plainText;
|
||||||
|
if (!decryptPayload(walletHash, nonce, cipherText, plainText)) return result;
|
||||||
|
|
||||||
|
try {
|
||||||
|
json payload = json::parse(plainText);
|
||||||
|
if (payload.value("schema_version", 0) != kSchemaVersion) return result;
|
||||||
|
if (payload.value("wallet_hash", std::string()) != walletHash) return result;
|
||||||
|
if (!payload.contains("transactions") || !payload["transactions"].is_array()) return result;
|
||||||
|
|
||||||
|
result.transactions.reserve(payload["transactions"].size());
|
||||||
|
for (const auto& transactionJson : payload["transactions"]) {
|
||||||
|
if (transactionJson.is_object()) {
|
||||||
|
result.transactions.push_back(transactionFromJson(transactionJson));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (payload.contains("shielded_scan_heights") && payload["shielded_scan_heights"].is_object()) {
|
||||||
|
for (auto it = payload["shielded_scan_heights"].begin();
|
||||||
|
it != payload["shielded_scan_heights"].end(); ++it) {
|
||||||
|
if (!it.key().empty() && it.value().is_number_integer()) {
|
||||||
|
result.shieldedScanHeights[it.key()] = it.value().get<int>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.tipHeight = tipHeight;
|
||||||
|
result.tipHash = tipHash;
|
||||||
|
result.updatedAt = updatedAt;
|
||||||
|
result.loaded = true;
|
||||||
|
} catch (...) {
|
||||||
|
result.transactions.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
sodium_memzero(plainText.data(), plainText.size());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TransactionHistoryCache::replace(const std::string& walletIdentity,
|
||||||
|
int tipHeight,
|
||||||
|
const std::string& tipHash,
|
||||||
|
const std::vector<TransactionInfo>& transactions,
|
||||||
|
std::time_t updatedAt,
|
||||||
|
const std::unordered_map<std::string, int>& shieldedScanHeights)
|
||||||
|
{
|
||||||
|
if (!isUnlockedFor(walletIdentity) || !ensureOpen()) return false;
|
||||||
|
|
||||||
|
std::string walletHash = walletIdentityHash(walletIdentity);
|
||||||
|
json payload;
|
||||||
|
payload["schema_version"] = kSchemaVersion;
|
||||||
|
payload["wallet_hash"] = walletHash;
|
||||||
|
payload["tip_height"] = tipHeight;
|
||||||
|
payload["tip_hash"] = tipHash;
|
||||||
|
payload["updated_at"] = static_cast<std::int64_t>(updatedAt);
|
||||||
|
payload["transactions"] = json::array();
|
||||||
|
for (const auto& transaction : transactions) {
|
||||||
|
payload["transactions"].push_back(transactionToJson(transaction));
|
||||||
|
}
|
||||||
|
payload["shielded_scan_heights"] = json::object();
|
||||||
|
for (const auto& [address, height] : shieldedScanHeights) {
|
||||||
|
if (!address.empty() && height >= 0) {
|
||||||
|
payload["shielded_scan_heights"][address] = height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string plainText = payload.dump();
|
||||||
|
std::vector<unsigned char> nonce;
|
||||||
|
std::vector<unsigned char> cipherText;
|
||||||
|
bool encrypted = encryptPayload(walletHash, plainText, nonce, cipherText);
|
||||||
|
sodium_memzero(plainText.data(), plainText.size());
|
||||||
|
if (!encrypted) return false;
|
||||||
|
|
||||||
|
Statement statement(db_,
|
||||||
|
"INSERT OR REPLACE INTO transaction_history_snapshots "
|
||||||
|
"(wallet_hash, schema_version, tip_height, tip_hash, updated_at, nonce, ciphertext) "
|
||||||
|
"VALUES (?, ?, ?, ?, ?, ?, ?)");
|
||||||
|
if (!statement.handle) return false;
|
||||||
|
|
||||||
|
if (!bindText(statement.handle, 1, walletHash)) return false;
|
||||||
|
sqlite3_bind_int(statement.handle, 2, kSchemaVersion);
|
||||||
|
sqlite3_bind_int(statement.handle, 3, std::max(0, tipHeight));
|
||||||
|
if (!bindText(statement.handle, 4, tipHash)) return false;
|
||||||
|
sqlite3_bind_int64(statement.handle, 5, static_cast<sqlite3_int64>(updatedAt));
|
||||||
|
if (!bindBlob(statement.handle, 6, nonce)) return false;
|
||||||
|
if (!bindBlob(statement.handle, 7, cipherText)) return false;
|
||||||
|
|
||||||
|
return sqlite3_step(statement.handle) == SQLITE_DONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TransactionHistoryCache::clearWallet(const std::string& walletIdentity)
|
||||||
|
{
|
||||||
|
if (walletIdentity.empty()) return;
|
||||||
|
clearWalletByHash(walletIdentityHash(walletIdentity));
|
||||||
|
}
|
||||||
|
|
||||||
|
int TransactionHistoryCache::snapshotCount()
|
||||||
|
{
|
||||||
|
if (!ensureOpen()) return 0;
|
||||||
|
Statement statement(db_, "SELECT COUNT(*) FROM transaction_history_snapshots");
|
||||||
|
if (!statement.handle || sqlite3_step(statement.handle) != SQLITE_ROW) return 0;
|
||||||
|
return sqlite3_column_int(statement.handle, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TransactionHistoryCache::exec(const char* sql)
|
||||||
|
{
|
||||||
|
if (!db_) return false;
|
||||||
|
char* error = nullptr;
|
||||||
|
int result = sqlite3_exec(db_, sql, nullptr, nullptr, &error);
|
||||||
|
if (result != SQLITE_OK) {
|
||||||
|
DEBUG_LOGF("Transaction history cache SQL error: %s\n", error ? error : sqlite3_errmsg(db_));
|
||||||
|
if (error) sqlite3_free(error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TransactionHistoryCache::createSchema()
|
||||||
|
{
|
||||||
|
return exec("CREATE TABLE IF NOT EXISTS transaction_history_keys ("
|
||||||
|
"wallet_hash TEXT PRIMARY KEY,"
|
||||||
|
"salt BLOB NOT NULL)") &&
|
||||||
|
exec("CREATE TABLE IF NOT EXISTS transaction_history_snapshots ("
|
||||||
|
"wallet_hash TEXT PRIMARY KEY,"
|
||||||
|
"schema_version INTEGER NOT NULL,"
|
||||||
|
"tip_height INTEGER NOT NULL,"
|
||||||
|
"tip_hash TEXT NOT NULL,"
|
||||||
|
"updated_at INTEGER NOT NULL,"
|
||||||
|
"nonce BLOB NOT NULL,"
|
||||||
|
"ciphertext BLOB NOT NULL)");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<unsigned char> TransactionHistoryCache::getOrCreateSalt(const std::string& walletHash)
|
||||||
|
{
|
||||||
|
if (!ensureOpen()) return {};
|
||||||
|
|
||||||
|
{
|
||||||
|
Statement statement(db_, "SELECT salt FROM transaction_history_keys WHERE wallet_hash = ?");
|
||||||
|
if (!statement.handle) return {};
|
||||||
|
if (!bindText(statement.handle, 1, walletHash)) return {};
|
||||||
|
if (sqlite3_step(statement.handle) == SQLITE_ROW) {
|
||||||
|
auto salt = readBlob(statement.handle, 0);
|
||||||
|
if (salt.size() == crypto_pwhash_SALTBYTES) return salt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<unsigned char> salt(crypto_pwhash_SALTBYTES);
|
||||||
|
randombytes_buf(salt.data(), salt.size());
|
||||||
|
|
||||||
|
Statement insert(db_,
|
||||||
|
"INSERT OR REPLACE INTO transaction_history_keys (wallet_hash, salt) VALUES (?, ?)");
|
||||||
|
if (!insert.handle) return {};
|
||||||
|
if (!bindText(insert.handle, 1, walletHash)) return {};
|
||||||
|
if (!bindBlob(insert.handle, 2, salt)) return {};
|
||||||
|
if (sqlite3_step(insert.handle) != SQLITE_DONE) return {};
|
||||||
|
return salt;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TransactionHistoryCache::deriveKey(const std::string& passphrase,
|
||||||
|
const std::vector<unsigned char>& salt)
|
||||||
|
{
|
||||||
|
if (salt.size() != crypto_pwhash_SALTBYTES) return false;
|
||||||
|
unsigned char derived[kKeyBytes];
|
||||||
|
int result = crypto_pwhash(derived, sizeof(derived),
|
||||||
|
passphrase.c_str(), passphrase.size(),
|
||||||
|
salt.data(),
|
||||||
|
crypto_pwhash_OPSLIMIT_INTERACTIVE,
|
||||||
|
crypto_pwhash_MEMLIMIT_INTERACTIVE,
|
||||||
|
crypto_pwhash_ALG_ARGON2ID13);
|
||||||
|
if (result != 0) return false;
|
||||||
|
std::copy(derived, derived + sizeof(derived), key_.begin());
|
||||||
|
sodium_memzero(derived, sizeof(derived));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TransactionHistoryCache::encryptPayload(const std::string& walletHash,
|
||||||
|
const std::string& plainText,
|
||||||
|
std::vector<unsigned char>& nonce,
|
||||||
|
std::vector<unsigned char>& cipherText) const
|
||||||
|
{
|
||||||
|
if (!key_ready_) return false;
|
||||||
|
nonce.resize(crypto_aead_xchacha20poly1305_ietf_NPUBBYTES);
|
||||||
|
randombytes_buf(nonce.data(), nonce.size());
|
||||||
|
|
||||||
|
std::string associatedData = associatedDataForWallet(walletHash);
|
||||||
|
cipherText.resize(plainText.size() + crypto_aead_xchacha20poly1305_ietf_ABYTES);
|
||||||
|
unsigned long long cipherLength = 0;
|
||||||
|
int result = crypto_aead_xchacha20poly1305_ietf_encrypt(
|
||||||
|
cipherText.data(), &cipherLength,
|
||||||
|
reinterpret_cast<const unsigned char*>(plainText.data()), plainText.size(),
|
||||||
|
reinterpret_cast<const unsigned char*>(associatedData.data()), associatedData.size(),
|
||||||
|
nullptr, nonce.data(), key_.data());
|
||||||
|
if (result != 0) return false;
|
||||||
|
cipherText.resize(static_cast<std::size_t>(cipherLength));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TransactionHistoryCache::decryptPayload(const std::string& walletHash,
|
||||||
|
const std::vector<unsigned char>& nonce,
|
||||||
|
const std::vector<unsigned char>& cipherText,
|
||||||
|
std::string& plainText) const
|
||||||
|
{
|
||||||
|
if (!key_ready_ || nonce.size() != crypto_aead_xchacha20poly1305_ietf_NPUBBYTES ||
|
||||||
|
cipherText.size() < crypto_aead_xchacha20poly1305_ietf_ABYTES) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string associatedData = associatedDataForWallet(walletHash);
|
||||||
|
std::vector<unsigned char> plain(cipherText.size() - crypto_aead_xchacha20poly1305_ietf_ABYTES);
|
||||||
|
unsigned long long plainLength = 0;
|
||||||
|
int result = crypto_aead_xchacha20poly1305_ietf_decrypt(
|
||||||
|
plain.data(), &plainLength, nullptr,
|
||||||
|
cipherText.data(), cipherText.size(),
|
||||||
|
reinterpret_cast<const unsigned char*>(associatedData.data()), associatedData.size(),
|
||||||
|
nonce.data(), key_.data());
|
||||||
|
if (result != 0) return false;
|
||||||
|
|
||||||
|
plainText.assign(reinterpret_cast<const char*>(plain.data()), static_cast<std::size_t>(plainLength));
|
||||||
|
sodium_memzero(plain.data(), plain.size());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TransactionHistoryCache::readSnapshot(const std::string& walletHash,
|
||||||
|
int& tipHeight,
|
||||||
|
std::string& tipHash,
|
||||||
|
std::time_t& updatedAt,
|
||||||
|
std::vector<unsigned char>& nonce,
|
||||||
|
std::vector<unsigned char>& cipherText)
|
||||||
|
{
|
||||||
|
Statement statement(db_,
|
||||||
|
"SELECT tip_height, tip_hash, updated_at, nonce, ciphertext "
|
||||||
|
"FROM transaction_history_snapshots WHERE wallet_hash = ?");
|
||||||
|
if (!statement.handle) return false;
|
||||||
|
if (!bindText(statement.handle, 1, walletHash)) return false;
|
||||||
|
if (sqlite3_step(statement.handle) != SQLITE_ROW) return false;
|
||||||
|
|
||||||
|
tipHeight = sqlite3_column_int(statement.handle, 0);
|
||||||
|
const unsigned char* tipHashText = sqlite3_column_text(statement.handle, 1);
|
||||||
|
tipHash = tipHashText ? reinterpret_cast<const char*>(tipHashText) : std::string();
|
||||||
|
updatedAt = static_cast<std::time_t>(sqlite3_column_int64(statement.handle, 2));
|
||||||
|
nonce = readBlob(statement.handle, 3);
|
||||||
|
cipherText = readBlob(statement.handle, 4);
|
||||||
|
return !nonce.empty() && !cipherText.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TransactionHistoryCache::clearWalletByHash(const std::string& walletHash)
|
||||||
|
{
|
||||||
|
if (!ensureOpen()) return;
|
||||||
|
Statement statement(db_, "DELETE FROM transaction_history_snapshots WHERE wallet_hash = ?");
|
||||||
|
if (!statement.handle) return;
|
||||||
|
if (!bindText(statement.handle, 1, walletHash)) return;
|
||||||
|
sqlite3_step(statement.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TransactionHistoryCache::close()
|
||||||
|
{
|
||||||
|
if (!db_) return;
|
||||||
|
sqlite3_close(db_);
|
||||||
|
db_ = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace data
|
||||||
|
} // namespace dragonx
|
||||||
90
src/data/transaction_history_cache.h
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "wallet_state.h"
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <ctime>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
struct sqlite3;
|
||||||
|
|
||||||
|
namespace dragonx {
|
||||||
|
namespace data {
|
||||||
|
|
||||||
|
class TransactionHistoryCache {
|
||||||
|
public:
|
||||||
|
struct LoadResult {
|
||||||
|
bool loaded = false;
|
||||||
|
bool invalidated = false;
|
||||||
|
int tipHeight = 0;
|
||||||
|
std::string tipHash;
|
||||||
|
std::time_t updatedAt = 0;
|
||||||
|
std::vector<TransactionInfo> transactions;
|
||||||
|
std::unordered_map<std::string, int> shieldedScanHeights;
|
||||||
|
};
|
||||||
|
|
||||||
|
TransactionHistoryCache();
|
||||||
|
explicit TransactionHistoryCache(std::string databasePath);
|
||||||
|
~TransactionHistoryCache();
|
||||||
|
|
||||||
|
TransactionHistoryCache(const TransactionHistoryCache&) = delete;
|
||||||
|
TransactionHistoryCache& operator=(const TransactionHistoryCache&) = delete;
|
||||||
|
|
||||||
|
static std::string defaultDatabasePath();
|
||||||
|
static std::string walletIdentityFromAddresses(const std::vector<std::string>& shieldedAddresses,
|
||||||
|
const std::vector<std::string>& transparentAddresses);
|
||||||
|
static std::string walletIdentityHash(const std::string& walletIdentity);
|
||||||
|
|
||||||
|
bool ensureOpen();
|
||||||
|
bool unlockWithPassphrase(const std::string& walletIdentity, const std::string& passphrase);
|
||||||
|
void lockKey();
|
||||||
|
bool hasKey() const { return key_ready_; }
|
||||||
|
bool isUnlockedFor(const std::string& walletIdentity) const;
|
||||||
|
|
||||||
|
LoadResult load(const std::string& walletIdentity,
|
||||||
|
int currentTipHeight,
|
||||||
|
const std::string& currentTipHash);
|
||||||
|
bool replace(const std::string& walletIdentity,
|
||||||
|
int tipHeight,
|
||||||
|
const std::string& tipHash,
|
||||||
|
const std::vector<TransactionInfo>& transactions,
|
||||||
|
std::time_t updatedAt,
|
||||||
|
const std::unordered_map<std::string, int>& shieldedScanHeights = {});
|
||||||
|
void clearWallet(const std::string& walletIdentity);
|
||||||
|
int snapshotCount();
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool exec(const char* sql);
|
||||||
|
bool createSchema();
|
||||||
|
std::vector<unsigned char> getOrCreateSalt(const std::string& walletHash);
|
||||||
|
bool deriveKey(const std::string& passphrase,
|
||||||
|
const std::vector<unsigned char>& salt);
|
||||||
|
bool encryptPayload(const std::string& walletHash,
|
||||||
|
const std::string& plainText,
|
||||||
|
std::vector<unsigned char>& nonce,
|
||||||
|
std::vector<unsigned char>& cipherText) const;
|
||||||
|
bool decryptPayload(const std::string& walletHash,
|
||||||
|
const std::vector<unsigned char>& nonce,
|
||||||
|
const std::vector<unsigned char>& cipherText,
|
||||||
|
std::string& plainText) const;
|
||||||
|
bool readSnapshot(const std::string& walletHash,
|
||||||
|
int& tipHeight,
|
||||||
|
std::string& tipHash,
|
||||||
|
std::time_t& updatedAt,
|
||||||
|
std::vector<unsigned char>& nonce,
|
||||||
|
std::vector<unsigned char>& cipherText);
|
||||||
|
void clearWalletByHash(const std::string& walletHash);
|
||||||
|
void close();
|
||||||
|
|
||||||
|
sqlite3* db_ = nullptr;
|
||||||
|
std::string database_path_;
|
||||||
|
std::array<unsigned char, 32> key_{};
|
||||||
|
bool key_ready_ = false;
|
||||||
|
std::string unlocked_wallet_hash_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace data
|
||||||
|
} // namespace dragonx
|
||||||
@@ -3,12 +3,44 @@
|
|||||||
// Released under the GPLv3
|
// Released under the GPLv3
|
||||||
|
|
||||||
#include "wallet_state.h"
|
#include "wallet_state.h"
|
||||||
|
#include <algorithm>
|
||||||
#include <ctime>
|
#include <ctime>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <iomanip>
|
#include <iomanip>
|
||||||
|
|
||||||
namespace dragonx {
|
namespace dragonx {
|
||||||
|
|
||||||
|
std::vector<size_t> sortedSpendableAddressIndices(const std::vector<AddressInfo>& addresses,
|
||||||
|
bool requirePositiveBalance)
|
||||||
|
{
|
||||||
|
std::vector<size_t> indices;
|
||||||
|
indices.reserve(addresses.size());
|
||||||
|
for (size_t i = 0; i < addresses.size(); ++i) {
|
||||||
|
const auto& address = addresses[i];
|
||||||
|
if (!address.isSpendable()) continue;
|
||||||
|
if (requirePositiveBalance && address.balance <= 0.0) continue;
|
||||||
|
indices.push_back(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::sort(indices.begin(), indices.end(), [&](size_t lhs, size_t rhs) {
|
||||||
|
return addresses[lhs].balance > addresses[rhs].balance;
|
||||||
|
});
|
||||||
|
return indices;
|
||||||
|
}
|
||||||
|
|
||||||
|
int bestSpendableAddressIndex(const std::vector<AddressInfo>& addresses)
|
||||||
|
{
|
||||||
|
int bestIndex = -1;
|
||||||
|
double bestBalance = 0.0;
|
||||||
|
for (size_t i = 0; i < addresses.size(); ++i) {
|
||||||
|
if (addresses[i].isSpendable() && addresses[i].balance > bestBalance) {
|
||||||
|
bestBalance = addresses[i].balance;
|
||||||
|
bestIndex = static_cast<int>(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestIndex;
|
||||||
|
}
|
||||||
|
|
||||||
std::string TransactionInfo::getTimeString() const
|
std::string TransactionInfo::getTimeString() const
|
||||||
{
|
{
|
||||||
if (timestamp == 0) return "Unknown";
|
if (timestamp == 0) return "Unknown";
|
||||||
|
|||||||