Compare commits
113
Commits
a792a1ec0c
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
abbe5d8412 | ||
|
|
00d298453e | ||
|
|
38461f084c | ||
|
|
16d60a72f2 | ||
|
|
a5cd211e89 | ||
|
|
0ebd1ac5d5 | ||
|
|
94d41e2932 | ||
|
|
4cd27dcd4c | ||
|
|
4dfdf8a36f | ||
|
|
1a01de9791 | ||
|
|
3b828e8a49 | ||
|
|
7138f633a8 | ||
|
|
fc9f8d52c3 | ||
|
|
d83169c963 | ||
|
|
9335e2bffb | ||
|
|
112752e8af | ||
|
|
045289115d | ||
|
|
64294d1518 | ||
|
|
54f8359bd4 | ||
|
|
dba97c8b35 | ||
|
|
acb6446076 | ||
|
|
e61772a825 | ||
|
|
5e33bf2c41 | ||
|
|
cc08854237 | ||
|
|
1729f7dfad | ||
|
|
12c61bd796 | ||
|
|
63c284684c | ||
|
|
2d63a72098 | ||
|
|
743e7f8b65 | ||
|
|
6b93a8ab60 | ||
|
|
19e9b62632 | ||
|
|
076bb3ff06 | ||
|
|
f0be72ac36 | ||
|
|
6e285bd4a4 | ||
|
|
fe3d2aa935 | ||
|
|
6b9bda3ee4 | ||
|
|
84ca1a5cbe | ||
|
|
669b8b7ca5 | ||
|
|
796d639ba3 | ||
|
|
daae693d1b | ||
|
|
2f1b666c20 | ||
|
|
5aa5fa7594 | ||
|
|
f56c94bc91 | ||
|
|
7320a98505 | ||
|
|
529286777f | ||
|
|
a3b4f534d5 | ||
|
|
293b633a24 | ||
|
|
ae4012adb7 | ||
|
|
6c2a277751 | ||
|
|
a558a2a271 | ||
|
|
3ee90993ed | ||
|
|
b404bb2aff | ||
|
|
3a33e9cae9 | ||
|
|
46de4cbfee | ||
|
|
d7fee801d3 | ||
|
|
4f4c6b7a16 | ||
|
|
5563becad2 | ||
|
|
6301cd5984 | ||
|
|
9b6971095c | ||
|
|
8a6d523062 | ||
|
|
16a1ca02e8 | ||
|
|
0cb1df3bfe | ||
|
|
6f369376ec | ||
|
|
02fa1a228a | ||
|
|
099d5df3e1 | ||
|
|
df98f98669 | ||
|
|
6d58802127 | ||
|
|
165675b131 | ||
|
|
08a4427e31 | ||
|
|
f4460beddd | ||
|
|
70d5c233c0 | ||
|
|
c0d10e9664 | ||
|
|
d16bbf3695 | ||
|
|
83ce73f26b | ||
|
|
31e9c2ee2c | ||
|
|
6b0f2eda22 | ||
|
|
dc1617a2c1 | ||
|
|
f213878a7c | ||
|
|
8d8e523a26 | ||
|
|
8b07f776e5 | ||
|
|
8b46ead72a | ||
|
|
8961cbdfc9 | ||
|
|
794150b11b | ||
|
|
2e74a3ca69 | ||
|
|
e94d88e723 | ||
|
|
097b2ea014 | ||
|
|
a43ac0156f | ||
|
|
534120d3de | ||
|
|
a90aa29478 | ||
|
|
a30a083df0 | ||
|
|
89d3975eb3 | ||
|
|
3f1838cfdd | ||
|
|
dcb8e6eb25 | ||
|
|
3f127f9b62 | ||
|
|
f2d9590058 | ||
|
|
ee6e99965c | ||
|
|
b3e09a3314 | ||
|
|
2fdf33f99b | ||
|
|
092a802959 | ||
|
|
e9eeaf00c3 | ||
|
|
572b98cbcf | ||
|
|
bcfc324426 | ||
|
|
7cea9b7339 | ||
|
|
f3f0b32a48 | ||
|
|
8fe20f6385 | ||
|
|
15c1181aef | ||
|
|
0101cc707c | ||
|
|
adf61e43d6 | ||
|
|
fb281714f9 | ||
|
|
74c497922e | ||
|
|
ae16c06d24 | ||
|
|
408393a547 | ||
|
|
a80be443d1 |
+25
-4
@@ -15,7 +15,7 @@ if(APPLE)
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
project(ObsidianDragon
|
project(ObsidianDragon
|
||||||
VERSION 2.0.2
|
VERSION 2.1.0
|
||||||
LANGUAGES C CXX
|
LANGUAGES C CXX
|
||||||
DESCRIPTION "DragonX Cryptocurrency Wallet"
|
DESCRIPTION "DragonX Cryptocurrency Wallet"
|
||||||
)
|
)
|
||||||
@@ -26,7 +26,7 @@ set(DRAGONX_VERSION_SUFFIX "")
|
|||||||
# ObsidianDragonLite is versioned INDEPENDENTLY of the full-node app above. The active variant's
|
# ObsidianDragonLite is versioned INDEPENDENTLY of the full-node app above. The active variant's
|
||||||
# version flows to the generated header, the Windows .rc/manifest, and build.sh's release names via
|
# version flows to the generated header, the Windows .rc/manifest, and build.sh's release names via
|
||||||
# DRAGONX_APP_VERSION* (resolved in the lite/full block below).
|
# DRAGONX_APP_VERSION* (resolved in the lite/full block below).
|
||||||
set(DRAGONX_LITE_VERSION "1.1.1")
|
set(DRAGONX_LITE_VERSION "2.0.0")
|
||||||
set(DRAGONX_LITE_VERSION_SUFFIX "")
|
set(DRAGONX_LITE_VERSION_SUFFIX "")
|
||||||
|
|
||||||
# C++17 standard
|
# C++17 standard
|
||||||
@@ -499,6 +499,7 @@ set(APP_SOURCES
|
|||||||
src/chat/chat_service.cpp
|
src/chat/chat_service.cpp
|
||||||
src/chat/chat_database.cpp
|
src/chat/chat_database.cpp
|
||||||
src/chat/chat_outgoing.cpp
|
src/chat/chat_outgoing.cpp
|
||||||
|
src/chat/chat_sensitive_vault.cpp
|
||||||
src/wallet/lite_owned_string.cpp
|
src/wallet/lite_owned_string.cpp
|
||||||
src/wallet/lite_rollout_policy.cpp
|
src/wallet/lite_rollout_policy.cpp
|
||||||
src/wallet/lite_client_bridge.cpp
|
src/wallet/lite_client_bridge.cpp
|
||||||
@@ -553,7 +554,6 @@ set(APP_SOURCES
|
|||||||
src/ui/windows/console_selection_controller.cpp
|
src/ui/windows/console_selection_controller.cpp
|
||||||
src/ui/windows/console_tab_helpers.cpp
|
src/ui/windows/console_tab_helpers.cpp
|
||||||
src/ui/windows/console_text_layout.cpp
|
src/ui/windows/console_text_layout.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
|
||||||
src/ui/windows/faq_dialog.cpp
|
src/ui/windows/faq_dialog.cpp
|
||||||
@@ -670,6 +670,7 @@ set(APP_HEADERS
|
|||||||
src/chat/chat_service.h
|
src/chat/chat_service.h
|
||||||
src/chat/chat_database.h
|
src/chat/chat_database.h
|
||||||
src/chat/chat_outgoing.h
|
src/chat/chat_outgoing.h
|
||||||
|
src/chat/chat_sensitive_vault.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/data/transaction_history_cache.h
|
||||||
@@ -704,7 +705,6 @@ set(APP_HEADERS
|
|||||||
src/ui/windows/console_selection_controller.h
|
src/ui/windows/console_selection_controller.h
|
||||||
src/ui/windows/console_tab.h
|
src/ui/windows/console_tab.h
|
||||||
src/ui/windows/console_tab_helpers.h
|
src/ui/windows/console_tab_helpers.h
|
||||||
src/ui/windows/settings_window.h
|
|
||||||
src/ui/windows/about_dialog.h
|
src/ui/windows/about_dialog.h
|
||||||
src/ui/windows/faq_dialog.h
|
src/ui/windows/faq_dialog.h
|
||||||
src/ui/windows/faq_content.h
|
src/ui/windows/faq_content.h
|
||||||
@@ -995,6 +995,26 @@ target_compile_definitions(HushChatFixtureCheck PRIVATE
|
|||||||
DRAGONX_ENABLE_CHAT=0
|
DRAGONX_ENABLE_CHAT=0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Regtest chat smoke driver (dev tool; never shipped). Links the real chat library so a runner
|
||||||
|
# script can verify a chunked message + Control memo round-trip through a live regtest dragonxd.
|
||||||
|
# cmake --build build/linux --target chat_regtest_smoke
|
||||||
|
add_executable(chat_regtest_smoke EXCLUDE_FROM_ALL
|
||||||
|
tools/chat_regtest_smoke.cpp
|
||||||
|
src/chat/chat_crypto.cpp
|
||||||
|
src/chat/chat_identity.cpp
|
||||||
|
src/chat/chat_outgoing.cpp
|
||||||
|
src/chat/chat_protocol.cpp
|
||||||
|
)
|
||||||
|
target_include_directories(chat_regtest_smoke PRIVATE
|
||||||
|
${CMAKE_SOURCE_DIR}/src
|
||||||
|
${SODIUM_INCLUDE_DIR}
|
||||||
|
)
|
||||||
|
target_link_libraries(chat_regtest_smoke PRIVATE
|
||||||
|
nlohmann_json::nlohmann_json
|
||||||
|
${SODIUM_LIBRARY}
|
||||||
|
)
|
||||||
|
target_compile_definitions(chat_regtest_smoke PRIVATE DRAGONX_ENABLE_CHAT=1)
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Copy resources
|
# Copy resources
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -1226,6 +1246,7 @@ if(BUILD_TESTING)
|
|||||||
src/chat/chat_service.cpp
|
src/chat/chat_service.cpp
|
||||||
src/chat/chat_database.cpp
|
src/chat/chat_database.cpp
|
||||||
src/chat/chat_outgoing.cpp
|
src/chat/chat_outgoing.cpp
|
||||||
|
src/chat/chat_sensitive_vault.cpp
|
||||||
src/wallet/lite_owned_string.cpp
|
src/wallet/lite_owned_string.cpp
|
||||||
src/wallet/lite_rollout_policy.cpp
|
src/wallet/lite_rollout_policy.cpp
|
||||||
src/wallet/lite_client_bridge.cpp
|
src/wallet/lite_client_bridge.cpp
|
||||||
|
|||||||
Binary file not shown.
+66
-3
@@ -141,6 +141,7 @@
|
|||||||
"appx_wallet_op_running": "Ein Schlüsselimport oder Neuscan läuft noch. Versuchen Sie es erneut, wenn er abgeschlossen ist.",
|
"appx_wallet_op_running": "Ein Schlüsselimport oder Neuscan läuft noch. Versuchen Sie es erneut, wenn er abgeschlossen ist.",
|
||||||
"appx_wallet_op_shutting_down": "Die Wallet wird beendet.",
|
"appx_wallet_op_shutting_down": "Die Wallet wird beendet.",
|
||||||
"appx_wallet_open_failed_prefix": "Öffnen des Wallets fehlgeschlagen: ",
|
"appx_wallet_open_failed_prefix": "Öffnen des Wallets fehlgeschlagen: ",
|
||||||
|
"auto_consolidate": "Notes automatisch konsolidieren",
|
||||||
"auto_shield": "Mining automatisch abschirmen",
|
"auto_shield": "Mining automatisch abschirmen",
|
||||||
"autoshield_by_node": "Ihr Node schützt für Sie gemäß Coinbase automatisch.",
|
"autoshield_by_node": "Ihr Node schützt für Sie gemäß Coinbase automatisch.",
|
||||||
"autoshield_off_backup_seed": "Geschürfte Coinbase-Coins werden noch nicht automatisch geschützt. Sichern Sie Ihre Wiederherstellungsphrase (Einstellungen → Sicherung & Daten), damit Ihr Node dies sicher aktivieren kann.",
|
"autoshield_off_backup_seed": "Geschürfte Coinbase-Coins werden noch nicht automatisch geschützt. Sichern Sie Ihre Wiederherstellungsphrase (Einstellungen → Sicherung & Daten), damit Ihr Node dies sicher aktivieren kann.",
|
||||||
@@ -276,8 +277,12 @@
|
|||||||
"chat_delete_body_prefix": "Ihre lokale Kopie der Unterhaltung mit ",
|
"chat_delete_body_prefix": "Ihre lokale Kopie der Unterhaltung mit ",
|
||||||
"chat_delete_body_suffix": " löschen?",
|
"chat_delete_body_suffix": " löschen?",
|
||||||
"chat_delete_confirm": "Löschen",
|
"chat_delete_confirm": "Löschen",
|
||||||
|
"chat_delete_everywhere": "Überall löschen",
|
||||||
|
"chat_delete_everywhere_note": "Weist auch Ihre anderen Geräte – und eine künftige Wiederherstellung aus Ihrer Seed-Phrase – an, diese Unterhaltung auszublenden. Dabei wird ein kleines On-Chain-Signal gesendet (kostet die übliche Chat-Gebühr). Sie können sie später über „Wiederherstellen“ zurückholen.",
|
||||||
|
"chat_delete_everywhere_toast": "Wird überall gelöscht – Signal gesendet",
|
||||||
"chat_delete_failed": "Die Unterhaltung konnte nicht gelöscht werden — es wurde nichts geändert.",
|
"chat_delete_failed": "Die Unterhaltung konnte nicht gelöscht werden — es wurde nichts geändert.",
|
||||||
"chat_delete_local_note": "Dies betrifft nur dieses Gerät — die Nachrichten bleiben auf der Blockchain, und die andere Person behält ihre Kopie.",
|
"chat_delete_local_note": "Dies betrifft nur dieses Gerät — die Nachrichten bleiben auf der Blockchain, und die andere Person behält ihre Kopie.",
|
||||||
|
"chat_delete_needs_sync": "Warten Sie, bis die Wallet vollständig synchronisiert ist, um überall löschen zu können.",
|
||||||
"chat_delete_revive_note": "„Löschen“ entfernt den Verlauf auf diesem Gerät. Wenn Ihnen die Person erneut schreibt, kommt die Unterhaltung zurück.",
|
"chat_delete_revive_note": "„Löschen“ entfernt den Verlauf auf diesem Gerät. Wenn Ihnen die Person erneut schreibt, kommt die Unterhaltung zurück.",
|
||||||
"chat_delete_title": "Unterhaltung löschen?",
|
"chat_delete_title": "Unterhaltung löschen?",
|
||||||
"chat_deleted_toast": "Unterhaltung gelöscht",
|
"chat_deleted_toast": "Unterhaltung gelöscht",
|
||||||
@@ -319,10 +324,24 @@
|
|||||||
"chat_opt_global_clock": "Globales Uhrzeitformat",
|
"chat_opt_global_clock": "Globales Uhrzeitformat",
|
||||||
"chat_opt_poll": "Abrufrate",
|
"chat_opt_poll": "Abrufrate",
|
||||||
"chat_opt_timestamp": "Zeitstempel",
|
"chat_opt_timestamp": "Zeitstempel",
|
||||||
|
"chat_parts_explain": "Diese lange Nachricht wird als eine Transaktion gesendet, aufgeteilt in diese Teile – sie kommt so schnell an wie eine normale Nachricht.",
|
||||||
|
"chat_parts_explain_multi": "Diese Nachricht ist zu groß für eine Transaktion und wird daher auf mehrere aufgeteilt. Der Empfänger sieht sie erst, wenn alle Teile angekommen sind – die Zustellung dauert länger und kostet pro Transaktion eine Netzwerkgebühr.",
|
||||||
|
"chat_parts_label": "Teile",
|
||||||
"chat_pick_contact": "Aus Kontakten wählen…",
|
"chat_pick_contact": "Aus Kontakten wählen…",
|
||||||
|
"chat_receiving_large": "Große Nachricht wird empfangen …",
|
||||||
"chat_rename": "Kontakt umbenennen",
|
"chat_rename": "Kontakt umbenennen",
|
||||||
"chat_rename_hint": "Kontaktname",
|
"chat_rename_hint": "Kontaktname",
|
||||||
"chat_renamed": "Kontakt umbenannt",
|
"chat_renamed": "Kontakt umbenannt",
|
||||||
|
"chat_restore_btn": "Wiederherstellen",
|
||||||
|
"chat_restore_desc": "Unterhaltungen, die Sie überall gelöscht haben, können Sie hier zurückholen – auf diesem Gerät oder nach dem Importieren Ihrer Seed-Phrase. Beim Wiederherstellen werden die Nachrichten erneut aus der Chain importiert, und Ihre anderen Geräte werden angewiesen, sie wieder anzuzeigen.",
|
||||||
|
"chat_restore_manage": "Wiederherstellen",
|
||||||
|
"chat_restore_none": "Noch keine gelöschten Unterhaltungen gefunden. Wenn Sie eine auf einem anderen Gerät gelöscht haben, versuchen Sie den Schnellscan oder fügen Sie unten ihre Chat-Adresse ein.",
|
||||||
|
"chat_restore_paste_hint": "Chat-(z-)Adresse der Unterhaltung einfügen",
|
||||||
|
"chat_restore_paste_label": "Über Chat-Adresse wiederherstellen",
|
||||||
|
"chat_restore_scan": "Schnellscan",
|
||||||
|
"chat_restore_scanning": "Die Chain wird nach gelöschten Unterhaltungen gescannt...",
|
||||||
|
"chat_restore_title": "Unterhaltungen wiederherstellen",
|
||||||
|
"chat_restore_toast": "Unterhaltung wird wiederhergestellt – sie erscheint nach dem nächsten Scan wieder",
|
||||||
"chat_retry": "Wiederholen",
|
"chat_retry": "Wiederholen",
|
||||||
"chat_search": "Unterhaltungen durchsuchen",
|
"chat_search": "Unterhaltungen durchsuchen",
|
||||||
"chat_sec_appearance": "DARSTELLUNG",
|
"chat_sec_appearance": "DARSTELLUNG",
|
||||||
@@ -331,6 +350,35 @@
|
|||||||
"chat_send": "Senden",
|
"chat_send": "Senden",
|
||||||
"chat_send_failed": "nicht gesendet",
|
"chat_send_failed": "nicht gesendet",
|
||||||
"chat_sending": "senden…",
|
"chat_sending": "senden…",
|
||||||
|
"chat_sensitive_btn_mark": "Vertraulich machen",
|
||||||
|
"chat_sensitive_btn_setup": "Einrichten & vertraulich machen",
|
||||||
|
"chat_sensitive_btn_unlock": "Entsperren",
|
||||||
|
"chat_sensitive_btn_unlock_mark": "Entsperren & vertraulich machen",
|
||||||
|
"chat_sensitive_btn_unlock_unmark": "Entsperren & normal machen",
|
||||||
|
"chat_sensitive_btn_unmark": "Normal machen",
|
||||||
|
"chat_sensitive_err_create": "Der Tresor für vertrauliche Chats konnte nicht eingerichtet werden.",
|
||||||
|
"chat_sensitive_err_pass": "Die Passphrasen stimmen nicht überein oder sind leer.",
|
||||||
|
"chat_sensitive_err_unlock": "Falsche Passphrase.",
|
||||||
|
"chat_sensitive_lock_btn": "Vertrauliche sperren",
|
||||||
|
"chat_sensitive_mark_tip": "Vertraulich machen",
|
||||||
|
"chat_sensitive_mark_title": "Diese Unterhaltung vertraulich machen?",
|
||||||
|
"chat_sensitive_pass2_hint": "Passphrase bestätigen",
|
||||||
|
"chat_sensitive_pass_hint": "Passphrase",
|
||||||
|
"chat_sensitive_setup_label": "Erstellen Sie eine Passphrase für vertrauliche Unterhaltungen",
|
||||||
|
"chat_sensitive_toast_marked": "Unterhaltung ist jetzt vertraulich",
|
||||||
|
"chat_sensitive_toast_unlocked": "Vertrauliche Unterhaltungen entsperrt",
|
||||||
|
"chat_sensitive_toast_unmarked": "Unterhaltung ist nicht mehr vertraulich",
|
||||||
|
"chat_sensitive_unlock_body": "Geben Sie Ihre Passphrase für vertrauliche Chats ein, um Ihre vertraulichen Unterhaltungen wieder anzuzeigen.",
|
||||||
|
"chat_sensitive_unlock_btn": "Vertrauliche entsperren",
|
||||||
|
"chat_sensitive_unlock_label": "Geben Sie Ihre Passphrase für vertrauliche Chats ein",
|
||||||
|
"chat_sensitive_unlock_title": "Vertrauliche Unterhaltungen entsperren",
|
||||||
|
"chat_sensitive_unmark_body": "Diese Unterhaltung wird nicht mehr unter Ihrer Passphrase für vertrauliche Chats verschlüsselt. Neue Nachrichten werden wie bei jeder anderen Unterhaltung gespeichert.",
|
||||||
|
"chat_sensitive_unmark_tip": "Vertraulich – zum Ändern klicken",
|
||||||
|
"chat_sensitive_unmark_title": "Diese Unterhaltung normal machen?",
|
||||||
|
"chat_sensitive_warn_1": "Die Nachrichten darin werden auf diesem Gerät unter einer separaten Passphrase verschlüsselt – nicht unter Ihrer Seed-Phrase. Wer Ihre Wallet später allein aus Ihrem Seed wiederherstellt, sieht diese Unterhaltung nicht.",
|
||||||
|
"chat_sensitive_warn_dir": "Ihre hier gesendeten Nachrichten sind vollständig geschützt. Empfangene Nachrichten liegen weiterhin auf der Blockchain und könnten theoretisch von jemandem wiederhergestellt werden, der Ihren Seed und weitere Software besitzt – DragonX selbst zeigt sie ohne Ihre Passphrase niemals an.",
|
||||||
|
"chat_sensitive_warn_hist": "Nachrichten, die bereits in dieser Unterhaltung vorhanden sind, bleiben über die Chain wiederherstellbar; ab jetzt sind Nachrichten vollständig geschützt.",
|
||||||
|
"chat_sensitive_warn_lose": "Wenn Sie diese Passphrase verlieren, sind diese Nachrichten für immer verloren – sie können nicht aus Ihrem Seed wiederhergestellt werden.",
|
||||||
"chat_settings_done": "Fertig",
|
"chat_settings_done": "Fertig",
|
||||||
"chat_settings_section": "CHAT & KONTAKTE",
|
"chat_settings_section": "CHAT & KONTAKTE",
|
||||||
"chat_settings_tip": "Chat anpassen",
|
"chat_settings_tip": "Chat anpassen",
|
||||||
@@ -344,12 +392,14 @@
|
|||||||
"chat_toast_not_connected": "Nicht verbunden – Nachricht nicht gesendet.",
|
"chat_toast_not_connected": "Nicht verbunden – Nachricht nicht gesendet.",
|
||||||
"chat_toast_request_compose_failed": "Kontaktanfrage konnte nicht erstellt werden (ungültige Adresse / ungültiger Text?).",
|
"chat_toast_request_compose_failed": "Kontaktanfrage konnte nicht erstellt werden (ungültige Adresse / ungültiger Text?).",
|
||||||
"chat_toast_request_queued": "Kontaktanfrage in Warteschlange.",
|
"chat_toast_request_queued": "Kontaktanfrage in Warteschlange.",
|
||||||
|
"chat_toast_too_long": "Nachricht zu lang – kürzen Sie sie und senden Sie erneut.",
|
||||||
"chat_toast_waiting_reply": "Warte auf die Antwort des Kontakts, bevor du ihm schreiben kannst.",
|
"chat_toast_waiting_reply": "Warte auf die Antwort des Kontakts, bevor du ihm schreiben kannst.",
|
||||||
"chat_today": "Heute",
|
"chat_today": "Heute",
|
||||||
"chat_ts_12h": "12-Stunden",
|
"chat_ts_12h": "12-Stunden",
|
||||||
"chat_ts_24h": "24-Stunden",
|
"chat_ts_24h": "24-Stunden",
|
||||||
"chat_ts_global": "Global folgen",
|
"chat_ts_global": "Global folgen",
|
||||||
"chat_ts_global_short": "Global",
|
"chat_ts_global_short": "Global",
|
||||||
|
"chat_txns_label": "Tx",
|
||||||
"chat_unblock": "Blockierung aufheben",
|
"chat_unblock": "Blockierung aufheben",
|
||||||
"chat_unblocked_toast": "Blockierung der Unterhaltung aufgehoben",
|
"chat_unblocked_toast": "Blockierung der Unterhaltung aufgehoben",
|
||||||
"chat_unhide": "Einblenden",
|
"chat_unhide": "Einblenden",
|
||||||
@@ -370,11 +420,20 @@
|
|||||||
"click_to_copy": "Klicken zum Kopieren",
|
"click_to_copy": "Klicken zum Kopieren",
|
||||||
"clock_format": "Uhrzeitformat",
|
"clock_format": "Uhrzeitformat",
|
||||||
"close": "Schließen",
|
"close": "Schließen",
|
||||||
|
"compact_needs_embedded": "Das Verdichten schreibt die wallet.dat neu, was die Wallet nur mit ihrem eingebauten Node kann. Sie sind mit einem dragonxd verbunden, den Sie selbst gestartet haben – aktivieren Sie in den Einstellungen \"Eingebetteten dragonxd verwenden\" oder starten Sie Ihren dragonxd mit -salvagewallet neu, um den Speicherplatz zurückzugewinnen.",
|
||||||
|
"compact_started": "Wallet wird verdichtet – der Node startet neu und scannt die Chain erneut.",
|
||||||
|
"compact_switch_stop_failed": "Ihr externer Node konnte nicht beendet werden – er läuft noch, daher wurde nichts geändert.",
|
||||||
|
"compact_switching": "Ihr Node wird beendet und zum eingebauten gewechselt…",
|
||||||
|
"compact_wallet_confirm": "Verdichten",
|
||||||
"conf_count": "%d Best.",
|
"conf_count": "%d Best.",
|
||||||
"confirm_and_send": "Bestätigen & Senden",
|
"confirm_and_send": "Bestätigen & Senden",
|
||||||
"confirm_clear_ztx_title": "Z-Tx-Verlauf löschen bestätigen",
|
"confirm_clear_ztx_title": "Z-Tx-Verlauf löschen bestätigen",
|
||||||
"confirm_clear_ztx_warning1": "Das Löschen des Z-Transaktionsverlaufs kann dazu führen, dass Ihr geschirmtes Guthaben als 0 angezeigt wird, bis ein Wallet-Rescan durchgeführt wird.",
|
"confirm_clear_ztx_warning1": "Das Löschen des Z-Transaktionsverlaufs kann dazu führen, dass Ihr geschirmtes Guthaben als 0 angezeigt wird, bis ein Wallet-Rescan durchgeführt wird.",
|
||||||
"confirm_clear_ztx_warning2": "Wenn dies geschieht, müssen Sie Ihre Z-Adresse-Privatschlüssel mit aktiviertem Rescan neu importieren, um Ihr Guthaben wiederherzustellen.",
|
"confirm_clear_ztx_warning2": "Wenn dies geschieht, müssen Sie Ihre Z-Adresse-Privatschlüssel mit aktiviertem Rescan neu importieren, um Ihr Guthaben wiederherzustellen.",
|
||||||
|
"confirm_compact_switch_msg": "Sie sind mit einem dragonxd verbunden, den Sie selbst gestartet haben. Zum Verdichten beendet die Wallet diesen Node, wechselt zu ihrem eigenen eingebauten Node und schreibt darauf die wallet.dat neu – dabei wird neu gestartet und die Blockchain erneut gescannt, was eine Weile dauern kann. Hinweis: Der eingebaute Node ist der dragonxd im eigenen Ordner der Wallet, daher ändert sich dadurch, welchen Node Sie verwenden.",
|
||||||
|
"confirm_compact_wallet_msg": "Das Konsolidieren hat Ihre Notes zusammengeführt, aber die Wallet-Datei kann nicht von selbst schrumpfen – der Node muss sie neu schreiben, um den freigewordenen Speicherplatz zurückzugewinnen. Dabei wird der Node neu gestartet und die Blockchain erneut gescannt, was bei einer großen Wallet eine Weile dauern kann.",
|
||||||
|
"confirm_compact_wallet_safe": "Ihre Mittel und Ihr Guthaben sind sicher – eine Sicherung der aktuellen Wallet mit Zeitstempel bleibt erhalten.",
|
||||||
|
"confirm_compact_wallet_title": "Wallet verdichten",
|
||||||
"confirm_delete_blockchain_msg": "Dies stoppt den Daemon, löscht alle Blockchain-Daten (blocks, chainstate, peers) und startet eine neue Synchronisierung. Dies kann mehrere Stunden dauern.",
|
"confirm_delete_blockchain_msg": "Dies stoppt den Daemon, löscht alle Blockchain-Daten (blocks, chainstate, peers) und startet eine neue Synchronisierung. Dies kann mehrere Stunden dauern.",
|
||||||
"confirm_delete_blockchain_safe": "Ihre wallet.dat, Konfiguration und Transaktionshistorie sind sicher und werden nicht gelöscht.",
|
"confirm_delete_blockchain_safe": "Ihre wallet.dat, Konfiguration und Transaktionshistorie sind sicher und werden nicht gelöscht.",
|
||||||
"confirm_delete_blockchain_title": "Blockchain-Daten löschen",
|
"confirm_delete_blockchain_title": "Blockchain-Daten löschen",
|
||||||
@@ -505,7 +564,7 @@
|
|||||||
"console_welcome": "Willkommen bei ObsidianDragon Konsole",
|
"console_welcome": "Willkommen bei ObsidianDragon Konsole",
|
||||||
"console_zoom_in": "Vergrößern",
|
"console_zoom_in": "Vergrößern",
|
||||||
"console_zoom_out": "Verkleinern",
|
"console_zoom_out": "Verkleinern",
|
||||||
"consolidate_desc": "Fassen Sie viele kleine Eingaben zu einer einzigen geschützten Note zusammen. Weniger Notes bedeuten eine kleinere Wallet-Datei und mehr Privatsphäre.",
|
"consolidate_desc": "Jede empfangene Zahlung wird als eigene „Note“ gespeichert – wie ein einzelner Geldschein in einer Geldbörse. Mit der Zeit häufen sie sich an, und jede Note fügt Witness-Daten hinzu, die die Synchronisierung der Wallet verlangsamen und sie immer weiter wachsen lassen. Das Konsolidieren fasst viele kleine Notes zu einer pro Adresse zusammen: Ihr Guthaben ändert sich nicht, aber die Wallet wird schneller durchsucht, wächst nicht weiter und verrät weniger über Ihre Mittel.",
|
||||||
"consolidate_funds_btn": "Konsolidieren",
|
"consolidate_funds_btn": "Konsolidieren",
|
||||||
"consolidate_title": "Guthaben konsolidieren",
|
"consolidate_title": "Guthaben konsolidieren",
|
||||||
"contact_avatar": "AVATAR",
|
"contact_avatar": "AVATAR",
|
||||||
@@ -2048,6 +2107,7 @@
|
|||||||
"tt_addr_url": "Basis-URL zum Anzeigen von Adressen in einem Block-Explorer",
|
"tt_addr_url": "Basis-URL zum Anzeigen von Adressen in einem Block-Explorer",
|
||||||
"tt_address_book": "Gespeicherte Adressen für schnelles Senden verwalten",
|
"tt_address_book": "Gespeicherte Adressen für schnelles Senden verwalten",
|
||||||
"tt_animate_avatars": "Animierte Kontakt-Avatare (GIF / WebP) abspielen; aus zeigt nur das erste Bild",
|
"tt_animate_avatars": "Animierte Kontakt-Avatare (GIF / WebP) abspielen; aus zeigt nur das erste Bild",
|
||||||
|
"tt_auto_consolidate": "Fasst kleine Notes im Hintergrund unauffällig in winzigen Durchgängen zusammen, solange die Wallet im Leerlauf ist. Langsamer als eine manuelle Konsolidierung, aber kaum bemerkbar – und die Mittel jeder Adresse bleiben getrennt. Gut geeignet für Mining-/Pool-Wallets, die Tausende von Notes ansammeln.",
|
||||||
"tt_auto_lock": "Wallet nach dieser Inaktivitätszeit sperren",
|
"tt_auto_lock": "Wallet nach dieser Inaktivitätszeit sperren",
|
||||||
"tt_auto_shield": "Transparentes Guthaben automatisch an geschirmte Adressen für Datenschutz verschieben",
|
"tt_auto_shield": "Transparentes Guthaben automatisch an geschirmte Adressen für Datenschutz verschieben",
|
||||||
"tt_backup": "Eine Sicherungskopie Ihrer wallet.dat erstellen",
|
"tt_backup": "Eine Sicherungskopie Ihrer wallet.dat erstellen",
|
||||||
@@ -2174,7 +2234,8 @@
|
|||||||
"tt_validate": "Prüfen, ob eine DragonX-Adresse gültig ist",
|
"tt_validate": "Prüfen, ob eine DragonX-Adresse gültig ist",
|
||||||
"tt_verbose": "Detaillierte Verbindungsdiagnosen,\\nDaemon-Status und Port-Besitzer-Info\\nin der Konsolen-Registerkarte protokollieren",
|
"tt_verbose": "Detaillierte Verbindungsdiagnosen,\\nDaemon-Status und Port-Besitzer-Info\\nin der Konsolen-Registerkarte protokollieren",
|
||||||
"tt_view_transactions": "Transaktionen mit dieser Adresse im Verlauf-Tab anzeigen",
|
"tt_view_transactions": "Transaktionen mit dieser Adresse im Verlauf-Tab anzeigen",
|
||||||
"tt_wallet_size_warn": "Geschützte Wallets wachsen mit den Witness-Daten jedes Notes — das Zusammenführen vieler Notes in eine Adresse verringert die Größe. Zuerst sichern.",
|
"tt_wallet_size_compact": "Gibt den durch das Konsolidieren freigewordenen Speicherplatz frei. Berkeley DB kann die Datei nicht an Ort und Stelle verkleinern; daher schreibt dies die wallet.dat in eine kleinere Datei um und startet den Node mit einem vollständigen Neuscan neu (kann eine Weile dauern). Eine Sicherung der aktuellen Wallet bleibt erhalten.",
|
||||||
|
"tt_wallet_size_warn": "Jede Zahlung kommt als eigene Shielded-Note an, und jede Note trägt Witness-Daten, die Scans verlangsamen und die Wallet-Datei immer weiter wachsen lassen. Das Zusammenfassen vieler Notes zu einer pro Adresse hält die Wallet schnell und stoppt das Wachstum – Ihre Mittel und Ihr Guthaben bleiben unverändert. Die Datei gibt den freigewordenen Speicherplatz erst frei, wenn Sie sie verdichten (Berkeley DB kann nicht an Ort und Stelle schrumpfen). Sichern Sie zuerst Ihre Seed-Phrase. Sie können in den Einstellungen auch „Notes automatisch konsolidieren“ aktivieren.",
|
||||||
"tt_wallets_button": "Ihre Wallet-Dateien auflisten und zwischen ihnen wechseln",
|
"tt_wallets_button": "Ihre Wallet-Dateien auflisten und zwischen ihnen wechseln",
|
||||||
"tt_website": "Die DragonX-Website öffnen",
|
"tt_website": "Die DragonX-Website öffnen",
|
||||||
"tt_window_opacity": "Hintergrund-Deckkraft (niedriger = Desktop durch Fenster sichtbar)",
|
"tt_window_opacity": "Hintergrund-Deckkraft (niedriger = Desktop durch Fenster sichtbar)",
|
||||||
@@ -2303,8 +2364,10 @@
|
|||||||
"wallet_restore_ok": "Ursprüngliche Wallet wiederhergestellt. Der Node lädt sie jetzt.",
|
"wallet_restore_ok": "Ursprüngliche Wallet wiederhergestellt. Der Node lädt sie jetzt.",
|
||||||
"wallet_restore_started": "Ihre Originaldatei wird wiederhergestellt — bitte schließen Sie dieses Fenster nicht.",
|
"wallet_restore_started": "Ihre Originaldatei wird wiederhergestellt — bitte schließen Sie dieses Fenster nicht.",
|
||||||
"wallet_restore_stop_failed": "Der Node hat nicht rechtzeitig gestoppt, daher wurde nichts geändert. Versuchen Sie es erneut.",
|
"wallet_restore_stop_failed": "Der Node hat nicht rechtzeitig gestoppt, daher wurde nichts geändert. Versuchen Sie es erneut.",
|
||||||
|
"wallet_size_auto_info": "Ihre Wallet hat sich viele Notes angesammelt – einzelne Teile empfangener Mittel, die die Synchronisierung verlangsamen und die Datei immer weiter wachsen lassen. Die automatische Konsolidierung fasst sie unauffällig im Hintergrund zusammen, damit die Wallet schnell bleibt und nicht weiter wächst. Sie müssen nichts tun.",
|
||||||
|
"wallet_size_compact": "Wallet verdichten…",
|
||||||
"wallet_size_consolidate": "Notizen konsolidieren…",
|
"wallet_size_consolidate": "Notizen konsolidieren…",
|
||||||
"wallet_size_warn": "Diese Wallet-Datei ist groß. Das Konsolidieren Ihrer Notizen kann weiteres Wachstum eindämmen.",
|
"wallet_size_warn": "Ihre Wallet hat sich viele Notes angesammelt. Jede empfangene Zahlung wird als eigene „Note“ gespeichert; wenn sich viele davon anhäufen, verlangsamen sie die Synchronisierung der Wallet und lassen sie immer weiter wachsen. Das Konsolidieren fasst sie zu weniger Notes zusammen, damit sie schneller durchsucht wird und nicht weiter wächst – Ihr Guthaben bleibt dabei exakt gleich.",
|
||||||
"wallets_active": "Aktiv",
|
"wallets_active": "Aktiv",
|
||||||
"wallets_add": "Ordner hinzufügen",
|
"wallets_add": "Ordner hinzufügen",
|
||||||
"wallets_add_folder": "Auch einen weiteren Ordner nach Wallet-Dateien durchsuchen:",
|
"wallets_add_folder": "Auch einen weiteren Ordner nach Wallet-Dateien durchsuchen:",
|
||||||
|
|||||||
+66
-3
@@ -141,6 +141,7 @@
|
|||||||
"appx_wallet_op_running": "Aún se está ejecutando una importación de clave o un reescaneo. Vuelve a intentarlo cuando termine.",
|
"appx_wallet_op_running": "Aún se está ejecutando una importación de clave o un reescaneo. Vuelve a intentarlo cuando termine.",
|
||||||
"appx_wallet_op_shutting_down": "La cartera se está cerrando.",
|
"appx_wallet_op_shutting_down": "La cartera se está cerrando.",
|
||||||
"appx_wallet_open_failed_prefix": "Error al abrir la cartera: ",
|
"appx_wallet_open_failed_prefix": "Error al abrir la cartera: ",
|
||||||
|
"auto_consolidate": "Autoconsolidar notas",
|
||||||
"auto_shield": "Auto-proteger minería",
|
"auto_shield": "Auto-proteger minería",
|
||||||
"autoshield_by_node": "Tu nodo blinda automáticamente el coinbase minado por ti.",
|
"autoshield_by_node": "Tu nodo blinda automáticamente el coinbase minado por ti.",
|
||||||
"autoshield_off_backup_seed": "El coinbase minado aún no se está blindando automáticamente. Respalda tu frase de recuperación (Ajustes → Copia de seguridad y datos) para que tu nodo pueda activarlo de forma segura.",
|
"autoshield_off_backup_seed": "El coinbase minado aún no se está blindando automáticamente. Respalda tu frase de recuperación (Ajustes → Copia de seguridad y datos) para que tu nodo pueda activarlo de forma segura.",
|
||||||
@@ -276,8 +277,12 @@
|
|||||||
"chat_delete_body_prefix": "¿Eliminar tu copia local de la conversación con ",
|
"chat_delete_body_prefix": "¿Eliminar tu copia local de la conversación con ",
|
||||||
"chat_delete_body_suffix": "?",
|
"chat_delete_body_suffix": "?",
|
||||||
"chat_delete_confirm": "Eliminar",
|
"chat_delete_confirm": "Eliminar",
|
||||||
|
"chat_delete_everywhere": "Eliminar en todas partes",
|
||||||
|
"chat_delete_everywhere_note": "También indica a tus otros dispositivos —y a una futura restauración desde tu frase de recuperación— que oculten esta conversación. Envía una pequeña señal en la cadena (cuesta la comisión habitual del chat). Puedes recuperarla más tarde desde Restaurar.",
|
||||||
|
"chat_delete_everywhere_toast": "Eliminando en todas partes: señal enviada",
|
||||||
"chat_delete_failed": "No se pudo eliminar la conversación — no se cambió nada.",
|
"chat_delete_failed": "No se pudo eliminar la conversación — no se cambió nada.",
|
||||||
"chat_delete_local_note": "Esto solo afecta a este dispositivo — los mensajes permanecen en la cadena de bloques y la otra persona conserva su copia.",
|
"chat_delete_local_note": "Esto solo afecta a este dispositivo — los mensajes permanecen en la cadena de bloques y la otra persona conserva su copia.",
|
||||||
|
"chat_delete_needs_sync": "Espera a que la cartera se sincronice por completo para eliminar en todas partes.",
|
||||||
"chat_delete_revive_note": "«Eliminar» borra el historial en este dispositivo. Si esa persona vuelve a escribirte, la conversación reaparece.",
|
"chat_delete_revive_note": "«Eliminar» borra el historial en este dispositivo. Si esa persona vuelve a escribirte, la conversación reaparece.",
|
||||||
"chat_delete_title": "¿Eliminar conversación?",
|
"chat_delete_title": "¿Eliminar conversación?",
|
||||||
"chat_deleted_toast": "Conversación eliminada",
|
"chat_deleted_toast": "Conversación eliminada",
|
||||||
@@ -319,10 +324,24 @@
|
|||||||
"chat_opt_global_clock": "Formato de reloj global",
|
"chat_opt_global_clock": "Formato de reloj global",
|
||||||
"chat_opt_poll": "Frecuencia de sondeo",
|
"chat_opt_poll": "Frecuencia de sondeo",
|
||||||
"chat_opt_timestamp": "Marcas de tiempo",
|
"chat_opt_timestamp": "Marcas de tiempo",
|
||||||
|
"chat_parts_explain": "Este mensaje largo se envía como una sola transacción dividida en estas partes: llega tan rápido como un mensaje normal.",
|
||||||
|
"chat_parts_explain_multi": "Este mensaje es demasiado grande para una sola transacción, por lo que se divide en varias. El destinatario lo ve cuando han llegado todas las partes, así que tarda más en entregarse y cuesta una comisión de red por transacción.",
|
||||||
|
"chat_parts_label": "partes",
|
||||||
"chat_pick_contact": "Elegir de contactos…",
|
"chat_pick_contact": "Elegir de contactos…",
|
||||||
|
"chat_receiving_large": "Recibiendo un mensaje grande…",
|
||||||
"chat_rename": "Renombrar contacto",
|
"chat_rename": "Renombrar contacto",
|
||||||
"chat_rename_hint": "Nombre del contacto",
|
"chat_rename_hint": "Nombre del contacto",
|
||||||
"chat_renamed": "Contacto renombrado",
|
"chat_renamed": "Contacto renombrado",
|
||||||
|
"chat_restore_btn": "Restaurar",
|
||||||
|
"chat_restore_desc": "Las conversaciones que eliminaste en todas partes se pueden recuperar aquí, en este dispositivo o tras importar tu frase de recuperación. Restaurar reimporta los mensajes desde la cadena e indica a tus otros dispositivos que la vuelvan a mostrar.",
|
||||||
|
"chat_restore_manage": "Restaurar",
|
||||||
|
"chat_restore_none": "Aún no se han encontrado conversaciones eliminadas. Si eliminaste una en otro dispositivo, prueba con Escaneo rápido o pega su dirección de chat más abajo.",
|
||||||
|
"chat_restore_paste_hint": "Pega la dirección de chat (z) de la conversación",
|
||||||
|
"chat_restore_paste_label": "Restaurar por dirección de chat",
|
||||||
|
"chat_restore_scan": "Escaneo rápido",
|
||||||
|
"chat_restore_scanning": "Escaneando la cadena en busca de conversaciones eliminadas…",
|
||||||
|
"chat_restore_title": "Restaurar conversaciones",
|
||||||
|
"chat_restore_toast": "Restaurando conversación: reaparece tras el próximo escaneo",
|
||||||
"chat_retry": "Reintentar",
|
"chat_retry": "Reintentar",
|
||||||
"chat_search": "Buscar conversaciones",
|
"chat_search": "Buscar conversaciones",
|
||||||
"chat_sec_appearance": "APARIENCIA",
|
"chat_sec_appearance": "APARIENCIA",
|
||||||
@@ -331,6 +350,35 @@
|
|||||||
"chat_send": "Enviar",
|
"chat_send": "Enviar",
|
||||||
"chat_send_failed": "no enviado",
|
"chat_send_failed": "no enviado",
|
||||||
"chat_sending": "enviando…",
|
"chat_sending": "enviando…",
|
||||||
|
"chat_sensitive_btn_mark": "Marcar como confidencial",
|
||||||
|
"chat_sensitive_btn_setup": "Configurar y marcar como confidencial",
|
||||||
|
"chat_sensitive_btn_unlock": "Desbloquear",
|
||||||
|
"chat_sensitive_btn_unlock_mark": "Desbloquear y marcar como confidencial",
|
||||||
|
"chat_sensitive_btn_unlock_unmark": "Desbloquear y hacer normal",
|
||||||
|
"chat_sensitive_btn_unmark": "Hacer normal",
|
||||||
|
"chat_sensitive_err_create": "No se pudo crear la bóveda de chat confidencial.",
|
||||||
|
"chat_sensitive_err_pass": "Las contraseñas no coinciden o están vacías.",
|
||||||
|
"chat_sensitive_err_unlock": "Contraseña incorrecta.",
|
||||||
|
"chat_sensitive_lock_btn": "Bloquear confidenciales",
|
||||||
|
"chat_sensitive_mark_tip": "Marcar como confidencial",
|
||||||
|
"chat_sensitive_mark_title": "¿Marcar esta conversación como confidencial?",
|
||||||
|
"chat_sensitive_pass2_hint": "Confirmar contraseña",
|
||||||
|
"chat_sensitive_pass_hint": "Contraseña",
|
||||||
|
"chat_sensitive_setup_label": "Crea una contraseña para las conversaciones confidenciales",
|
||||||
|
"chat_sensitive_toast_marked": "La conversación ahora es confidencial",
|
||||||
|
"chat_sensitive_toast_unlocked": "Conversaciones confidenciales desbloqueadas",
|
||||||
|
"chat_sensitive_toast_unmarked": "La conversación ya no es confidencial",
|
||||||
|
"chat_sensitive_unlock_body": "Introduce tu contraseña de chat confidencial para volver a mostrar tus conversaciones confidenciales.",
|
||||||
|
"chat_sensitive_unlock_btn": "Desbloquear confidenciales",
|
||||||
|
"chat_sensitive_unlock_label": "Introduce tu contraseña de chat confidencial",
|
||||||
|
"chat_sensitive_unlock_title": "Desbloquear las conversaciones confidenciales",
|
||||||
|
"chat_sensitive_unmark_body": "Esta conversación dejará de estar cifrada con tu contraseña de chat confidencial. Los mensajes nuevos se guardarán como en cualquier otra conversación.",
|
||||||
|
"chat_sensitive_unmark_tip": "Confidencial: clic para cambiar",
|
||||||
|
"chat_sensitive_unmark_title": "¿Hacer normal esta conversación?",
|
||||||
|
"chat_sensitive_warn_1": "Los mensajes de esta conversación se cifrarán en este dispositivo con una contraseña aparte, no con tu frase de recuperación. Quien más adelante restaure tu cartera solo con tu frase de recuperación no verá esta conversación.",
|
||||||
|
"chat_sensitive_warn_dir": "Tus mensajes enviados aquí quedan totalmente protegidos. Los mensajes recibidos siguen en la cadena de bloques y, en teoría, alguien que tenga tu frase de recuperación y otro software podría recuperarlos; DragonX nunca los mostrará sin tu contraseña.",
|
||||||
|
"chat_sensitive_warn_hist": "Los mensajes que ya están en esta conversación siguen siendo recuperables desde la cadena; esto protege por completo los mensajes a partir de ahora.",
|
||||||
|
"chat_sensitive_warn_lose": "Si pierdes esta contraseña, estos mensajes se perderán para siempre: no se pueden recuperar con tu frase de recuperación.",
|
||||||
"chat_settings_done": "Listo",
|
"chat_settings_done": "Listo",
|
||||||
"chat_settings_section": "CHAT Y CONTACTOS",
|
"chat_settings_section": "CHAT Y CONTACTOS",
|
||||||
"chat_settings_tip": "Personalizar chat",
|
"chat_settings_tip": "Personalizar chat",
|
||||||
@@ -344,12 +392,14 @@
|
|||||||
"chat_toast_not_connected": "Sin conexión: mensaje de chat no enviado.",
|
"chat_toast_not_connected": "Sin conexión: mensaje de chat no enviado.",
|
||||||
"chat_toast_request_compose_failed": "No se pudo componer la solicitud de contacto (¿dirección o texto no válidos?).",
|
"chat_toast_request_compose_failed": "No se pudo componer la solicitud de contacto (¿dirección o texto no válidos?).",
|
||||||
"chat_toast_request_queued": "Solicitud de contacto en cola.",
|
"chat_toast_request_queued": "Solicitud de contacto en cola.",
|
||||||
|
"chat_toast_too_long": "Mensaje demasiado largo: acórtalo y envíalo de nuevo.",
|
||||||
"chat_toast_waiting_reply": "Espera a que este contacto responda antes de poder escribirle.",
|
"chat_toast_waiting_reply": "Espera a que este contacto responda antes de poder escribirle.",
|
||||||
"chat_today": "Hoy",
|
"chat_today": "Hoy",
|
||||||
"chat_ts_12h": "12 horas",
|
"chat_ts_12h": "12 horas",
|
||||||
"chat_ts_24h": "24 horas",
|
"chat_ts_24h": "24 horas",
|
||||||
"chat_ts_global": "Seguir global",
|
"chat_ts_global": "Seguir global",
|
||||||
"chat_ts_global_short": "Global",
|
"chat_ts_global_short": "Global",
|
||||||
|
"chat_txns_label": "tx",
|
||||||
"chat_unblock": "Desbloquear",
|
"chat_unblock": "Desbloquear",
|
||||||
"chat_unblocked_toast": "Conversación desbloqueada",
|
"chat_unblocked_toast": "Conversación desbloqueada",
|
||||||
"chat_unhide": "Mostrar",
|
"chat_unhide": "Mostrar",
|
||||||
@@ -370,11 +420,20 @@
|
|||||||
"click_to_copy": "Clic para copiar",
|
"click_to_copy": "Clic para copiar",
|
||||||
"clock_format": "Formato de hora",
|
"clock_format": "Formato de hora",
|
||||||
"close": "Cerrar",
|
"close": "Cerrar",
|
||||||
|
"compact_needs_embedded": "Compactar reescribe wallet.dat, y la cartera solo puede hacerlo con su nodo integrado. Estás conectado a un dragonxd que iniciaste tú mismo — activa \"Usar dragonxd integrado\" en Ajustes, o reinicia tu dragonxd con -salvagewallet para recuperar el espacio.",
|
||||||
|
"compact_started": "Compactando la cartera — el nodo se está reiniciando y volverá a escanear la cadena.",
|
||||||
|
"compact_switch_stop_failed": "No se pudo detener tu nodo externo — sigue en ejecución, así que no se cambió nada.",
|
||||||
|
"compact_switching": "Deteniendo tu nodo y cambiando al integrado…",
|
||||||
|
"compact_wallet_confirm": "Compactar",
|
||||||
"conf_count": "%d conf",
|
"conf_count": "%d conf",
|
||||||
"confirm_and_send": "Confirmar y Enviar",
|
"confirm_and_send": "Confirmar y Enviar",
|
||||||
"confirm_clear_ztx_title": "Confirmar limpieza del historial Z-Tx",
|
"confirm_clear_ztx_title": "Confirmar limpieza del historial Z-Tx",
|
||||||
"confirm_clear_ztx_warning1": "Limpiar el historial de z-transacciones puede hacer que su saldo blindado se muestre como 0 hasta que se realice un reescaneo de la billetera.",
|
"confirm_clear_ztx_warning1": "Limpiar el historial de z-transacciones puede hacer que su saldo blindado se muestre como 0 hasta que se realice un reescaneo de la billetera.",
|
||||||
"confirm_clear_ztx_warning2": "Si esto sucede, deberá reimportar las claves privadas de su dirección z con el reescaneo habilitado para recuperar su saldo.",
|
"confirm_clear_ztx_warning2": "Si esto sucede, deberá reimportar las claves privadas de su dirección z con el reescaneo habilitado para recuperar su saldo.",
|
||||||
|
"confirm_compact_switch_msg": "Estás conectado a un dragonxd que iniciaste tú mismo. Para compactar, la cartera detendrá ese nodo, cambiará a su propio nodo integrado y reescribirá wallet.dat en él — reiniciando y volviendo a escanear la blockchain, lo que puede tardar un buen rato. Nota: el nodo integrado es el dragonxd de la carpeta de la propia cartera, así que esto cambia el nodo que ejecutas.",
|
||||||
|
"confirm_compact_wallet_msg": "Al consolidar se fusionaron tus notas, pero el archivo de la cartera no puede reducirse por sí solo — el nodo tiene que reescribirlo para recuperar el espacio liberado. Esto reinicia el nodo y vuelve a escanear la blockchain, lo que puede tardar un buen rato en una cartera grande.",
|
||||||
|
"confirm_compact_wallet_safe": "Tus fondos y tu saldo están a salvo — se conserva una copia de seguridad con marca de tiempo de la cartera actual.",
|
||||||
|
"confirm_compact_wallet_title": "Compactar cartera",
|
||||||
"confirm_delete_blockchain_msg": "Esto detendrá el daemon, eliminará todos los datos de la blockchain (blocks, chainstate, peers) y comenzará una nueva sincronización desde cero. Esto puede tardar varias horas.",
|
"confirm_delete_blockchain_msg": "Esto detendrá el daemon, eliminará todos los datos de la blockchain (blocks, chainstate, peers) y comenzará una nueva sincronización desde cero. Esto puede tardar varias horas.",
|
||||||
"confirm_delete_blockchain_safe": "Su wallet.dat, configuración e historial de transacciones están seguros y no se eliminarán.",
|
"confirm_delete_blockchain_safe": "Su wallet.dat, configuración e historial de transacciones están seguros y no se eliminarán.",
|
||||||
"confirm_delete_blockchain_title": "Eliminar Datos de Blockchain",
|
"confirm_delete_blockchain_title": "Eliminar Datos de Blockchain",
|
||||||
@@ -505,7 +564,7 @@
|
|||||||
"console_welcome": "Bienvenido a la Consola de ObsidianDragon",
|
"console_welcome": "Bienvenido a la Consola de ObsidianDragon",
|
||||||
"console_zoom_in": "Acercar",
|
"console_zoom_in": "Acercar",
|
||||||
"console_zoom_out": "Alejar",
|
"console_zoom_out": "Alejar",
|
||||||
"consolidate_desc": "Combina muchas entradas pequeñas en una sola nota blindada. Menos notas significa un archivo de cartera más pequeño y mejor privacidad.",
|
"consolidate_desc": "Cada pago que recibes se almacena como una \"nota\" independiente — como un billete suelto en una cartera. Se acumulan con el tiempo, y cada nota añade datos de testigo que hacen que la cartera sea más lenta de sincronizar y que siga creciendo. Consolidar combina muchas notas pequeñas en una por dirección: tu saldo no cambia, pero la cartera escanea más rápido, deja de crecer y revela menos sobre tus fondos.",
|
||||||
"consolidate_funds_btn": "Consolidar",
|
"consolidate_funds_btn": "Consolidar",
|
||||||
"consolidate_title": "Consolidar fondos",
|
"consolidate_title": "Consolidar fondos",
|
||||||
"contact_avatar": "AVATAR",
|
"contact_avatar": "AVATAR",
|
||||||
@@ -2048,6 +2107,7 @@
|
|||||||
"tt_addr_url": "URL base para ver direcciones en un explorador de bloques",
|
"tt_addr_url": "URL base para ver direcciones en un explorador de bloques",
|
||||||
"tt_address_book": "Administrar direcciones guardadas para envío rápido",
|
"tt_address_book": "Administrar direcciones guardadas para envío rápido",
|
||||||
"tt_animate_avatars": "Reproduce avatares de contacto animados (GIF / WebP); desactivado muestra solo el primer fotograma",
|
"tt_animate_avatars": "Reproduce avatares de contacto animados (GIF / WebP); desactivado muestra solo el primer fotograma",
|
||||||
|
"tt_auto_consolidate": "Fusiona discretamente las notas pequeñas en segundo plano mientras la cartera está inactiva, en lotes diminutos. Es más lento que una consolidación manual, pero apenas se nota, y los fondos de cada dirección permanecen separados. Ideal para carteras de minería o de pool que acumulan miles de notas.",
|
||||||
"tt_auto_lock": "Bloquear billetera después de este tiempo de inactividad",
|
"tt_auto_lock": "Bloquear billetera después de este tiempo de inactividad",
|
||||||
"tt_auto_shield": "Mover automáticamente el saldo transparente a direcciones blindadas para privacidad",
|
"tt_auto_shield": "Mover automáticamente el saldo transparente a direcciones blindadas para privacidad",
|
||||||
"tt_backup": "Crear una copia de seguridad de su wallet.dat",
|
"tt_backup": "Crear una copia de seguridad de su wallet.dat",
|
||||||
@@ -2174,7 +2234,8 @@
|
|||||||
"tt_validate": "Comprobar si una dirección DragonX es válida",
|
"tt_validate": "Comprobar si una dirección DragonX es válida",
|
||||||
"tt_verbose": "Registrar diagnósticos detallados de conexión,\\nestado del daemon e info de propietario de puerto\\nen la pestaña de Consola",
|
"tt_verbose": "Registrar diagnósticos detallados de conexión,\\nestado del daemon e info de propietario de puerto\\nen la pestaña de Consola",
|
||||||
"tt_view_transactions": "Mostrar las transacciones relacionadas con esta dirección en la pestaña Historial",
|
"tt_view_transactions": "Mostrar las transacciones relacionadas con esta dirección en la pestaña Historial",
|
||||||
"tt_wallet_size_warn": "Las carteras blindadas crecen con los datos de testigo de cada nota — fusionar muchas notas en una sola dirección lo reduce. Haz una copia de seguridad primero.",
|
"tt_wallet_size_compact": "Recupera el espacio en disco liberado al consolidar. Berkeley DB no puede reducir el archivo en el mismo sitio, así que esto reescribe wallet.dat en un archivo más pequeño y reinicia el nodo con un re-escaneo completo (puede tardar un buen rato). Se conserva una copia de seguridad de la cartera actual.",
|
||||||
|
"tt_wallet_size_warn": "Cada pago llega como una nota blindada independiente, y cada nota lleva datos de testigo que ralentizan los escaneos y hacen que el archivo de la cartera siga creciendo. Fusionar muchas notas en una por dirección la mantiene rápida y detiene el crecimiento — tus fondos y tu saldo no cambian. El archivo solo recupera el espacio liberado cuando lo Compactas (Berkeley DB no puede reducirse en el mismo sitio). Haz primero una copia de seguridad de tu semilla. También puedes activar Autoconsolidar notas en Ajustes.",
|
||||||
"tt_wallets_button": "Enumera tus archivos de cartera y cambia entre ellos",
|
"tt_wallets_button": "Enumera tus archivos de cartera y cambia entre ellos",
|
||||||
"tt_website": "Abrir el sitio web de DragonX",
|
"tt_website": "Abrir el sitio web de DragonX",
|
||||||
"tt_window_opacity": "Opacidad del fondo (menor = escritorio visible a través de la ventana)",
|
"tt_window_opacity": "Opacidad del fondo (menor = escritorio visible a través de la ventana)",
|
||||||
@@ -2303,8 +2364,10 @@
|
|||||||
"wallet_restore_ok": "Cartera original restaurada. El nodo la está cargando ahora.",
|
"wallet_restore_ok": "Cartera original restaurada. El nodo la está cargando ahora.",
|
||||||
"wallet_restore_started": "Restaurando tu archivo original — por favor no cierres esta ventana.",
|
"wallet_restore_started": "Restaurando tu archivo original — por favor no cierres esta ventana.",
|
||||||
"wallet_restore_stop_failed": "El nodo no se detuvo a tiempo, así que no se cambió nada. Inténtalo de nuevo.",
|
"wallet_restore_stop_failed": "El nodo no se detuvo a tiempo, así que no se cambió nada. Inténtalo de nuevo.",
|
||||||
|
"wallet_size_auto_info": "Tu cartera ha acumulado muchas notas: piezas separadas de los fondos recibidos que ralentizan la sincronización y hacen que el archivo siga creciendo. La autoconsolidación las está fusionando discretamente en segundo plano para mantener la cartera rápida y evitar que crezca, así que no tienes que hacer nada.",
|
||||||
|
"wallet_size_compact": "Compactar cartera…",
|
||||||
"wallet_size_consolidate": "Consolidar notas…",
|
"wallet_size_consolidate": "Consolidar notas…",
|
||||||
"wallet_size_warn": "Este archivo de cartera es grande. Consolidar tus notas puede frenar un mayor crecimiento.",
|
"wallet_size_warn": "Tu cartera ha acumulado muchas notas. Cada pago que recibes se almacena como una \"nota\" independiente; al acumularse, ralentizan la sincronización de la cartera y hacen que siga creciendo. Consolidar las fusiona en menos notas para que escanee más rápido y deje de crecer — tu saldo no cambia en absoluto.",
|
||||||
"wallets_active": "Activa",
|
"wallets_active": "Activa",
|
||||||
"wallets_add": "Agregar carpeta",
|
"wallets_add": "Agregar carpeta",
|
||||||
"wallets_add_folder": "Escanear también otra carpeta en busca de archivos de cartera:",
|
"wallets_add_folder": "Escanear también otra carpeta en busca de archivos de cartera:",
|
||||||
|
|||||||
+66
-3
@@ -141,6 +141,7 @@
|
|||||||
"appx_wallet_op_running": "Une importation de clé ou une réanalyse est encore en cours. Réessayez une fois qu'elle est finie.",
|
"appx_wallet_op_running": "Une importation de clé ou une réanalyse est encore en cours. Réessayez une fois qu'elle est finie.",
|
||||||
"appx_wallet_op_shutting_down": "Le portefeuille est en cours de fermeture.",
|
"appx_wallet_op_shutting_down": "Le portefeuille est en cours de fermeture.",
|
||||||
"appx_wallet_open_failed_prefix": "Échec de l'ouverture du portefeuille : ",
|
"appx_wallet_open_failed_prefix": "Échec de l'ouverture du portefeuille : ",
|
||||||
|
"auto_consolidate": "Consolidation auto des notes",
|
||||||
"auto_shield": "Auto-blindage du minage",
|
"auto_shield": "Auto-blindage du minage",
|
||||||
"autoshield_by_node": "Votre nœud blinde automatiquement les coinbase minées pour vous.",
|
"autoshield_by_node": "Votre nœud blinde automatiquement les coinbase minées pour vous.",
|
||||||
"autoshield_off_backup_seed": "Les coinbase minées ne sont pas encore blindées automatiquement. Sauvegardez votre phrase de récupération (Paramètres → Sauvegarde et données) pour que votre nœud puisse l'activer en toute sécurité.",
|
"autoshield_off_backup_seed": "Les coinbase minées ne sont pas encore blindées automatiquement. Sauvegardez votre phrase de récupération (Paramètres → Sauvegarde et données) pour que votre nœud puisse l'activer en toute sécurité.",
|
||||||
@@ -276,8 +277,12 @@
|
|||||||
"chat_delete_body_prefix": "Supprimer votre copie locale de la conversation avec ",
|
"chat_delete_body_prefix": "Supprimer votre copie locale de la conversation avec ",
|
||||||
"chat_delete_body_suffix": " ?",
|
"chat_delete_body_suffix": " ?",
|
||||||
"chat_delete_confirm": "Supprimer",
|
"chat_delete_confirm": "Supprimer",
|
||||||
|
"chat_delete_everywhere": "Supprimer partout",
|
||||||
|
"chat_delete_everywhere_note": "Indique aussi à vos autres appareils — et à une future restauration depuis votre phrase de récupération — de masquer cette conversation. Un petit signal est envoyé sur la chaîne (il coûte les frais de chat habituels). Vous pourrez la restaurer plus tard depuis Restaurer.",
|
||||||
|
"chat_delete_everywhere_toast": "Suppression partout — signal envoyé",
|
||||||
"chat_delete_failed": "Impossible de supprimer la conversation — rien n'a été modifié.",
|
"chat_delete_failed": "Impossible de supprimer la conversation — rien n'a été modifié.",
|
||||||
"chat_delete_local_note": "Cela n'affecte que cet appareil — les messages restent sur la blockchain et l'autre personne conserve sa copie.",
|
"chat_delete_local_note": "Cela n'affecte que cet appareil — les messages restent sur la blockchain et l'autre personne conserve sa copie.",
|
||||||
|
"chat_delete_needs_sync": "Attendez que le portefeuille soit entièrement synchronisé pour supprimer partout.",
|
||||||
"chat_delete_revive_note": "« Supprimer » efface l'historique sur cet appareil. Si cette personne vous écrit à nouveau, la conversation revient.",
|
"chat_delete_revive_note": "« Supprimer » efface l'historique sur cet appareil. Si cette personne vous écrit à nouveau, la conversation revient.",
|
||||||
"chat_delete_title": "Supprimer la conversation ?",
|
"chat_delete_title": "Supprimer la conversation ?",
|
||||||
"chat_deleted_toast": "Conversation supprimée",
|
"chat_deleted_toast": "Conversation supprimée",
|
||||||
@@ -319,10 +324,24 @@
|
|||||||
"chat_opt_global_clock": "Format d'horloge global",
|
"chat_opt_global_clock": "Format d'horloge global",
|
||||||
"chat_opt_poll": "Fréquence d'actualisation",
|
"chat_opt_poll": "Fréquence d'actualisation",
|
||||||
"chat_opt_timestamp": "Horodatage",
|
"chat_opt_timestamp": "Horodatage",
|
||||||
|
"chat_parts_explain": "Ce long message est envoyé en une seule transaction répartie entre ces parties — il arrive aussi vite qu'un message normal.",
|
||||||
|
"chat_parts_explain_multi": "Ce message est trop volumineux pour une seule transaction : il est donc réparti sur plusieurs. Le destinataire le voit une fois toutes les parties arrivées, sa livraison prend donc plus de temps et coûte des frais réseau par transaction.",
|
||||||
|
"chat_parts_label": "parties",
|
||||||
"chat_pick_contact": "Choisir dans les contacts…",
|
"chat_pick_contact": "Choisir dans les contacts…",
|
||||||
|
"chat_receiving_large": "Réception d'un message volumineux…",
|
||||||
"chat_rename": "Renommer le contact",
|
"chat_rename": "Renommer le contact",
|
||||||
"chat_rename_hint": "Nom du contact",
|
"chat_rename_hint": "Nom du contact",
|
||||||
"chat_renamed": "Contact renommé",
|
"chat_renamed": "Contact renommé",
|
||||||
|
"chat_restore_btn": "Restaurer",
|
||||||
|
"chat_restore_desc": "Les conversations que vous avez supprimées partout peuvent être rétablies ici — sur cet appareil ou après avoir importé votre phrase de récupération. La restauration réimporte les messages depuis la chaîne et indique à vos autres appareils de l'afficher à nouveau.",
|
||||||
|
"chat_restore_manage": "Restaurer",
|
||||||
|
"chat_restore_none": "Aucune conversation supprimée trouvée pour l'instant. Si vous en avez supprimé une sur un autre appareil, essayez l'Analyse rapide ou collez son adresse de chat ci-dessous.",
|
||||||
|
"chat_restore_paste_hint": "Collez l'adresse de chat (z) de la conversation",
|
||||||
|
"chat_restore_paste_label": "Restaurer par adresse de chat",
|
||||||
|
"chat_restore_scan": "Analyse rapide",
|
||||||
|
"chat_restore_scanning": "Analyse de la chaîne à la recherche de conversations supprimées...",
|
||||||
|
"chat_restore_title": "Restaurer des conversations",
|
||||||
|
"chat_restore_toast": "Restauration de la conversation — elle réapparaît après la prochaine analyse",
|
||||||
"chat_retry": "Réessayer",
|
"chat_retry": "Réessayer",
|
||||||
"chat_search": "Rechercher des conversations",
|
"chat_search": "Rechercher des conversations",
|
||||||
"chat_sec_appearance": "APPARENCE",
|
"chat_sec_appearance": "APPARENCE",
|
||||||
@@ -331,6 +350,35 @@
|
|||||||
"chat_send": "Envoyer",
|
"chat_send": "Envoyer",
|
||||||
"chat_send_failed": "non envoyé",
|
"chat_send_failed": "non envoyé",
|
||||||
"chat_sending": "envoi…",
|
"chat_sending": "envoi…",
|
||||||
|
"chat_sensitive_btn_mark": "Rendre confidentielle",
|
||||||
|
"chat_sensitive_btn_setup": "Configurer et rendre confidentielle",
|
||||||
|
"chat_sensitive_btn_unlock": "Déverrouiller",
|
||||||
|
"chat_sensitive_btn_unlock_mark": "Déverrouiller et rendre confidentielle",
|
||||||
|
"chat_sensitive_btn_unlock_unmark": "Déverrouiller et rendre normale",
|
||||||
|
"chat_sensitive_btn_unmark": "Rendre normale",
|
||||||
|
"chat_sensitive_err_create": "Impossible de créer le coffre des conversations confidentielles.",
|
||||||
|
"chat_sensitive_err_pass": "Les phrases secrètes ne correspondent pas, ou sont vides.",
|
||||||
|
"chat_sensitive_err_unlock": "Phrase secrète incorrecte.",
|
||||||
|
"chat_sensitive_lock_btn": "Verrouiller les confidentielles",
|
||||||
|
"chat_sensitive_mark_tip": "Rendre confidentielle",
|
||||||
|
"chat_sensitive_mark_title": "Rendre cette conversation confidentielle ?",
|
||||||
|
"chat_sensitive_pass2_hint": "Confirmez la phrase secrète",
|
||||||
|
"chat_sensitive_pass_hint": "Phrase secrète",
|
||||||
|
"chat_sensitive_setup_label": "Créez une phrase secrète pour les conversations confidentielles",
|
||||||
|
"chat_sensitive_toast_marked": "La conversation est désormais confidentielle",
|
||||||
|
"chat_sensitive_toast_unlocked": "Conversations confidentielles déverrouillées",
|
||||||
|
"chat_sensitive_toast_unmarked": "La conversation n'est plus confidentielle",
|
||||||
|
"chat_sensitive_unlock_body": "Saisissez votre phrase secrète des conversations confidentielles pour afficher à nouveau vos conversations confidentielles.",
|
||||||
|
"chat_sensitive_unlock_btn": "Déverrouiller les confidentielles",
|
||||||
|
"chat_sensitive_unlock_label": "Saisissez votre phrase secrète des conversations confidentielles",
|
||||||
|
"chat_sensitive_unlock_title": "Déverrouiller les conversations confidentielles",
|
||||||
|
"chat_sensitive_unmark_body": "Cette conversation ne sera plus chiffrée avec votre phrase secrète des conversations confidentielles. Les nouveaux messages seront stockés comme ceux de n'importe quelle autre conversation.",
|
||||||
|
"chat_sensitive_unmark_tip": "Confidentielle — cliquez pour modifier",
|
||||||
|
"chat_sensitive_unmark_title": "Rendre cette conversation normale ?",
|
||||||
|
"chat_sensitive_warn_1": "Ses messages seront chiffrés sur cet appareil avec une phrase secrète distincte — et non avec votre phrase de récupération. Quiconque restaurera plus tard votre portefeuille à partir de votre seule phrase de récupération ne verra pas cette conversation.",
|
||||||
|
"chat_sensitive_warn_dir": "Vos messages envoyés ici sont entièrement protégés. Les messages reçus restent sur la blockchain et pourraient, en théorie, être récupérés par une personne disposant de votre phrase de récupération et d'un autre logiciel — mais DragonX lui-même ne les affichera jamais sans votre phrase secrète.",
|
||||||
|
"chat_sensitive_warn_hist": "Les messages déjà présents dans cette conversation restent récupérables depuis la chaîne ; la protection s'applique pleinement aux messages à venir.",
|
||||||
|
"chat_sensitive_warn_lose": "Si vous perdez cette phrase secrète, ces messages sont perdus à jamais — ils ne pourront pas être récupérés à partir de votre phrase de récupération.",
|
||||||
"chat_settings_done": "Terminé",
|
"chat_settings_done": "Terminé",
|
||||||
"chat_settings_section": "CHAT ET CONTACTS",
|
"chat_settings_section": "CHAT ET CONTACTS",
|
||||||
"chat_settings_tip": "Personnaliser le chat",
|
"chat_settings_tip": "Personnaliser le chat",
|
||||||
@@ -344,12 +392,14 @@
|
|||||||
"chat_toast_not_connected": "Non connecté — message non envoyé.",
|
"chat_toast_not_connected": "Non connecté — message non envoyé.",
|
||||||
"chat_toast_request_compose_failed": "Impossible de composer la demande de contact (adresse / texte invalide ?).",
|
"chat_toast_request_compose_failed": "Impossible de composer la demande de contact (adresse / texte invalide ?).",
|
||||||
"chat_toast_request_queued": "Demande de contact mise en file d'attente.",
|
"chat_toast_request_queued": "Demande de contact mise en file d'attente.",
|
||||||
|
"chat_toast_too_long": "Message trop long — raccourcissez-le et renvoyez-le.",
|
||||||
"chat_toast_waiting_reply": "En attente de la réponse de ce contact avant de pouvoir lui écrire.",
|
"chat_toast_waiting_reply": "En attente de la réponse de ce contact avant de pouvoir lui écrire.",
|
||||||
"chat_today": "Aujourd'hui",
|
"chat_today": "Aujourd'hui",
|
||||||
"chat_ts_12h": "12 heures",
|
"chat_ts_12h": "12 heures",
|
||||||
"chat_ts_24h": "24 heures",
|
"chat_ts_24h": "24 heures",
|
||||||
"chat_ts_global": "Suivre global",
|
"chat_ts_global": "Suivre global",
|
||||||
"chat_ts_global_short": "Global",
|
"chat_ts_global_short": "Global",
|
||||||
|
"chat_txns_label": "tx",
|
||||||
"chat_unblock": "Débloquer",
|
"chat_unblock": "Débloquer",
|
||||||
"chat_unblocked_toast": "Conversation débloquée",
|
"chat_unblocked_toast": "Conversation débloquée",
|
||||||
"chat_unhide": "Afficher",
|
"chat_unhide": "Afficher",
|
||||||
@@ -370,11 +420,20 @@
|
|||||||
"click_to_copy": "Cliquez pour copier",
|
"click_to_copy": "Cliquez pour copier",
|
||||||
"clock_format": "Format d'horloge",
|
"clock_format": "Format d'horloge",
|
||||||
"close": "Fermer",
|
"close": "Fermer",
|
||||||
|
"compact_needs_embedded": "Le compactage réécrit wallet.dat, ce que le portefeuille ne peut faire qu'avec son nœud intégré. Vous êtes connecté à un dragonxd que vous avez lancé vous-même — activez « Utiliser le dragonxd intégré » dans Paramètres, ou redémarrez votre dragonxd avec -salvagewallet pour récupérer l'espace.",
|
||||||
|
"compact_started": "Compactage du portefeuille — le nœud redémarre et va re-scanner la chaîne.",
|
||||||
|
"compact_switch_stop_failed": "Impossible d'arrêter votre nœud externe — il est toujours en cours d'exécution, donc rien n'a été modifié.",
|
||||||
|
"compact_switching": "Arrêt de votre nœud et passage au nœud intégré…",
|
||||||
|
"compact_wallet_confirm": "Compacter",
|
||||||
"conf_count": "%d conf.",
|
"conf_count": "%d conf.",
|
||||||
"confirm_and_send": "Confirmer & Envoyer",
|
"confirm_and_send": "Confirmer & Envoyer",
|
||||||
"confirm_clear_ztx_title": "Confirmer l'effacement de l'historique Z-Tx",
|
"confirm_clear_ztx_title": "Confirmer l'effacement de l'historique Z-Tx",
|
||||||
"confirm_clear_ztx_warning1": "L'effacement de l'historique des z-transactions peut faire apparaître votre solde blindé à 0 jusqu'à ce qu'un rescan du portefeuille soit effectué.",
|
"confirm_clear_ztx_warning1": "L'effacement de l'historique des z-transactions peut faire apparaître votre solde blindé à 0 jusqu'à ce qu'un rescan du portefeuille soit effectué.",
|
||||||
"confirm_clear_ztx_warning2": "Si cela se produit, vous devrez réimporter les clés privées de votre adresse z avec le rescan activé pour récupérer votre solde.",
|
"confirm_clear_ztx_warning2": "Si cela se produit, vous devrez réimporter les clés privées de votre adresse z avec le rescan activé pour récupérer votre solde.",
|
||||||
|
"confirm_compact_switch_msg": "Vous êtes connecté à un dragonxd que vous avez lancé vous-même. Pour compacter, le portefeuille va arrêter ce nœud, passer à son propre nœud intégré et y réécrire wallet.dat — ce qui redémarre le nœud et re-scanne la blockchain, et peut prendre du temps. Remarque : le nœud intégré est le dragonxd situé dans le dossier propre au portefeuille, donc cela change le nœud que vous utilisez.",
|
||||||
|
"confirm_compact_wallet_msg": "La consolidation a fusionné vos notes, mais le fichier du portefeuille ne peut pas rétrécir de lui-même — le nœud doit le réécrire pour récupérer l'espace libéré. Cela redémarre le nœud et re-scanne la blockchain, ce qui peut prendre du temps sur un gros portefeuille.",
|
||||||
|
"confirm_compact_wallet_safe": "Vos fonds et votre solde sont en sécurité — une sauvegarde horodatée du portefeuille actuel est conservée.",
|
||||||
|
"confirm_compact_wallet_title": "Compacter le portefeuille",
|
||||||
"confirm_delete_blockchain_msg": "Cela arrêtera le daemon, supprimera toutes les données de la blockchain (blocks, chainstate, peers) et démarrera une nouvelle synchronisation. Cela peut prendre plusieurs heures.",
|
"confirm_delete_blockchain_msg": "Cela arrêtera le daemon, supprimera toutes les données de la blockchain (blocks, chainstate, peers) et démarrera une nouvelle synchronisation. Cela peut prendre plusieurs heures.",
|
||||||
"confirm_delete_blockchain_safe": "Votre wallet.dat, votre configuration et votre historique de transactions sont en sécurité et ne seront pas supprimés.",
|
"confirm_delete_blockchain_safe": "Votre wallet.dat, votre configuration et votre historique de transactions sont en sécurité et ne seront pas supprimés.",
|
||||||
"confirm_delete_blockchain_title": "Supprimer les données Blockchain",
|
"confirm_delete_blockchain_title": "Supprimer les données Blockchain",
|
||||||
@@ -505,7 +564,7 @@
|
|||||||
"console_welcome": "Bienvenue dans la console ObsidianDragon",
|
"console_welcome": "Bienvenue dans la console ObsidianDragon",
|
||||||
"console_zoom_in": "Agrandir",
|
"console_zoom_in": "Agrandir",
|
||||||
"console_zoom_out": "Réduire",
|
"console_zoom_out": "Réduire",
|
||||||
"consolidate_desc": "Combinez de nombreuses petites entrées en une seule note blindée. Moins de notes signifie un fichier de portefeuille plus petit et une meilleure confidentialité.",
|
"consolidate_desc": "Chaque paiement que vous recevez est stocké comme une « note » distincte — comme un billet individuel dans un portefeuille. Elles s'accumulent avec le temps, et chaque note ajoute des données de témoin qui ralentissent la synchronisation du portefeuille et le font grossir sans cesse. Consolider combine de nombreuses petites notes en une seule par adresse : votre solde ne change pas, mais le portefeuille s'analyse plus vite, cesse de grossir et révèle moins de choses sur vos fonds.",
|
||||||
"consolidate_funds_btn": "Consolider",
|
"consolidate_funds_btn": "Consolider",
|
||||||
"consolidate_title": "Consolider les fonds",
|
"consolidate_title": "Consolider les fonds",
|
||||||
"contact_avatar": "AVATAR",
|
"contact_avatar": "AVATAR",
|
||||||
@@ -2048,6 +2107,7 @@
|
|||||||
"tt_addr_url": "URL de base pour consulter les adresses dans un explorateur de blocs",
|
"tt_addr_url": "URL de base pour consulter les adresses dans un explorateur de blocs",
|
||||||
"tt_address_book": "Gérer les adresses enregistrées pour un envoi rapide",
|
"tt_address_book": "Gérer les adresses enregistrées pour un envoi rapide",
|
||||||
"tt_animate_avatars": "Lit les avatars de contact animés (GIF / WebP) ; désactivé n'affiche que la première image",
|
"tt_animate_avatars": "Lit les avatars de contact animés (GIF / WebP) ; désactivé n'affiche que la première image",
|
||||||
|
"tt_auto_consolidate": "Fusionne discrètement les petites notes en arrière-plan lorsque le portefeuille est inactif, par minuscules lots. Plus lent qu'une consolidation manuelle, mais à peine perceptible, et les fonds de chaque adresse restent séparés. Idéal pour les portefeuilles de minage/pool qui accumulent des milliers de notes.",
|
||||||
"tt_auto_lock": "Verrouiller le portefeuille après cette durée d'inactivité",
|
"tt_auto_lock": "Verrouiller le portefeuille après cette durée d'inactivité",
|
||||||
"tt_auto_shield": "Déplacer automatiquement le solde transparent vers des adresses blindées pour la confidentialité",
|
"tt_auto_shield": "Déplacer automatiquement le solde transparent vers des adresses blindées pour la confidentialité",
|
||||||
"tt_backup": "Créer une sauvegarde de votre wallet.dat",
|
"tt_backup": "Créer une sauvegarde de votre wallet.dat",
|
||||||
@@ -2174,7 +2234,8 @@
|
|||||||
"tt_validate": "Vérifier si une adresse DragonX est valide",
|
"tt_validate": "Vérifier si une adresse DragonX est valide",
|
||||||
"tt_verbose": "Journaliser les diagnostics de connexion détaillés,\\nl'état du daemon et les informations de propriétaire de port\\ndans l'onglet Console",
|
"tt_verbose": "Journaliser les diagnostics de connexion détaillés,\\nl'état du daemon et les informations de propriétaire de port\\ndans l'onglet Console",
|
||||||
"tt_view_transactions": "Afficher les transactions impliquant cette adresse dans l'onglet Historique",
|
"tt_view_transactions": "Afficher les transactions impliquant cette adresse dans l'onglet Historique",
|
||||||
"tt_wallet_size_warn": "Les portefeuilles blindés grandissent avec les données de témoin de chaque note — fusionner de nombreuses notes en une seule adresse réduit cette taille. Sauvegardez d'abord.",
|
"tt_wallet_size_compact": "Récupérez l'espace disque libéré par la consolidation. Berkeley DB ne peut pas réduire le fichier sur place : cette action réécrit wallet.dat dans un fichier plus petit et redémarre le nœud avec un re-scan complet (cela peut prendre du temps). Une sauvegarde du portefeuille actuel est conservée.",
|
||||||
|
"tt_wallet_size_warn": "Chaque paiement arrive sous forme de note blindée distincte, et chaque note porte des données de témoin qui ralentissent les analyses et font grossir le fichier du portefeuille. Fusionner de nombreuses notes en une seule par adresse le garde rapide et stoppe la croissance — vos fonds et votre solde restent inchangés. Le fichier ne récupère l'espace libéré que lorsque vous le compactez (Berkeley DB ne peut pas réduire un fichier sur place). Sauvegardez d'abord votre phrase de récupération. Vous pouvez aussi activer la consolidation auto des notes dans Paramètres.",
|
||||||
"tt_wallets_button": "Répertoriez vos fichiers de portefeuille et passez de l'un à l'autre",
|
"tt_wallets_button": "Répertoriez vos fichiers de portefeuille et passez de l'un à l'autre",
|
||||||
"tt_website": "Ouvrir le site web DragonX",
|
"tt_website": "Ouvrir le site web DragonX",
|
||||||
"tt_window_opacity": "Opacité de l'arrière-plan (plus bas = bureau visible à travers la fenêtre)",
|
"tt_window_opacity": "Opacité de l'arrière-plan (plus bas = bureau visible à travers la fenêtre)",
|
||||||
@@ -2303,8 +2364,10 @@
|
|||||||
"wallet_restore_ok": "Portefeuille original restauré. Le nœud le charge maintenant.",
|
"wallet_restore_ok": "Portefeuille original restauré. Le nœud le charge maintenant.",
|
||||||
"wallet_restore_started": "Restauration de votre fichier original — veuillez ne pas fermer cette fenêtre.",
|
"wallet_restore_started": "Restauration de votre fichier original — veuillez ne pas fermer cette fenêtre.",
|
||||||
"wallet_restore_stop_failed": "Le nœud ne s'est pas arrêté à temps, donc rien n'a été modifié. Réessayez.",
|
"wallet_restore_stop_failed": "Le nœud ne s'est pas arrêté à temps, donc rien n'a été modifié. Réessayez.",
|
||||||
|
"wallet_size_auto_info": "Votre portefeuille a accumulé beaucoup de notes — des morceaux distincts de fonds reçus qui ralentissent la synchronisation et font grossir le fichier. La consolidation auto des notes les fusionne discrètement en arrière-plan pour garder le portefeuille rapide et l'empêcher de grossir, vous n'avez donc rien à faire.",
|
||||||
|
"wallet_size_compact": "Compacter le portefeuille…",
|
||||||
"wallet_size_consolidate": "Consolider les notes…",
|
"wallet_size_consolidate": "Consolider les notes…",
|
||||||
"wallet_size_warn": "Ce fichier de portefeuille est volumineux. Consolider vos notes peut freiner sa croissance.",
|
"wallet_size_warn": "Votre portefeuille a accumulé beaucoup de notes. Chaque paiement que vous recevez est stocké comme une « note » distincte ; en s'accumulant, elles ralentissent la synchronisation du portefeuille et le font grossir sans cesse. Consolider les fusionne en moins de notes pour qu'il s'analyse plus vite et cesse de grossir — votre solde reste exactement le même.",
|
||||||
"wallets_active": "Actif",
|
"wallets_active": "Actif",
|
||||||
"wallets_add": "Ajouter le dossier",
|
"wallets_add": "Ajouter le dossier",
|
||||||
"wallets_add_folder": "Analyser aussi un autre dossier à la recherche de fichiers de portefeuille :",
|
"wallets_add_folder": "Analyser aussi un autre dossier à la recherche de fichiers de portefeuille :",
|
||||||
|
|||||||
+66
-3
@@ -141,6 +141,7 @@
|
|||||||
"appx_wallet_op_running": "鍵のインポートまたは再スキャンがまだ実行中です。完了してからもう一度お試しください。",
|
"appx_wallet_op_running": "鍵のインポートまたは再スキャンがまだ実行中です。完了してからもう一度お試しください。",
|
||||||
"appx_wallet_op_shutting_down": "ウォレットを終了しています。",
|
"appx_wallet_op_shutting_down": "ウォレットを終了しています。",
|
||||||
"appx_wallet_open_failed_prefix": "ウォレットを開けませんでした: ",
|
"appx_wallet_open_failed_prefix": "ウォレットを開けませんでした: ",
|
||||||
|
"auto_consolidate": "ノートを自動統合",
|
||||||
"auto_shield": "マイニング自動シールド",
|
"auto_shield": "マイニング自動シールド",
|
||||||
"autoshield_by_node": "あなたのノードは、マイニングしたコインベースを自動的にシールドします。",
|
"autoshield_by_node": "あなたのノードは、マイニングしたコインベースを自動的にシールドします。",
|
||||||
"autoshield_off_backup_seed": "マイニングしたコインベースはまだ自動シールドされていません。ノードが安全に有効化できるよう、シードフレーズをバックアップしてください(設定 → バックアップとデータ)。",
|
"autoshield_off_backup_seed": "マイニングしたコインベースはまだ自動シールドされていません。ノードが安全に有効化できるよう、シードフレーズをバックアップしてください(設定 → バックアップとデータ)。",
|
||||||
@@ -276,8 +277,12 @@
|
|||||||
"chat_delete_body_prefix": "次の相手との会話のローカルコピーを削除しますか:",
|
"chat_delete_body_prefix": "次の相手との会話のローカルコピーを削除しますか:",
|
||||||
"chat_delete_body_suffix": "?",
|
"chat_delete_body_suffix": "?",
|
||||||
"chat_delete_confirm": "削除",
|
"chat_delete_confirm": "削除",
|
||||||
|
"chat_delete_everywhere": "すべての端末で削除",
|
||||||
|
"chat_delete_everywhere_note": "他の端末にも、そして将来シードフレーズから復元した際にも、この会話を非表示にするよう伝えます。チェーン上に小さな信号を送信します(通常のチャット手数料がかかります)。後で「復元」から元に戻せます。",
|
||||||
|
"chat_delete_everywhere_toast": "すべての端末で削除中 — 信号を送信しました",
|
||||||
"chat_delete_failed": "会話を削除できませんでした — 変更はありません。",
|
"chat_delete_failed": "会話を削除できませんでした — 変更はありません。",
|
||||||
"chat_delete_local_note": "これはこの端末のみに影響します — メッセージはブロックチェーン上に残り、相手も自分のコピーを保持し続けます。",
|
"chat_delete_local_note": "これはこの端末のみに影響します — メッセージはブロックチェーン上に残り、相手も自分のコピーを保持し続けます。",
|
||||||
|
"chat_delete_needs_sync": "すべての端末で削除するには、ウォレットの同期が完了するまでお待ちください。",
|
||||||
"chat_delete_revive_note": "「削除」はこの端末上の履歴を消去します。相手から再度メッセージが届くと、会話は復活します。",
|
"chat_delete_revive_note": "「削除」はこの端末上の履歴を消去します。相手から再度メッセージが届くと、会話は復活します。",
|
||||||
"chat_delete_title": "会話を削除しますか?",
|
"chat_delete_title": "会話を削除しますか?",
|
||||||
"chat_deleted_toast": "会話を削除しました",
|
"chat_deleted_toast": "会話を削除しました",
|
||||||
@@ -319,10 +324,24 @@
|
|||||||
"chat_opt_global_clock": "全体の時刻形式",
|
"chat_opt_global_clock": "全体の時刻形式",
|
||||||
"chat_opt_poll": "取得間隔",
|
"chat_opt_poll": "取得間隔",
|
||||||
"chat_opt_timestamp": "タイムスタンプ",
|
"chat_opt_timestamp": "タイムスタンプ",
|
||||||
|
"chat_parts_explain": "この長いメッセージは、これらのパートに分割された1つのトランザクションとして送信されます。通常のメッセージと同じ速さで届きます。",
|
||||||
|
"chat_parts_explain_multi": "このメッセージは1つのトランザクションに収まらないため、複数に分割して送信されます。受信者はすべてのパートが届いてから表示できるため、配信に時間がかかり、トランザクションごとにネットワーク手数料がかかります。",
|
||||||
|
"chat_parts_label": "パート",
|
||||||
"chat_pick_contact": "連絡先から選択…",
|
"chat_pick_contact": "連絡先から選択…",
|
||||||
|
"chat_receiving_large": "大きなメッセージを受信中…",
|
||||||
"chat_rename": "連絡先の名前を変更",
|
"chat_rename": "連絡先の名前を変更",
|
||||||
"chat_rename_hint": "連絡先名",
|
"chat_rename_hint": "連絡先名",
|
||||||
"chat_renamed": "連絡先の名前を変更しました",
|
"chat_renamed": "連絡先の名前を変更しました",
|
||||||
|
"chat_restore_btn": "復元",
|
||||||
|
"chat_restore_desc": "すべての端末で削除した会話は、ここから元に戻せます — この端末でも、シードフレーズをインポートした後でも可能です。復元すると、チェーンからメッセージが再インポートされ、他の端末にも再表示するよう伝えます。",
|
||||||
|
"chat_restore_manage": "復元",
|
||||||
|
"chat_restore_none": "削除された会話はまだ見つかりません。別の端末で削除した場合は、「クイックスキャン」を試すか、下にチャットアドレスを貼り付けてください。",
|
||||||
|
"chat_restore_paste_hint": "会話のチャット(z)アドレスを貼り付け",
|
||||||
|
"chat_restore_paste_label": "チャットアドレスで復元",
|
||||||
|
"chat_restore_scan": "クイックスキャン",
|
||||||
|
"chat_restore_scanning": "削除された会話をチェーンでスキャン中…",
|
||||||
|
"chat_restore_title": "会話を復元",
|
||||||
|
"chat_restore_toast": "会話を復元中 — 次回のスキャン後に再表示されます",
|
||||||
"chat_retry": "再送信",
|
"chat_retry": "再送信",
|
||||||
"chat_search": "会話を検索",
|
"chat_search": "会話を検索",
|
||||||
"chat_sec_appearance": "外観",
|
"chat_sec_appearance": "外観",
|
||||||
@@ -331,6 +350,35 @@
|
|||||||
"chat_send": "送信",
|
"chat_send": "送信",
|
||||||
"chat_send_failed": "未送信",
|
"chat_send_failed": "未送信",
|
||||||
"chat_sending": "送信中…",
|
"chat_sending": "送信中…",
|
||||||
|
"chat_sensitive_btn_mark": "機密にする",
|
||||||
|
"chat_sensitive_btn_setup": "設定して機密にする",
|
||||||
|
"chat_sensitive_btn_unlock": "ロック解除",
|
||||||
|
"chat_sensitive_btn_unlock_mark": "ロック解除して機密にする",
|
||||||
|
"chat_sensitive_btn_unlock_unmark": "ロック解除して通常に戻す",
|
||||||
|
"chat_sensitive_btn_unmark": "通常に戻す",
|
||||||
|
"chat_sensitive_err_create": "機密チャット用ボールトの作成に失敗しました。",
|
||||||
|
"chat_sensitive_err_pass": "パスフレーズが一致しないか、空です。",
|
||||||
|
"chat_sensitive_err_unlock": "パスフレーズが正しくありません。",
|
||||||
|
"chat_sensitive_lock_btn": "機密をロック",
|
||||||
|
"chat_sensitive_mark_tip": "機密にする",
|
||||||
|
"chat_sensitive_mark_title": "この会話を機密にしますか?",
|
||||||
|
"chat_sensitive_pass2_hint": "パスフレーズを再入力",
|
||||||
|
"chat_sensitive_pass_hint": "パスフレーズ",
|
||||||
|
"chat_sensitive_setup_label": "機密会話用のパスフレーズを作成",
|
||||||
|
"chat_sensitive_toast_marked": "会話を機密にしました",
|
||||||
|
"chat_sensitive_toast_unlocked": "機密会話のロックを解除しました",
|
||||||
|
"chat_sensitive_toast_unmarked": "会話を通常に戻しました",
|
||||||
|
"chat_sensitive_unlock_body": "機密会話を再び表示するには、機密チャット用パスフレーズを入力してください。",
|
||||||
|
"chat_sensitive_unlock_btn": "機密をロック解除",
|
||||||
|
"chat_sensitive_unlock_label": "機密チャット用パスフレーズを入力",
|
||||||
|
"chat_sensitive_unlock_title": "機密会話のロックを解除",
|
||||||
|
"chat_sensitive_unmark_body": "この会話は、機密チャット用パスフレーズで暗号化されなくなります。新しいメッセージは、他の会話と同じように保存されます。",
|
||||||
|
"chat_sensitive_unmark_tip": "機密 — タップして変更",
|
||||||
|
"chat_sensitive_unmark_title": "この会話を通常に戻しますか?",
|
||||||
|
"chat_sensitive_warn_1": "メッセージはこの端末上で、シードフレーズとは別の専用パスフレーズを使って暗号化されます。後でシードフレーズだけからウォレットを復元した人には、この会話は表示されません。",
|
||||||
|
"chat_sensitive_warn_dir": "ここで送信したメッセージは完全に保護されます。受信したメッセージはブロックチェーン上に残っているため、あなたのシードフレーズと別のソフトウェアを持つ人なら、理論上は復元できる可能性があります。ただしDragonX自体が、パスフレーズなしにそれらを表示することは決してありません。",
|
||||||
|
"chat_sensitive_warn_hist": "この会話に既にあるメッセージは、チェーンから復元可能なままです。この設定は、これ以降のメッセージを完全に保護します。",
|
||||||
|
"chat_sensitive_warn_lose": "このパスフレーズを失うと、これらのメッセージは永久に失われます。シードフレーズからは復元できません。",
|
||||||
"chat_settings_done": "完了",
|
"chat_settings_done": "完了",
|
||||||
"chat_settings_section": "チャットと連絡先",
|
"chat_settings_section": "チャットと連絡先",
|
||||||
"chat_settings_tip": "チャットのカスタマイズ",
|
"chat_settings_tip": "チャットのカスタマイズ",
|
||||||
@@ -344,12 +392,14 @@
|
|||||||
"chat_toast_not_connected": "未接続 — チャットメッセージは送信されませんでした。",
|
"chat_toast_not_connected": "未接続 — チャットメッセージは送信されませんでした。",
|
||||||
"chat_toast_request_compose_failed": "連絡リクエストを作成できませんでした(アドレスまたはテキストが無効?)。",
|
"chat_toast_request_compose_failed": "連絡リクエストを作成できませんでした(アドレスまたはテキストが無効?)。",
|
||||||
"chat_toast_request_queued": "連絡リクエストを送信待ちに追加しました。",
|
"chat_toast_request_queued": "連絡リクエストを送信待ちに追加しました。",
|
||||||
|
"chat_toast_too_long": "メッセージが長すぎます。短くしてもう一度送信してください。",
|
||||||
"chat_toast_waiting_reply": "メッセージを送るには、この相手からの返信を待つ必要があります。",
|
"chat_toast_waiting_reply": "メッセージを送るには、この相手からの返信を待つ必要があります。",
|
||||||
"chat_today": "今日",
|
"chat_today": "今日",
|
||||||
"chat_ts_12h": "12時間",
|
"chat_ts_12h": "12時間",
|
||||||
"chat_ts_24h": "24時間",
|
"chat_ts_24h": "24時間",
|
||||||
"chat_ts_global": "全体設定に従う",
|
"chat_ts_global": "全体設定に従う",
|
||||||
"chat_ts_global_short": "全体",
|
"chat_ts_global_short": "全体",
|
||||||
|
"chat_txns_label": "件",
|
||||||
"chat_unblock": "ブロック解除",
|
"chat_unblock": "ブロック解除",
|
||||||
"chat_unblocked_toast": "会話のブロックを解除しました",
|
"chat_unblocked_toast": "会話のブロックを解除しました",
|
||||||
"chat_unhide": "再表示",
|
"chat_unhide": "再表示",
|
||||||
@@ -370,11 +420,20 @@
|
|||||||
"click_to_copy": "クリックしてコピー",
|
"click_to_copy": "クリックしてコピー",
|
||||||
"clock_format": "時刻形式",
|
"clock_format": "時刻形式",
|
||||||
"close": "閉じる",
|
"close": "閉じる",
|
||||||
|
"compact_needs_embedded": "コンパクト化は wallet.dat を書き直す操作で、ウォレットは内蔵ノードでのみ実行できます。現在、ご自身で起動した dragonxd に接続しています — 設定で「内蔵dragonxdを使用」をオンにするか、dragonxd を -salvagewallet 付きで再起動して、領域を回収してください。",
|
||||||
|
"compact_started": "ウォレットをコンパクト化しています — ノードを再起動し、チェーンを再スキャンします。",
|
||||||
|
"compact_switch_stop_failed": "外部ノードを停止できませんでした — まだ実行中のため、何も変更されていません。",
|
||||||
|
"compact_switching": "ノードを停止し、内蔵ノードに切り替えています…",
|
||||||
|
"compact_wallet_confirm": "コンパクト化",
|
||||||
"conf_count": "%d 確認",
|
"conf_count": "%d 確認",
|
||||||
"confirm_and_send": "確認して送金",
|
"confirm_and_send": "確認して送金",
|
||||||
"confirm_clear_ztx_title": "Z-Tx 履歴クリアの確認",
|
"confirm_clear_ztx_title": "Z-Tx 履歴クリアの確認",
|
||||||
"confirm_clear_ztx_warning1": "z-トランザクション履歴をクリアすると、ウォレットの再スキャンが実行されるまでシールド残高が0と表示される場合があります。",
|
"confirm_clear_ztx_warning1": "z-トランザクション履歴をクリアすると、ウォレットの再スキャンが実行されるまでシールド残高が0と表示される場合があります。",
|
||||||
"confirm_clear_ztx_warning2": "これが発生した場合、残高を回復するにはz-アドレスの秘密鍵を再スキャンを有効にして再インポートする必要があります。",
|
"confirm_clear_ztx_warning2": "これが発生した場合、残高を回復するにはz-アドレスの秘密鍵を再スキャンを有効にして再インポートする必要があります。",
|
||||||
|
"confirm_compact_switch_msg": "ご自身で起動した dragonxd に接続しています。コンパクト化するため、ウォレットはそのノードを停止し、独自の内蔵ノードに切り替えて、そのノード上で wallet.dat を書き直します — 再起動とブロックチェーンの再スキャンが行われるため、時間がかかる場合があります。注意: 内蔵ノードはウォレット自身のフォルダにある dragonxd のため、この操作で使用するノードが変わります。",
|
||||||
|
"confirm_compact_wallet_msg": "統合によってノートはまとまりましたが、ウォレットファイルは自動では縮小されません — 解放された領域を回収するには、ノードがファイルを書き直す必要があります。この操作ではノードが再起動され、ブロックチェーンが再スキャンされるため、大きなウォレットでは時間がかかる場合があります。",
|
||||||
|
"confirm_compact_wallet_safe": "資金と残高は安全です — 現在のウォレットのタイムスタンプ付きバックアップが保持されます。",
|
||||||
|
"confirm_compact_wallet_title": "ウォレットをコンパクト化",
|
||||||
"confirm_delete_blockchain_msg": "デーモンを停止し、すべてのブロックチェーンデータ(blocks、chainstate、peers)を削除して、最初から再同期を開始します。数時間かかる場合があります。",
|
"confirm_delete_blockchain_msg": "デーモンを停止し、すべてのブロックチェーンデータ(blocks、chainstate、peers)を削除して、最初から再同期を開始します。数時間かかる場合があります。",
|
||||||
"confirm_delete_blockchain_safe": "wallet.dat、設定、トランザクション履歴は安全で削除されません。",
|
"confirm_delete_blockchain_safe": "wallet.dat、設定、トランザクション履歴は安全で削除されません。",
|
||||||
"confirm_delete_blockchain_title": "ブロックチェーンデータを削除",
|
"confirm_delete_blockchain_title": "ブロックチェーンデータを削除",
|
||||||
@@ -505,7 +564,7 @@
|
|||||||
"console_welcome": "ObsidianDragonコンソールへようこそ",
|
"console_welcome": "ObsidianDragonコンソールへようこそ",
|
||||||
"console_zoom_in": "拡大",
|
"console_zoom_in": "拡大",
|
||||||
"console_zoom_out": "縮小",
|
"console_zoom_out": "縮小",
|
||||||
"consolidate_desc": "多数の小さな入力を1つのシールドノートにまとめます。ノート数が減るとウォレットファイルが小さくなり、プライバシーも向上します。",
|
"consolidate_desc": "受け取った支払いはそれぞれ個別の「ノート」として保存されます — 財布に入っている1枚1枚の紙幣のようなものです。ノートは時間とともに溜まり、どのノートもウィットネスデータを追加するため、ウォレットの同期が遅くなり、ファイルも大きくなり続けます。統合すると、多数の小さなノートがアドレスごとに1つにまとまります。残高は変わらず、ウォレットのスキャンは速くなり、肥大化も止まり、資金に関して明らかになる情報も減ります。",
|
||||||
"consolidate_funds_btn": "統合",
|
"consolidate_funds_btn": "統合",
|
||||||
"consolidate_title": "資金を統合",
|
"consolidate_title": "資金を統合",
|
||||||
"contact_avatar": "アバター",
|
"contact_avatar": "アバター",
|
||||||
@@ -2048,6 +2107,7 @@
|
|||||||
"tt_addr_url": "ブロックエクスプローラーでアドレスを表示するためのベース URL",
|
"tt_addr_url": "ブロックエクスプローラーでアドレスを表示するためのベース URL",
|
||||||
"tt_address_book": "クイック送信用の保存済みアドレスを管理",
|
"tt_address_book": "クイック送信用の保存済みアドレスを管理",
|
||||||
"tt_animate_avatars": "アニメーション連絡先アバター(GIF / WebP)を再生。オフでは最初のフレームのみ表示",
|
"tt_animate_avatars": "アニメーション連絡先アバター(GIF / WebP)を再生。オフでは最初のフレームのみ表示",
|
||||||
|
"tt_auto_consolidate": "ウォレットがアイドル状態の間、バックグラウンドで小さなノートを少しずつ静かに統合します。手動で統合するより時間はかかりますが、ほとんど気になりません。各アドレスの資金は分かれたままです。数千件のノートが溜まるマイニング/プールのウォレットに最適です。",
|
||||||
"tt_auto_lock": "この無操作時間後にウォレットをロック",
|
"tt_auto_lock": "この無操作時間後にウォレットをロック",
|
||||||
"tt_auto_shield": "プライバシーのため透明残高を自動的にシールドアドレスに移動",
|
"tt_auto_shield": "プライバシーのため透明残高を自動的にシールドアドレスに移動",
|
||||||
"tt_backup": "wallet.dat のバックアップを作成",
|
"tt_backup": "wallet.dat のバックアップを作成",
|
||||||
@@ -2174,7 +2234,8 @@
|
|||||||
"tt_validate": "DragonX アドレスが有効かどうかを確認",
|
"tt_validate": "DragonX アドレスが有効かどうかを確認",
|
||||||
"tt_verbose": "詳細な接続診断、デーモン状態、\\nポート所有者情報をコンソールタブに記録",
|
"tt_verbose": "詳細な接続診断、デーモン状態、\\nポート所有者情報をコンソールタブに記録",
|
||||||
"tt_view_transactions": "このアドレスに関連する取引を履歴タブに表示",
|
"tt_view_transactions": "このアドレスに関連する取引を履歴タブに表示",
|
||||||
"tt_wallet_size_warn": "シールドウォレットはノートごとのウィットネスデータによって肥大化します — 多数のノートを1つのアドレスに統合するとサイズを抑えられます。事前にバックアップしてください。",
|
"tt_wallet_size_compact": "統合によって解放されたディスク領域を回収します。Berkeley DB はファイルをその場で縮小できないため、wallet.dat をより小さいファイルに書き直し、ノードを再起動してフルの再スキャンを行います(時間がかかる場合があります)。現在のウォレットのバックアップは保持されます。",
|
||||||
|
"tt_wallet_size_warn": "支払いは1件ごとに個別のシールドノートとして届き、どのノートにもウィットネスデータが付随するため、スキャンが遅くなりウォレットファイルが大きくなり続けます。多数のノートをアドレスごとに1つへ統合すると、高速に保たれ肥大化も止まります — 資金と残高は変わりません。解放された領域がファイルに戻るのは、コンパクト化したときだけです(Berkeley DB はその場で縮小できません)。事前にシードをバックアップしてください。設定で「ノートを自動統合」をオンにすることもできます。",
|
||||||
"tt_wallets_button": "ウォレットファイルを一覧表示して切り替えます",
|
"tt_wallets_button": "ウォレットファイルを一覧表示して切り替えます",
|
||||||
"tt_website": "DragonX ウェブサイトを開く",
|
"tt_website": "DragonX ウェブサイトを開く",
|
||||||
"tt_window_opacity": "背景の不透明度(低い = デスクトップがウィンドウ越しに見える)",
|
"tt_window_opacity": "背景の不透明度(低い = デスクトップがウィンドウ越しに見える)",
|
||||||
@@ -2303,8 +2364,10 @@
|
|||||||
"wallet_restore_ok": "オリジナルのウォレットを復元しました。ノードが読み込み中です。",
|
"wallet_restore_ok": "オリジナルのウォレットを復元しました。ノードが読み込み中です。",
|
||||||
"wallet_restore_started": "オリジナルファイルを復元しています — このウィンドウを閉じないでください。",
|
"wallet_restore_started": "オリジナルファイルを復元しています — このウィンドウを閉じないでください。",
|
||||||
"wallet_restore_stop_failed": "ノードが時間内に停止しなかったため、何も変更されていません。もう一度お試しください。",
|
"wallet_restore_stop_failed": "ノードが時間内に停止しなかったため、何も変更されていません。もう一度お試しください。",
|
||||||
|
"wallet_size_auto_info": "このウォレットには大量のノート(受け取った資金の個別の断片)が溜まっており、同期が遅くなってファイルも大きくなり続けています。ノートの自動統合がバックグラウンドで静かに統合を進め、ウォレットを高速に保ちつつ肥大化を止めているため、操作は必要ありません。",
|
||||||
|
"wallet_size_compact": "ウォレットをコンパクト化…",
|
||||||
"wallet_size_consolidate": "ノートを統合…",
|
"wallet_size_consolidate": "ノートを統合…",
|
||||||
"wallet_size_warn": "このウォレットファイルは大きくなっています。ノートを統合すると、さらなる増加を抑えられます。",
|
"wallet_size_warn": "このウォレットには大量のノートが溜まっています。受け取った支払いはそれぞれ個別の「ノート」として保存され、増えるほどウォレットの同期が遅くなり、ファイルも大きくなり続けます。統合すると、ノートの数が減ってスキャンが速くなり、肥大化も止まります — 残高はまったく変わりません。",
|
||||||
"wallets_active": "アクティブ",
|
"wallets_active": "アクティブ",
|
||||||
"wallets_add": "フォルダを追加",
|
"wallets_add": "フォルダを追加",
|
||||||
"wallets_add_folder": "ウォレットファイルを検索する別のフォルダを追加:",
|
"wallets_add_folder": "ウォレットファイルを検索する別のフォルダを追加:",
|
||||||
|
|||||||
+66
-3
@@ -141,6 +141,7 @@
|
|||||||
"appx_wallet_op_running": "키 가져오기 또는 재스캔이 아직 실행 중입니다. 끝난 뒤 다시 시도하세요.",
|
"appx_wallet_op_running": "키 가져오기 또는 재스캔이 아직 실행 중입니다. 끝난 뒤 다시 시도하세요.",
|
||||||
"appx_wallet_op_shutting_down": "지갑을 종료하는 중입니다.",
|
"appx_wallet_op_shutting_down": "지갑을 종료하는 중입니다.",
|
||||||
"appx_wallet_open_failed_prefix": "지갑 열기 실패: ",
|
"appx_wallet_open_failed_prefix": "지갑 열기 실패: ",
|
||||||
|
"auto_consolidate": "노트 자동 통합",
|
||||||
"auto_shield": "채굴 자동 차폐",
|
"auto_shield": "채굴 자동 차폐",
|
||||||
"autoshield_by_node": "노드가 채굴한 코인베이스를 자동으로 보호 처리합니다.",
|
"autoshield_by_node": "노드가 채굴한 코인베이스를 자동으로 보호 처리합니다.",
|
||||||
"autoshield_off_backup_seed": "채굴한 코인베이스가 아직 자동으로 보호 처리되지 않고 있습니다. 노드가 안전하게 이를 활성화할 수 있도록 시드 문구를 백업하세요 (설정 → 백업 & 데이터).",
|
"autoshield_off_backup_seed": "채굴한 코인베이스가 아직 자동으로 보호 처리되지 않고 있습니다. 노드가 안전하게 이를 활성화할 수 있도록 시드 문구를 백업하세요 (설정 → 백업 & 데이터).",
|
||||||
@@ -276,8 +277,12 @@
|
|||||||
"chat_delete_body_prefix": "다음 상대와의 대화 로컬 사본을 삭제할까요: ",
|
"chat_delete_body_prefix": "다음 상대와의 대화 로컬 사본을 삭제할까요: ",
|
||||||
"chat_delete_body_suffix": "?",
|
"chat_delete_body_suffix": "?",
|
||||||
"chat_delete_confirm": "삭제",
|
"chat_delete_confirm": "삭제",
|
||||||
|
"chat_delete_everywhere": "모든 기기에서 삭제",
|
||||||
|
"chat_delete_everywhere_note": "다른 기기에도, 그리고 나중에 시드 문구로 복원할 때에도 이 대화를 숨기도록 알립니다. 아주 작은 온체인 신호를 전송하며(평소 채팅 수수료가 듭니다), 나중에 '복원'에서 다시 가져올 수 있습니다.",
|
||||||
|
"chat_delete_everywhere_toast": "모든 기기에서 삭제하는 중 — 신호를 보냈습니다",
|
||||||
"chat_delete_failed": "대화를 삭제하지 못했습니다 — 변경된 내용이 없습니다.",
|
"chat_delete_failed": "대화를 삭제하지 못했습니다 — 변경된 내용이 없습니다.",
|
||||||
"chat_delete_local_note": "이 기기에만 영향을 줍니다 — 메시지는 블록체인에 남아 있고 상대방도 자신의 사본을 계속 보관합니다.",
|
"chat_delete_local_note": "이 기기에만 영향을 줍니다 — 메시지는 블록체인에 남아 있고 상대방도 자신의 사본을 계속 보관합니다.",
|
||||||
|
"chat_delete_needs_sync": "모든 기기에서 삭제하려면 지갑이 완전히 동기화될 때까지 기다리세요.",
|
||||||
"chat_delete_revive_note": "\"삭제\"는 이 기기의 기록만 지웁니다. 상대방이 다시 메시지를 보내면 대화가 되살아납니다.",
|
"chat_delete_revive_note": "\"삭제\"는 이 기기의 기록만 지웁니다. 상대방이 다시 메시지를 보내면 대화가 되살아납니다.",
|
||||||
"chat_delete_title": "대화를 삭제할까요?",
|
"chat_delete_title": "대화를 삭제할까요?",
|
||||||
"chat_deleted_toast": "대화를 삭제했습니다",
|
"chat_deleted_toast": "대화를 삭제했습니다",
|
||||||
@@ -319,10 +324,24 @@
|
|||||||
"chat_opt_global_clock": "전역 시간 형식",
|
"chat_opt_global_clock": "전역 시간 형식",
|
||||||
"chat_opt_poll": "폴링 주기",
|
"chat_opt_poll": "폴링 주기",
|
||||||
"chat_opt_timestamp": "타임스탬프",
|
"chat_opt_timestamp": "타임스탬프",
|
||||||
|
"chat_parts_explain": "이 긴 메시지는 여러 조각으로 나뉜 하나의 거래로 전송됩니다. 일반 메시지만큼 빠르게 도착합니다.",
|
||||||
|
"chat_parts_explain_multi": "이 메시지는 하나의 거래에 담기에는 너무 커서 여러 거래로 나뉩니다. 받는 사람은 모든 조각이 도착한 후에야 볼 수 있으므로 전달에 시간이 더 걸리고 거래마다 네트워크 수수료가 듭니다.",
|
||||||
|
"chat_parts_label": "조각",
|
||||||
"chat_pick_contact": "연락처에서 선택…",
|
"chat_pick_contact": "연락처에서 선택…",
|
||||||
|
"chat_receiving_large": "큰 메시지를 받는 중…",
|
||||||
"chat_rename": "연락처 이름 변경",
|
"chat_rename": "연락처 이름 변경",
|
||||||
"chat_rename_hint": "연락처 이름",
|
"chat_rename_hint": "연락처 이름",
|
||||||
"chat_renamed": "연락처 이름이 변경되었습니다",
|
"chat_renamed": "연락처 이름이 변경되었습니다",
|
||||||
|
"chat_restore_btn": "복원",
|
||||||
|
"chat_restore_desc": "모든 기기에서 삭제한 대화를 여기서 다시 가져올 수 있습니다 — 이 기기에서든, 시드 문구를 가져온 뒤에든 가능합니다. 복원하면 체인에서 메시지를 다시 가져오고, 다른 기기에도 대화를 다시 표시하도록 알립니다.",
|
||||||
|
"chat_restore_manage": "복원",
|
||||||
|
"chat_restore_none": "삭제된 대화를 아직 찾지 못했습니다. 다른 기기에서 삭제한 대화가 있다면 '빠른 스캔'을 해보거나 아래에 채팅 주소를 붙여넣으세요.",
|
||||||
|
"chat_restore_paste_hint": "대화의 채팅 (z) 주소를 붙여넣으세요",
|
||||||
|
"chat_restore_paste_label": "채팅 주소로 복원",
|
||||||
|
"chat_restore_scan": "빠른 스캔",
|
||||||
|
"chat_restore_scanning": "체인에서 삭제된 대화를 스캔하는 중...",
|
||||||
|
"chat_restore_title": "대화 복원",
|
||||||
|
"chat_restore_toast": "대화를 복원하는 중 — 다음 스캔 후에 다시 표시됩니다",
|
||||||
"chat_retry": "다시 시도",
|
"chat_retry": "다시 시도",
|
||||||
"chat_search": "대화 검색",
|
"chat_search": "대화 검색",
|
||||||
"chat_sec_appearance": "모양",
|
"chat_sec_appearance": "모양",
|
||||||
@@ -331,6 +350,35 @@
|
|||||||
"chat_send": "전송",
|
"chat_send": "전송",
|
||||||
"chat_send_failed": "전송 안 됨",
|
"chat_send_failed": "전송 안 됨",
|
||||||
"chat_sending": "전송 중…",
|
"chat_sending": "전송 중…",
|
||||||
|
"chat_sensitive_btn_mark": "민감한 대화로 전환",
|
||||||
|
"chat_sensitive_btn_setup": "암호 설정 후 민감한 대화로 전환",
|
||||||
|
"chat_sensitive_btn_unlock": "잠금 해제",
|
||||||
|
"chat_sensitive_btn_unlock_mark": "잠금 해제 후 민감한 대화로 전환",
|
||||||
|
"chat_sensitive_btn_unlock_unmark": "잠금 해제 후 일반 대화로 전환",
|
||||||
|
"chat_sensitive_btn_unmark": "일반 대화로 전환",
|
||||||
|
"chat_sensitive_err_create": "민감한 대화 보관소를 만들지 못했습니다.",
|
||||||
|
"chat_sensitive_err_pass": "암호가 일치하지 않거나 비어 있습니다.",
|
||||||
|
"chat_sensitive_err_unlock": "잘못된 암호입니다.",
|
||||||
|
"chat_sensitive_lock_btn": "민감한 대화 잠그기",
|
||||||
|
"chat_sensitive_mark_tip": "민감한 대화로 전환",
|
||||||
|
"chat_sensitive_mark_title": "이 대화를 민감한 대화로 전환할까요?",
|
||||||
|
"chat_sensitive_pass2_hint": "암호 확인",
|
||||||
|
"chat_sensitive_pass_hint": "암호",
|
||||||
|
"chat_sensitive_setup_label": "민감한 대화를 위한 암호를 만드세요",
|
||||||
|
"chat_sensitive_toast_marked": "이제 민감한 대화입니다",
|
||||||
|
"chat_sensitive_toast_unlocked": "민감한 대화 잠금이 해제되었습니다",
|
||||||
|
"chat_sensitive_toast_unmarked": "더 이상 민감한 대화가 아닙니다",
|
||||||
|
"chat_sensitive_unlock_body": "민감한 대화를 다시 표시하려면 민감한 대화 암호를 입력하세요.",
|
||||||
|
"chat_sensitive_unlock_btn": "민감한 대화 잠금 해제",
|
||||||
|
"chat_sensitive_unlock_label": "민감한 대화 암호를 입력하세요",
|
||||||
|
"chat_sensitive_unlock_title": "민감한 대화 잠금 해제",
|
||||||
|
"chat_sensitive_unmark_body": "이 대화는 더 이상 민감한 대화 암호로 암호화되지 않습니다. 새 메시지는 다른 대화와 똑같이 저장됩니다.",
|
||||||
|
"chat_sensitive_unmark_tip": "민감한 대화 — 눌러서 변경",
|
||||||
|
"chat_sensitive_unmark_title": "이 대화를 일반 대화로 전환할까요?",
|
||||||
|
"chat_sensitive_warn_1": "이 대화의 메시지는 시드 문구가 아닌 별도의 암호로 이 기기에서 암호화됩니다. 나중에 시드만으로 지갑을 복원하는 사람은 이 대화를 볼 수 없습니다.",
|
||||||
|
"chat_sensitive_warn_dir": "여기서 보낸 메시지는 완전히 보호됩니다. 받은 메시지는 여전히 블록체인에 남아 있어, 이론적으로는 시드와 다른 소프트웨어를 가진 사람이 복구할 수도 있습니다 — 다만 DragonX 자체는 암호 없이는 그 메시지를 절대 표시하지 않습니다.",
|
||||||
|
"chat_sensitive_warn_hist": "이 대화에 이미 있는 메시지는 체인에서 계속 복구될 수 있습니다. 이렇게 하면 지금부터의 메시지는 완전히 보호됩니다.",
|
||||||
|
"chat_sensitive_warn_lose": "이 암호를 잃어버리면 이 메시지는 영원히 사라집니다 — 시드로는 복구할 수 없습니다.",
|
||||||
"chat_settings_done": "완료",
|
"chat_settings_done": "완료",
|
||||||
"chat_settings_section": "채팅 및 연락처",
|
"chat_settings_section": "채팅 및 연락처",
|
||||||
"chat_settings_tip": "채팅 사용자 지정",
|
"chat_settings_tip": "채팅 사용자 지정",
|
||||||
@@ -344,12 +392,14 @@
|
|||||||
"chat_toast_not_connected": "연결되지 않음 — 채팅 메시지가 전송되지 않았습니다.",
|
"chat_toast_not_connected": "연결되지 않음 — 채팅 메시지가 전송되지 않았습니다.",
|
||||||
"chat_toast_request_compose_failed": "연락 요청을 작성할 수 없습니다 (잘못된 주소 / 텍스트?).",
|
"chat_toast_request_compose_failed": "연락 요청을 작성할 수 없습니다 (잘못된 주소 / 텍스트?).",
|
||||||
"chat_toast_request_queued": "연락 요청이 대기열에 추가되었습니다.",
|
"chat_toast_request_queued": "연락 요청이 대기열에 추가되었습니다.",
|
||||||
|
"chat_toast_too_long": "메시지가 너무 깁니다. 줄여서 다시 보내세요.",
|
||||||
"chat_toast_waiting_reply": "상대방이 답장해야 메시지를 보낼 수 있습니다.",
|
"chat_toast_waiting_reply": "상대방이 답장해야 메시지를 보낼 수 있습니다.",
|
||||||
"chat_today": "오늘",
|
"chat_today": "오늘",
|
||||||
"chat_ts_12h": "12시간",
|
"chat_ts_12h": "12시간",
|
||||||
"chat_ts_24h": "24시간",
|
"chat_ts_24h": "24시간",
|
||||||
"chat_ts_global": "전역 설정 따르기",
|
"chat_ts_global": "전역 설정 따르기",
|
||||||
"chat_ts_global_short": "전역",
|
"chat_ts_global_short": "전역",
|
||||||
|
"chat_txns_label": "건",
|
||||||
"chat_unblock": "차단 해제",
|
"chat_unblock": "차단 해제",
|
||||||
"chat_unblocked_toast": "대화 차단을 해제했습니다",
|
"chat_unblocked_toast": "대화 차단을 해제했습니다",
|
||||||
"chat_unhide": "다시 표시",
|
"chat_unhide": "다시 표시",
|
||||||
@@ -370,11 +420,20 @@
|
|||||||
"click_to_copy": "복사하려면 클릭",
|
"click_to_copy": "복사하려면 클릭",
|
||||||
"clock_format": "시간 형식",
|
"clock_format": "시간 형식",
|
||||||
"close": "닫기",
|
"close": "닫기",
|
||||||
|
"compact_needs_embedded": "압축은 wallet.dat을 다시 쓰는 작업이며, 지갑은 내장 노드에서만 이 작업을 수행할 수 있습니다. 현재 직접 실행한 dragonxd에 연결되어 있습니다. 설정에서 \"내장 dragonxd 사용\"을 켜거나, dragonxd를 -salvagewallet 옵션으로 다시 시작하여 공간을 회수하세요.",
|
||||||
|
"compact_started": "지갑을 압축하는 중입니다. 노드가 재시작되며 체인을 다시 스캔합니다.",
|
||||||
|
"compact_switch_stop_failed": "외부 노드를 중지하지 못했습니다. 노드가 여전히 실행 중이므로 변경된 것은 없습니다.",
|
||||||
|
"compact_switching": "노드를 중지하고 내장 노드로 전환하는 중…",
|
||||||
|
"compact_wallet_confirm": "압축",
|
||||||
"conf_count": "%d 확인",
|
"conf_count": "%d 확인",
|
||||||
"confirm_and_send": "확인 후 전송",
|
"confirm_and_send": "확인 후 전송",
|
||||||
"confirm_clear_ztx_title": "Z-Tx 기록 삭제 확인",
|
"confirm_clear_ztx_title": "Z-Tx 기록 삭제 확인",
|
||||||
"confirm_clear_ztx_warning1": "z-트랜잭션 기록을 삭제하면 지갑 재스캔이 수행될 때까지 차폐 잔액이 0으로 표시될 수 있습니다.",
|
"confirm_clear_ztx_warning1": "z-트랜잭션 기록을 삭제하면 지갑 재스캔이 수행될 때까지 차폐 잔액이 0으로 표시될 수 있습니다.",
|
||||||
"confirm_clear_ztx_warning2": "이런 경우, 잔액을 복구하려면 재스캔을 활성화하여 z-주소 개인키를 다시 가져와야 합니다.",
|
"confirm_clear_ztx_warning2": "이런 경우, 잔액을 복구하려면 재스캔을 활성화하여 z-주소 개인키를 다시 가져와야 합니다.",
|
||||||
|
"confirm_compact_switch_msg": "직접 실행한 dragonxd에 연결되어 있습니다. 압축하려면 지갑이 해당 노드를 중지하고 자체 내장 노드로 전환한 뒤, 그 노드에서 wallet.dat을 다시 씁니다. 이 과정에서 노드가 재시작되고 블록체인을 다시 스캔하며, 시간이 오래 걸릴 수 있습니다. 참고: 내장 노드는 지갑 폴더에 있는 dragonxd이므로, 실행하는 노드가 바뀌게 됩니다.",
|
||||||
|
"confirm_compact_wallet_msg": "통합으로 노트가 병합되었지만 지갑 파일은 저절로 줄어들지 않습니다. 비워진 공간을 회수하려면 노드가 파일을 다시 써야 합니다. 이 작업은 노드를 재시작하고 블록체인을 다시 스캔하며, 큰 지갑에서는 시간이 오래 걸릴 수 있습니다.",
|
||||||
|
"confirm_compact_wallet_safe": "자금과 잔액은 안전합니다. 현재 지갑의 타임스탬프 백업이 보관됩니다.",
|
||||||
|
"confirm_compact_wallet_title": "지갑 압축",
|
||||||
"confirm_delete_blockchain_msg": "데몬을 중지하고 모든 블록체인 데이터(blocks, chainstate, peers)를 삭제한 후 처음부터 다시 동기화합니다. 몇 시간이 걸릴 수 있습니다.",
|
"confirm_delete_blockchain_msg": "데몬을 중지하고 모든 블록체인 데이터(blocks, chainstate, peers)를 삭제한 후 처음부터 다시 동기화합니다. 몇 시간이 걸릴 수 있습니다.",
|
||||||
"confirm_delete_blockchain_safe": "wallet.dat, 설정 및 거래 내역은 안전하며 삭제되지 않습니다.",
|
"confirm_delete_blockchain_safe": "wallet.dat, 설정 및 거래 내역은 안전하며 삭제되지 않습니다.",
|
||||||
"confirm_delete_blockchain_title": "블록체인 데이터 삭제",
|
"confirm_delete_blockchain_title": "블록체인 데이터 삭제",
|
||||||
@@ -505,7 +564,7 @@
|
|||||||
"console_welcome": "ObsidianDragon 콘솔에 오신 것을 환영합니다",
|
"console_welcome": "ObsidianDragon 콘솔에 오신 것을 환영합니다",
|
||||||
"console_zoom_in": "확대",
|
"console_zoom_in": "확대",
|
||||||
"console_zoom_out": "축소",
|
"console_zoom_out": "축소",
|
||||||
"consolidate_desc": "여러 개의 작은 입력을 하나의 보호된 노트로 합칩니다. 노트 수가 줄면 지갑 파일이 작아지고 프라이버시도 향상됩니다.",
|
"consolidate_desc": "받은 결제는 각각 별도의 \"노트\"로 저장됩니다. 지갑 속의 개별 지폐와 비슷합니다. 노트는 시간이 지나면서 쌓이고, 노트마다 위트니스 데이터가 추가되어 지갑 동기화는 느려지고 지갑 파일은 계속 커집니다. 통합하면 여러 개의 작은 노트가 주소당 하나로 합쳐집니다. 잔액은 변하지 않지만 지갑 스캔은 빨라지고, 더 이상 커지지 않으며, 자금에 대해 드러나는 정보도 줄어듭니다.",
|
||||||
"consolidate_funds_btn": "통합",
|
"consolidate_funds_btn": "통합",
|
||||||
"consolidate_title": "자금 통합",
|
"consolidate_title": "자금 통합",
|
||||||
"contact_avatar": "아바타",
|
"contact_avatar": "아바타",
|
||||||
@@ -2048,6 +2107,7 @@
|
|||||||
"tt_addr_url": "블록 탐색기에서 주소를 보기 위한 기본 URL",
|
"tt_addr_url": "블록 탐색기에서 주소를 보기 위한 기본 URL",
|
||||||
"tt_address_book": "빠른 전송을 위해 저장된 주소 관리",
|
"tt_address_book": "빠른 전송을 위해 저장된 주소 관리",
|
||||||
"tt_animate_avatars": "애니메이션 연락처 아바타(GIF / WebP) 재생, 끄면 첫 프레임만 표시",
|
"tt_animate_avatars": "애니메이션 연락처 아바타(GIF / WebP) 재생, 끄면 첫 프레임만 표시",
|
||||||
|
"tt_auto_consolidate": "지갑이 유휴 상태일 때 백그라운드에서 작은 노트를 아주 작은 배치로 조용히 병합합니다. 수동 통합보다 느리지만 거의 눈에 띄지 않으며, 각 주소의 자금은 그대로 분리되어 유지됩니다. 수천 개의 노트가 쌓이는 채굴/풀 지갑에 적합합니다.",
|
||||||
"tt_auto_lock": "이 비활성 시간 후 지갑 잠금",
|
"tt_auto_lock": "이 비활성 시간 후 지갑 잠금",
|
||||||
"tt_auto_shield": "개인 정보 보호를 위해 투명 잔액을 자동으로 차폐 주소로 이동",
|
"tt_auto_shield": "개인 정보 보호를 위해 투명 잔액을 자동으로 차폐 주소로 이동",
|
||||||
"tt_backup": "wallet.dat 백업 만들기",
|
"tt_backup": "wallet.dat 백업 만들기",
|
||||||
@@ -2174,7 +2234,8 @@
|
|||||||
"tt_validate": "DragonX 주소가 유효한지 확인",
|
"tt_validate": "DragonX 주소가 유효한지 확인",
|
||||||
"tt_verbose": "콘솔 탭에 상세 연결 진단,\\n데몬 상태 및 포트 소유자 정보 기록",
|
"tt_verbose": "콘솔 탭에 상세 연결 진단,\\n데몬 상태 및 포트 소유자 정보 기록",
|
||||||
"tt_view_transactions": "이 주소와 관련된 거래를 기록 탭에 표시",
|
"tt_view_transactions": "이 주소와 관련된 거래를 기록 탭에 표시",
|
||||||
"tt_wallet_size_warn": "보호 지갑은 각 노트의 위트니스 데이터로 인해 커집니다 — 여러 노트를 하나의 주소로 병합하면 크기를 줄일 수 있습니다. 먼저 백업하세요.",
|
"tt_wallet_size_compact": "통합으로 비워진 디스크 공간을 회수합니다. Berkeley DB는 파일을 제자리에서 줄일 수 없으므로, wallet.dat을 더 작은 파일로 다시 쓰고 전체 재스캔과 함께 노드를 재시작합니다(시간이 걸릴 수 있습니다). 현재 지갑의 백업은 보관됩니다.",
|
||||||
|
"tt_wallet_size_warn": "받은 결제는 각각 별도의 차폐 노트로 저장되며, 노트마다 위트니스 데이터가 있어 스캔이 느려지고 지갑 파일이 계속 커집니다. 여러 노트를 주소당 하나로 병합하면 지갑이 빠르게 유지되고 더 이상 커지지 않으며, 자금과 잔액은 그대로입니다. 비워진 공간은 지갑을 압축할 때에만 파일에서 회수됩니다(Berkeley DB는 파일을 제자리에서 줄일 수 없습니다). 먼저 시드를 백업하세요. 설정에서 노트 자동 통합을 켤 수도 있습니다.",
|
||||||
"tt_wallets_button": "지갑 파일 목록을 보고 전환합니다",
|
"tt_wallets_button": "지갑 파일 목록을 보고 전환합니다",
|
||||||
"tt_website": "DragonX 웹사이트 열기",
|
"tt_website": "DragonX 웹사이트 열기",
|
||||||
"tt_window_opacity": "배경 불투명도 (낮을수록 = 창을 통해 바탕 화면이 보임)",
|
"tt_window_opacity": "배경 불투명도 (낮을수록 = 창을 통해 바탕 화면이 보임)",
|
||||||
@@ -2303,8 +2364,10 @@
|
|||||||
"wallet_restore_ok": "원본 지갑이 복원되었습니다. 노드가 지금 불러오고 있습니다.",
|
"wallet_restore_ok": "원본 지갑이 복원되었습니다. 노드가 지금 불러오고 있습니다.",
|
||||||
"wallet_restore_started": "원본 파일을 복원하는 중입니다 — 이 창을 닫지 마세요.",
|
"wallet_restore_started": "원본 파일을 복원하는 중입니다 — 이 창을 닫지 마세요.",
|
||||||
"wallet_restore_stop_failed": "노드가 제시간에 멈추지 않아 아무것도 변경되지 않았습니다. 다시 시도하세요.",
|
"wallet_restore_stop_failed": "노드가 제시간에 멈추지 않아 아무것도 변경되지 않았습니다. 다시 시도하세요.",
|
||||||
|
"wallet_size_auto_info": "지갑에 노트가 많이 쌓였습니다. 노트는 받은 자금의 개별 조각으로, 동기화를 느리게 만들고 파일을 계속 키웁니다. 노트 자동 통합이 백그라운드에서 조용히 노트를 병합하여 지갑을 빠르게 유지하고 더 이상 커지지 않게 하고 있으니, 따로 하실 일은 없습니다.",
|
||||||
|
"wallet_size_compact": "지갑 압축…",
|
||||||
"wallet_size_consolidate": "노트 통합…",
|
"wallet_size_consolidate": "노트 통합…",
|
||||||
"wallet_size_warn": "이 지갑 파일이 큽니다. 노트를 통합하면 추가 증가를 억제할 수 있습니다.",
|
"wallet_size_warn": "지갑에 노트가 많이 쌓였습니다. 받은 결제는 각각 별도의 \"노트\"로 저장되며, 노트가 쌓일수록 지갑 동기화가 느려지고 지갑 파일은 계속 커집니다. 통합하면 노트를 더 적은 수로 병합하여 스캔이 빨라지고 더 이상 커지지 않습니다. 잔액은 정확히 그대로 유지됩니다.",
|
||||||
"wallets_active": "활성",
|
"wallets_active": "활성",
|
||||||
"wallets_add": "폴더 추가",
|
"wallets_add": "폴더 추가",
|
||||||
"wallets_add_folder": "지갑 파일을 검색할 폴더를 추가로 지정:",
|
"wallets_add_folder": "지갑 파일을 검색할 폴더를 추가로 지정:",
|
||||||
|
|||||||
+66
-3
@@ -141,6 +141,7 @@
|
|||||||
"appx_wallet_op_running": "Ainda há uma importação de chave ou um reescaneamento em andamento. Tente novamente quando terminar.",
|
"appx_wallet_op_running": "Ainda há uma importação de chave ou um reescaneamento em andamento. Tente novamente quando terminar.",
|
||||||
"appx_wallet_op_shutting_down": "A carteira está sendo encerrada.",
|
"appx_wallet_op_shutting_down": "A carteira está sendo encerrada.",
|
||||||
"appx_wallet_open_failed_prefix": "Falha ao abrir a carteira: ",
|
"appx_wallet_open_failed_prefix": "Falha ao abrir a carteira: ",
|
||||||
|
"auto_consolidate": "Consolidar notas automaticamente",
|
||||||
"auto_shield": "Auto-blindar mineração",
|
"auto_shield": "Auto-blindar mineração",
|
||||||
"autoshield_by_node": "Seu node blinda automaticamente o coinbase minerado para você.",
|
"autoshield_by_node": "Seu node blinda automaticamente o coinbase minerado para você.",
|
||||||
"autoshield_off_backup_seed": "O coinbase minerado ainda não está sendo blindado automaticamente. Faça backup da sua frase de recuperação (Configurações → Backup e Dados) para que seu node possa ativar isso com segurança.",
|
"autoshield_off_backup_seed": "O coinbase minerado ainda não está sendo blindado automaticamente. Faça backup da sua frase de recuperação (Configurações → Backup e Dados) para que seu node possa ativar isso com segurança.",
|
||||||
@@ -276,8 +277,12 @@
|
|||||||
"chat_delete_body_prefix": "Excluir sua cópia local da conversa com ",
|
"chat_delete_body_prefix": "Excluir sua cópia local da conversa com ",
|
||||||
"chat_delete_body_suffix": "?",
|
"chat_delete_body_suffix": "?",
|
||||||
"chat_delete_confirm": "Excluir",
|
"chat_delete_confirm": "Excluir",
|
||||||
|
"chat_delete_everywhere": "Excluir em todo lugar",
|
||||||
|
"chat_delete_everywhere_note": "Também avisa seus outros dispositivos — e uma futura restauração a partir da sua frase de recuperação — para ocultar esta conversa. Isso envia um pequeno sinal on-chain (custa a taxa de chat habitual). Você pode trazê-la de volta depois em Restaurar.",
|
||||||
|
"chat_delete_everywhere_toast": "Excluindo em todo lugar — sinal enviado",
|
||||||
"chat_delete_failed": "Não foi possível excluir a conversa — nada foi alterado.",
|
"chat_delete_failed": "Não foi possível excluir a conversa — nada foi alterado.",
|
||||||
"chat_delete_local_note": "Isso afeta apenas este dispositivo — as mensagens permanecem na blockchain e a outra pessoa mantém sua cópia.",
|
"chat_delete_local_note": "Isso afeta apenas este dispositivo — as mensagens permanecem na blockchain e a outra pessoa mantém sua cópia.",
|
||||||
|
"chat_delete_needs_sync": "Aguarde a carteira sincronizar totalmente para excluir em todo lugar.",
|
||||||
"chat_delete_revive_note": "\"Excluir\" limpa o histórico neste dispositivo. Se essa pessoa enviar uma mensagem novamente, a conversa volta.",
|
"chat_delete_revive_note": "\"Excluir\" limpa o histórico neste dispositivo. Se essa pessoa enviar uma mensagem novamente, a conversa volta.",
|
||||||
"chat_delete_title": "Excluir conversa?",
|
"chat_delete_title": "Excluir conversa?",
|
||||||
"chat_deleted_toast": "Conversa excluída",
|
"chat_deleted_toast": "Conversa excluída",
|
||||||
@@ -319,10 +324,24 @@
|
|||||||
"chat_opt_global_clock": "Formato de relógio global",
|
"chat_opt_global_clock": "Formato de relógio global",
|
||||||
"chat_opt_poll": "Taxa de atualização",
|
"chat_opt_poll": "Taxa de atualização",
|
||||||
"chat_opt_timestamp": "Carimbos de data/hora",
|
"chat_opt_timestamp": "Carimbos de data/hora",
|
||||||
|
"chat_parts_explain": "Esta mensagem longa é enviada como uma única transação dividida nestas partes — ela chega tão rápido quanto uma mensagem normal.",
|
||||||
|
"chat_parts_explain_multi": "Esta mensagem é grande demais para uma única transação, por isso é dividida em várias. O destinatário a vê somente depois que todas as partes chegam, então leva mais tempo para ser entregue e custa uma taxa de rede por transação.",
|
||||||
|
"chat_parts_label": "partes",
|
||||||
"chat_pick_contact": "Escolher dos contatos…",
|
"chat_pick_contact": "Escolher dos contatos…",
|
||||||
|
"chat_receiving_large": "Recebendo uma mensagem grande…",
|
||||||
"chat_rename": "Renomear contato",
|
"chat_rename": "Renomear contato",
|
||||||
"chat_rename_hint": "Nome do contato",
|
"chat_rename_hint": "Nome do contato",
|
||||||
"chat_renamed": "Contato renomeado",
|
"chat_renamed": "Contato renomeado",
|
||||||
|
"chat_restore_btn": "Restaurar",
|
||||||
|
"chat_restore_desc": "Conversas que você excluiu em todo lugar podem ser trazidas de volta aqui — neste dispositivo ou após importar sua frase de recuperação. Restaurar reimporta as mensagens a partir da chain e avisa seus outros dispositivos para exibi-la novamente.",
|
||||||
|
"chat_restore_manage": "Restaurar",
|
||||||
|
"chat_restore_none": "Nenhuma conversa excluída encontrada ainda. Se você excluiu uma em outro dispositivo, tente o Escaneamento rápido ou cole o endereço de chat dela abaixo.",
|
||||||
|
"chat_restore_paste_hint": "Cole o endereço de chat (z) da conversa",
|
||||||
|
"chat_restore_paste_label": "Restaurar pelo endereço de chat",
|
||||||
|
"chat_restore_scan": "Escaneamento rápido",
|
||||||
|
"chat_restore_scanning": "Escaneando a chain em busca de conversas excluídas...",
|
||||||
|
"chat_restore_title": "Restaurar conversas",
|
||||||
|
"chat_restore_toast": "Restaurando a conversa — ela reaparece após o próximo escaneamento",
|
||||||
"chat_retry": "Tentar novamente",
|
"chat_retry": "Tentar novamente",
|
||||||
"chat_search": "Pesquisar conversas",
|
"chat_search": "Pesquisar conversas",
|
||||||
"chat_sec_appearance": "APARÊNCIA",
|
"chat_sec_appearance": "APARÊNCIA",
|
||||||
@@ -331,6 +350,35 @@
|
|||||||
"chat_send": "Enviar",
|
"chat_send": "Enviar",
|
||||||
"chat_send_failed": "não enviada",
|
"chat_send_failed": "não enviada",
|
||||||
"chat_sending": "enviando…",
|
"chat_sending": "enviando…",
|
||||||
|
"chat_sensitive_btn_mark": "Tornar confidencial",
|
||||||
|
"chat_sensitive_btn_setup": "Configurar e tornar confidencial",
|
||||||
|
"chat_sensitive_btn_unlock": "Desbloquear",
|
||||||
|
"chat_sensitive_btn_unlock_mark": "Desbloquear e tornar confidencial",
|
||||||
|
"chat_sensitive_btn_unlock_unmark": "Desbloquear e tornar normal",
|
||||||
|
"chat_sensitive_btn_unmark": "Tornar normal",
|
||||||
|
"chat_sensitive_err_create": "Não foi possível criar o cofre das conversas confidenciais.",
|
||||||
|
"chat_sensitive_err_pass": "As senhas não coincidem ou estão vazias.",
|
||||||
|
"chat_sensitive_err_unlock": "Senha incorreta.",
|
||||||
|
"chat_sensitive_lock_btn": "Bloquear confidenciais",
|
||||||
|
"chat_sensitive_mark_tip": "Tornar confidencial",
|
||||||
|
"chat_sensitive_mark_title": "Tornar esta conversa confidencial?",
|
||||||
|
"chat_sensitive_pass2_hint": "Confirmar senha",
|
||||||
|
"chat_sensitive_pass_hint": "Senha",
|
||||||
|
"chat_sensitive_setup_label": "Crie uma senha para as conversas confidenciais",
|
||||||
|
"chat_sensitive_toast_marked": "A conversa agora é confidencial",
|
||||||
|
"chat_sensitive_toast_unlocked": "Conversas confidenciais desbloqueadas",
|
||||||
|
"chat_sensitive_toast_unmarked": "A conversa não é mais confidencial",
|
||||||
|
"chat_sensitive_unlock_body": "Digite a sua senha de conversas confidenciais para exibir novamente as suas conversas confidenciais.",
|
||||||
|
"chat_sensitive_unlock_btn": "Desbloquear confidenciais",
|
||||||
|
"chat_sensitive_unlock_label": "Digite a sua senha de conversas confidenciais",
|
||||||
|
"chat_sensitive_unlock_title": "Desbloquear conversas confidenciais",
|
||||||
|
"chat_sensitive_unmark_body": "Esta conversa deixará de ser criptografada com a sua senha de conversas confidenciais. As novas mensagens serão armazenadas como em qualquer outra conversa.",
|
||||||
|
"chat_sensitive_unmark_tip": "Confidencial — toque para alterar",
|
||||||
|
"chat_sensitive_unmark_title": "Tornar esta conversa normal?",
|
||||||
|
"chat_sensitive_warn_1": "As mensagens dela serão criptografadas neste dispositivo com uma senha separada — não a sua frase de recuperação. Quem mais tarde restaurar sua carteira apenas a partir da sua semente não verá esta conversa.",
|
||||||
|
"chat_sensitive_warn_dir": "As mensagens que você envia aqui ficam totalmente protegidas. As mensagens recebidas continuam na blockchain e poderiam, em teoria, ser recuperadas por alguém que tenha a sua semente e outros softwares — o próprio DragonX nunca as exibirá sem a sua senha.",
|
||||||
|
"chat_sensitive_warn_hist": "As mensagens que já estão nesta conversa continuam recuperáveis a partir da chain; isso protege totalmente as mensagens daqui em diante.",
|
||||||
|
"chat_sensitive_warn_lose": "Se você perder esta senha, estas mensagens desaparecem para sempre — elas não podem ser recuperadas a partir da sua semente.",
|
||||||
"chat_settings_done": "Concluído",
|
"chat_settings_done": "Concluído",
|
||||||
"chat_settings_section": "CHAT E CONTATOS",
|
"chat_settings_section": "CHAT E CONTATOS",
|
||||||
"chat_settings_tip": "Personalizar chat",
|
"chat_settings_tip": "Personalizar chat",
|
||||||
@@ -344,12 +392,14 @@
|
|||||||
"chat_toast_not_connected": "Não conectado — mensagem de chat não enviada.",
|
"chat_toast_not_connected": "Não conectado — mensagem de chat não enviada.",
|
||||||
"chat_toast_request_compose_failed": "Não foi possível compor a solicitação de contato (endereço / texto inválido?).",
|
"chat_toast_request_compose_failed": "Não foi possível compor a solicitação de contato (endereço / texto inválido?).",
|
||||||
"chat_toast_request_queued": "Solicitação de contato na fila.",
|
"chat_toast_request_queued": "Solicitação de contato na fila.",
|
||||||
|
"chat_toast_too_long": "Mensagem muito longa — encurte-a e envie novamente.",
|
||||||
"chat_toast_waiting_reply": "Aguardando a resposta do contato antes que você possa enviar mensagens a ele.",
|
"chat_toast_waiting_reply": "Aguardando a resposta do contato antes que você possa enviar mensagens a ele.",
|
||||||
"chat_today": "Hoje",
|
"chat_today": "Hoje",
|
||||||
"chat_ts_12h": "12 horas",
|
"chat_ts_12h": "12 horas",
|
||||||
"chat_ts_24h": "24 horas",
|
"chat_ts_24h": "24 horas",
|
||||||
"chat_ts_global": "Seguir global",
|
"chat_ts_global": "Seguir global",
|
||||||
"chat_ts_global_short": "Global",
|
"chat_ts_global_short": "Global",
|
||||||
|
"chat_txns_label": "tx",
|
||||||
"chat_unblock": "Desbloquear",
|
"chat_unblock": "Desbloquear",
|
||||||
"chat_unblocked_toast": "Conversa desbloqueada",
|
"chat_unblocked_toast": "Conversa desbloqueada",
|
||||||
"chat_unhide": "Mostrar",
|
"chat_unhide": "Mostrar",
|
||||||
@@ -370,11 +420,20 @@
|
|||||||
"click_to_copy": "Clique para copiar",
|
"click_to_copy": "Clique para copiar",
|
||||||
"clock_format": "Formato de hora",
|
"clock_format": "Formato de hora",
|
||||||
"close": "Fechar",
|
"close": "Fechar",
|
||||||
|
"compact_needs_embedded": "Compactar reescreve o wallet.dat, o que a carteira só consegue fazer com o seu nó integrado. Você está conectado a um dragonxd que iniciou por conta própria — ative \"Usar dragonxd integrado\" em Ajustes, ou reinicie o seu dragonxd com -salvagewallet para recuperar o espaço.",
|
||||||
|
"compact_started": "Compactando a carteira — o nó está reiniciando e vai escanear a cadeia novamente.",
|
||||||
|
"compact_switch_stop_failed": "Não foi possível parar o seu nó externo — ele ainda está em execução, então nada foi alterado.",
|
||||||
|
"compact_switching": "Parando o seu nó e mudando para o integrado…",
|
||||||
|
"compact_wallet_confirm": "Compactar",
|
||||||
"conf_count": "%d conf.",
|
"conf_count": "%d conf.",
|
||||||
"confirm_and_send": "Confirmar & Enviar",
|
"confirm_and_send": "Confirmar & Enviar",
|
||||||
"confirm_clear_ztx_title": "Confirmar limpeza do histórico Z-Tx",
|
"confirm_clear_ztx_title": "Confirmar limpeza do histórico Z-Tx",
|
||||||
"confirm_clear_ztx_warning1": "Limpar o histórico de z-transações pode fazer com que seu saldo blindado apareça como 0 até que um reescaneamento da carteira seja realizado.",
|
"confirm_clear_ztx_warning1": "Limpar o histórico de z-transações pode fazer com que seu saldo blindado apareça como 0 até que um reescaneamento da carteira seja realizado.",
|
||||||
"confirm_clear_ztx_warning2": "Se isso acontecer, você precisará reimportar as chaves privadas do seu endereço z com reescaneamento habilitado para recuperar seu saldo.",
|
"confirm_clear_ztx_warning2": "Se isso acontecer, você precisará reimportar as chaves privadas do seu endereço z com reescaneamento habilitado para recuperar seu saldo.",
|
||||||
|
"confirm_compact_switch_msg": "Você está conectado a um dragonxd que iniciou por conta própria. Para compactar, a carteira vai parar esse nó, mudar para o seu próprio nó integrado e reescrever o wallet.dat nele — reiniciando e escaneando o blockchain novamente, o que pode demorar. Observação: o nó integrado é o dragonxd da pasta da própria carteira, então isso muda qual nó você usa.",
|
||||||
|
"confirm_compact_wallet_msg": "A consolidação mesclou suas notas, mas o arquivo da carteira não encolhe sozinho — o nó precisa reescrevê-lo para recuperar o espaço liberado. Isso reinicia o nó e escaneia o blockchain novamente, o que pode demorar em uma carteira grande.",
|
||||||
|
"confirm_compact_wallet_safe": "Seus fundos e saldo estão seguros — um backup com data e hora da carteira atual é mantido.",
|
||||||
|
"confirm_compact_wallet_title": "Compactar carteira",
|
||||||
"confirm_delete_blockchain_msg": "Isso irá parar o daemon, excluir todos os dados da blockchain (blocks, chainstate, peers) e iniciar uma nova sincronização do zero. Isso pode levar várias horas.",
|
"confirm_delete_blockchain_msg": "Isso irá parar o daemon, excluir todos os dados da blockchain (blocks, chainstate, peers) e iniciar uma nova sincronização do zero. Isso pode levar várias horas.",
|
||||||
"confirm_delete_blockchain_safe": "Seu wallet.dat, configuração e histórico de transações estão seguros e não serão excluídos.",
|
"confirm_delete_blockchain_safe": "Seu wallet.dat, configuração e histórico de transações estão seguros e não serão excluídos.",
|
||||||
"confirm_delete_blockchain_title": "Excluir Dados da Blockchain",
|
"confirm_delete_blockchain_title": "Excluir Dados da Blockchain",
|
||||||
@@ -505,7 +564,7 @@
|
|||||||
"console_welcome": "Bem-vindo ao Console ObsidianDragon",
|
"console_welcome": "Bem-vindo ao Console ObsidianDragon",
|
||||||
"console_zoom_in": "Aumentar zoom",
|
"console_zoom_in": "Aumentar zoom",
|
||||||
"console_zoom_out": "Diminuir zoom",
|
"console_zoom_out": "Diminuir zoom",
|
||||||
"consolidate_desc": "Combine muitas entradas pequenas em uma única nota blindada. Menos notas significa um arquivo de carteira menor e mais privacidade.",
|
"consolidate_desc": "Cada pagamento que você recebe é armazenado como uma \"nota\" separada — como uma cédula individual em uma carteira. Elas se acumulam com o tempo, e cada nota adiciona dados de testemunha que deixam a carteira mais lenta para sincronizar e fazem com que continue crescendo. Consolidar combina muitas notas pequenas em uma por endereço: seu saldo não muda, mas a carteira escaneia mais rápido, para de crescer e revela menos sobre os seus fundos.",
|
||||||
"consolidate_funds_btn": "Consolidar",
|
"consolidate_funds_btn": "Consolidar",
|
||||||
"consolidate_title": "Consolidar fundos",
|
"consolidate_title": "Consolidar fundos",
|
||||||
"contact_avatar": "AVATAR",
|
"contact_avatar": "AVATAR",
|
||||||
@@ -2048,6 +2107,7 @@
|
|||||||
"tt_addr_url": "URL base para visualizar endereços em um explorador de blocos",
|
"tt_addr_url": "URL base para visualizar endereços em um explorador de blocos",
|
||||||
"tt_address_book": "Gerenciar endereços salvos para envio rápido",
|
"tt_address_book": "Gerenciar endereços salvos para envio rápido",
|
||||||
"tt_animate_avatars": "Reproduz avatares de contato animados (GIF / WebP); desativado mostra só o primeiro quadro",
|
"tt_animate_avatars": "Reproduz avatares de contato animados (GIF / WebP); desativado mostra só o primeiro quadro",
|
||||||
|
"tt_auto_consolidate": "Mescla discretamente as notas pequenas em segundo plano enquanto a carteira está ociosa, em lotes minúsculos. Mais lento que uma consolidação manual, mas quase imperceptível, e os fundos de cada endereço continuam separados. Ideal para carteiras de mineração/pool que acumulam milhares de notas.",
|
||||||
"tt_auto_lock": "Bloquear carteira após este tempo de inatividade",
|
"tt_auto_lock": "Bloquear carteira após este tempo de inatividade",
|
||||||
"tt_auto_shield": "Mover automaticamente o saldo transparente para endereços blindados para privacidade",
|
"tt_auto_shield": "Mover automaticamente o saldo transparente para endereços blindados para privacidade",
|
||||||
"tt_backup": "Criar um backup do seu wallet.dat",
|
"tt_backup": "Criar um backup do seu wallet.dat",
|
||||||
@@ -2174,7 +2234,8 @@
|
|||||||
"tt_validate": "Verificar se um endereço DragonX é válido",
|
"tt_validate": "Verificar se um endereço DragonX é válido",
|
||||||
"tt_verbose": "Registrar diagnósticos detalhados de conexão,\\nestado do daemon e info de proprietário de porta\\nna aba Console",
|
"tt_verbose": "Registrar diagnósticos detalhados de conexão,\\nestado do daemon e info de proprietário de porta\\nna aba Console",
|
||||||
"tt_view_transactions": "Mostrar as transações que envolvem este endereço na aba Histórico",
|
"tt_view_transactions": "Mostrar as transações que envolvem este endereço na aba Histórico",
|
||||||
"tt_wallet_size_warn": "Carteiras blindadas crescem com os dados de testemunha de cada nota — mesclar várias notas em um único endereço reduz esse tamanho. Faça backup antes.",
|
"tt_wallet_size_compact": "Recupere o espaço em disco liberado pela consolidação. O Berkeley DB não consegue encolher o arquivo no próprio local, então isto reescreve o wallet.dat em um arquivo menor e reinicia o nó com um reescaneamento completo (pode demorar). Um backup da carteira atual é mantido.",
|
||||||
|
"tt_wallet_size_warn": "Cada pagamento chega como uma nota blindada separada, e cada nota carrega dados de testemunha que deixam as varreduras lentas e fazem o arquivo da carteira continuar crescendo. Mesclar muitas notas em uma por endereço mantém a carteira rápida e interrompe o crescimento — seus fundos e saldo não mudam. O arquivo só recupera o espaço liberado quando você o Compacta (o Berkeley DB não consegue encolher no próprio arquivo). Faça backup da sua seed primeiro. Você também pode ativar a opção Consolidar notas automaticamente em Ajustes.",
|
||||||
"tt_wallets_button": "Liste os arquivos de carteira e alterne entre eles",
|
"tt_wallets_button": "Liste os arquivos de carteira e alterne entre eles",
|
||||||
"tt_website": "Abrir o site do DragonX",
|
"tt_website": "Abrir o site do DragonX",
|
||||||
"tt_window_opacity": "Opacidade do fundo (menor = área de trabalho visível através da janela)",
|
"tt_window_opacity": "Opacidade do fundo (menor = área de trabalho visível através da janela)",
|
||||||
@@ -2303,8 +2364,10 @@
|
|||||||
"wallet_restore_ok": "Carteira original restaurada. O node está carregando-a agora.",
|
"wallet_restore_ok": "Carteira original restaurada. O node está carregando-a agora.",
|
||||||
"wallet_restore_started": "Restaurando seu arquivo original — por favor não feche esta janela.",
|
"wallet_restore_started": "Restaurando seu arquivo original — por favor não feche esta janela.",
|
||||||
"wallet_restore_stop_failed": "O node não parou a tempo, então nada foi alterado. Tente novamente.",
|
"wallet_restore_stop_failed": "O node não parou a tempo, então nada foi alterado. Tente novamente.",
|
||||||
|
"wallet_size_auto_info": "Sua carteira acumulou muitas notas — partes separadas dos fundos recebidos que deixam a sincronização lenta e fazem o arquivo continuar crescendo. A consolidação automática as está mesclando discretamente em segundo plano para manter a carteira rápida e impedir que ela cresça, então não há nada que você precise fazer.",
|
||||||
|
"wallet_size_compact": "Compactar carteira…",
|
||||||
"wallet_size_consolidate": "Consolidar notas…",
|
"wallet_size_consolidate": "Consolidar notas…",
|
||||||
"wallet_size_warn": "Este arquivo de carteira está grande. Consolidar suas notas pode conter o crescimento.",
|
"wallet_size_warn": "Sua carteira acumulou muitas notas. Cada pagamento que você recebe é armazenado como uma \"nota\" separada; conforme se acumulam, elas deixam a carteira mais lenta para sincronizar e fazem com que continue crescendo. Consolidar as mescla em menos notas para que ela escaneie mais rápido e pare de crescer — seu saldo permanece exatamente o mesmo.",
|
||||||
"wallets_active": "Ativa",
|
"wallets_active": "Ativa",
|
||||||
"wallets_add": "Adicionar pasta",
|
"wallets_add": "Adicionar pasta",
|
||||||
"wallets_add_folder": "Verificar também outra pasta em busca de arquivos de carteira:",
|
"wallets_add_folder": "Verificar também outra pasta em busca de arquivos de carteira:",
|
||||||
|
|||||||
+66
-3
@@ -141,6 +141,7 @@
|
|||||||
"appx_wallet_op_running": "Импорт ключа или повторное сканирование ещё выполняется. Повторите попытку, когда оно завершится.",
|
"appx_wallet_op_running": "Импорт ключа или повторное сканирование ещё выполняется. Повторите попытку, когда оно завершится.",
|
||||||
"appx_wallet_op_shutting_down": "Кошелёк завершает работу.",
|
"appx_wallet_op_shutting_down": "Кошелёк завершает работу.",
|
||||||
"appx_wallet_open_failed_prefix": "Не удалось открыть кошелёк: ",
|
"appx_wallet_open_failed_prefix": "Не удалось открыть кошелёк: ",
|
||||||
|
"auto_consolidate": "Автообъединение заметок",
|
||||||
"auto_shield": "Авто-экранирование майнинга",
|
"auto_shield": "Авто-экранирование майнинга",
|
||||||
"autoshield_by_node": "Ваш узел автоматически защищает добытый coinbase за вас.",
|
"autoshield_by_node": "Ваш узел автоматически защищает добытый coinbase за вас.",
|
||||||
"autoshield_off_backup_seed": "Добытый coinbase пока не защищается автоматически. Сделайте резервную копию сид-фразы (Настройки → Резервное копирование и данные), чтобы узел мог безопасно включить эту функцию.",
|
"autoshield_off_backup_seed": "Добытый coinbase пока не защищается автоматически. Сделайте резервную копию сид-фразы (Настройки → Резервное копирование и данные), чтобы узел мог безопасно включить эту функцию.",
|
||||||
@@ -276,8 +277,12 @@
|
|||||||
"chat_delete_body_prefix": "Удалить локальную копию разговора с ",
|
"chat_delete_body_prefix": "Удалить локальную копию разговора с ",
|
||||||
"chat_delete_body_suffix": "?",
|
"chat_delete_body_suffix": "?",
|
||||||
"chat_delete_confirm": "Удалить",
|
"chat_delete_confirm": "Удалить",
|
||||||
|
"chat_delete_everywhere": "Удалить везде",
|
||||||
|
"chat_delete_everywhere_note": "Также сообщает другим вашим устройствам — и кошельку, который вы позже восстановите из сид-фразы, — что этот разговор нужно скрыть. Отправляется крошечный сигнал в блокчейне (взимается обычная комиссия чата). Позже вы сможете вернуть его в разделе «Восстановить».",
|
||||||
|
"chat_delete_everywhere_toast": "Удаление везде — сигнал отправлен",
|
||||||
"chat_delete_failed": "Не удалось удалить разговор — ничего не изменено.",
|
"chat_delete_failed": "Не удалось удалить разговор — ничего не изменено.",
|
||||||
"chat_delete_local_note": "Это влияет только на это устройство — сообщения остаются в блокчейне, и у собеседника сохранится своя копия.",
|
"chat_delete_local_note": "Это влияет только на это устройство — сообщения остаются в блокчейне, и у собеседника сохранится своя копия.",
|
||||||
|
"chat_delete_needs_sync": "Дождитесь полной синхронизации кошелька, чтобы удалить везде.",
|
||||||
"chat_delete_revive_note": "«Удалить» очищает историю на этом устройстве. Если вам снова напишут, разговор появится снова.",
|
"chat_delete_revive_note": "«Удалить» очищает историю на этом устройстве. Если вам снова напишут, разговор появится снова.",
|
||||||
"chat_delete_title": "Удалить разговор?",
|
"chat_delete_title": "Удалить разговор?",
|
||||||
"chat_deleted_toast": "Разговор удалён",
|
"chat_deleted_toast": "Разговор удалён",
|
||||||
@@ -319,10 +324,24 @@
|
|||||||
"chat_opt_global_clock": "Глобальный формат времени",
|
"chat_opt_global_clock": "Глобальный формат времени",
|
||||||
"chat_opt_poll": "Частота опроса",
|
"chat_opt_poll": "Частота опроса",
|
||||||
"chat_opt_timestamp": "Метки времени",
|
"chat_opt_timestamp": "Метки времени",
|
||||||
|
"chat_parts_explain": "Это длинное сообщение отправляется как одна транзакция, разделённая на эти части, — оно доходит так же быстро, как обычное сообщение.",
|
||||||
|
"chat_parts_explain_multi": "Это сообщение слишком велико для одной транзакции, поэтому оно разбивается на несколько. Получатель увидит его только после того, как придут все части, поэтому доставка занимает больше времени и за каждую транзакцию взимается сетевая комиссия.",
|
||||||
|
"chat_parts_label": "частей",
|
||||||
"chat_pick_contact": "Выбрать из контактов…",
|
"chat_pick_contact": "Выбрать из контактов…",
|
||||||
|
"chat_receiving_large": "Получение большого сообщения…",
|
||||||
"chat_rename": "Переименовать контакт",
|
"chat_rename": "Переименовать контакт",
|
||||||
"chat_rename_hint": "Имя контакта",
|
"chat_rename_hint": "Имя контакта",
|
||||||
"chat_renamed": "Контакт переименован",
|
"chat_renamed": "Контакт переименован",
|
||||||
|
"chat_restore_btn": "Восстановить",
|
||||||
|
"chat_restore_desc": "Разговоры, которые вы удалили везде, можно вернуть здесь — на этом устройстве или после импорта сид-фразы. Восстановление заново импортирует сообщения из блокчейна и сообщает другим вашим устройствам, что разговор снова нужно показывать.",
|
||||||
|
"chat_restore_manage": "Восстановить",
|
||||||
|
"chat_restore_none": "Удалённых разговоров пока не найдено. Если вы удалили разговор на другом устройстве, попробуйте «Быстрое сканирование» или вставьте его адрес чата ниже.",
|
||||||
|
"chat_restore_paste_hint": "Вставьте защищённый (z) адрес этого разговора",
|
||||||
|
"chat_restore_paste_label": "Восстановить по адресу чата",
|
||||||
|
"chat_restore_scan": "Быстрое сканирование",
|
||||||
|
"chat_restore_scanning": "Сканирование блокчейна в поиске удалённых разговоров…",
|
||||||
|
"chat_restore_title": "Восстановление разговоров",
|
||||||
|
"chat_restore_toast": "Восстановление разговора — он появится снова после следующего сканирования",
|
||||||
"chat_retry": "Повторить",
|
"chat_retry": "Повторить",
|
||||||
"chat_search": "Поиск разговоров",
|
"chat_search": "Поиск разговоров",
|
||||||
"chat_sec_appearance": "ВИД",
|
"chat_sec_appearance": "ВИД",
|
||||||
@@ -331,6 +350,35 @@
|
|||||||
"chat_send": "Отправить",
|
"chat_send": "Отправить",
|
||||||
"chat_send_failed": "не отправлено",
|
"chat_send_failed": "не отправлено",
|
||||||
"chat_sending": "отправка…",
|
"chat_sending": "отправка…",
|
||||||
|
"chat_sensitive_btn_mark": "Сделать конфиденциальным",
|
||||||
|
"chat_sensitive_btn_setup": "Настроить и сделать конфиденциальным",
|
||||||
|
"chat_sensitive_btn_unlock": "Разблокировать",
|
||||||
|
"chat_sensitive_btn_unlock_mark": "Разблокировать и сделать конфиденциальным",
|
||||||
|
"chat_sensitive_btn_unlock_unmark": "Разблокировать и сделать обычным",
|
||||||
|
"chat_sensitive_btn_unmark": "Сделать обычным",
|
||||||
|
"chat_sensitive_err_create": "Не удалось создать хранилище конфиденциальных разговоров.",
|
||||||
|
"chat_sensitive_err_pass": "Пароли не совпадают или не заполнены.",
|
||||||
|
"chat_sensitive_err_unlock": "Неверный пароль.",
|
||||||
|
"chat_sensitive_lock_btn": "Заблокировать конфиденциальные",
|
||||||
|
"chat_sensitive_mark_tip": "Сделать конфиденциальным",
|
||||||
|
"chat_sensitive_mark_title": "Сделать этот разговор конфиденциальным?",
|
||||||
|
"chat_sensitive_pass2_hint": "Подтвердите пароль",
|
||||||
|
"chat_sensitive_pass_hint": "Пароль",
|
||||||
|
"chat_sensitive_setup_label": "Создайте пароль для конфиденциальных разговоров",
|
||||||
|
"chat_sensitive_toast_marked": "Разговор теперь конфиденциальный",
|
||||||
|
"chat_sensitive_toast_unlocked": "Конфиденциальные разговоры разблокированы",
|
||||||
|
"chat_sensitive_toast_unmarked": "Разговор больше не конфиденциальный",
|
||||||
|
"chat_sensitive_unlock_body": "Введите пароль для конфиденциальных разговоров, чтобы снова показать их.",
|
||||||
|
"chat_sensitive_unlock_btn": "Разблокировать конфиденциальные",
|
||||||
|
"chat_sensitive_unlock_label": "Введите пароль для конфиденциальных разговоров",
|
||||||
|
"chat_sensitive_unlock_title": "Разблокировать конфиденциальные разговоры",
|
||||||
|
"chat_sensitive_unmark_body": "Этот разговор больше не будет зашифрован вашим паролем для конфиденциальных разговоров. Новые сообщения будут храниться как в любом другом разговоре.",
|
||||||
|
"chat_sensitive_unmark_tip": "Конфиденциальный — нажмите, чтобы изменить",
|
||||||
|
"chat_sensitive_unmark_title": "Сделать этот разговор обычным?",
|
||||||
|
"chat_sensitive_warn_1": "Сообщения в нём будут зашифрованы на этом устройстве отдельным паролем, а не вашей сид-фразой. Любой, кто позже восстановит ваш кошелёк только по сид-фразе, не увидит этот разговор.",
|
||||||
|
"chat_sensitive_warn_dir": "Отправленные вами здесь сообщения защищены полностью. Полученные сообщения по-прежнему находятся в блокчейне, и теоретически их может восстановить тот, у кого есть ваша сид-фраза и другое ПО, — но сам DragonX никогда не покажет их без вашего пароля.",
|
||||||
|
"chat_sensitive_warn_hist": "Сообщения, которые уже есть в этом разговоре, по-прежнему можно восстановить из блокчейна; полная защита действует для сообщений, начиная с этого момента.",
|
||||||
|
"chat_sensitive_warn_lose": "Если вы потеряете этот пароль, эти сообщения пропадут навсегда — их нельзя восстановить по сид-фразе.",
|
||||||
"chat_settings_done": "Готово",
|
"chat_settings_done": "Готово",
|
||||||
"chat_settings_section": "ЧАТ И КОНТАКТЫ",
|
"chat_settings_section": "ЧАТ И КОНТАКТЫ",
|
||||||
"chat_settings_tip": "Настройка чата",
|
"chat_settings_tip": "Настройка чата",
|
||||||
@@ -344,12 +392,14 @@
|
|||||||
"chat_toast_not_connected": "Нет подключения — сообщение не отправлено.",
|
"chat_toast_not_connected": "Нет подключения — сообщение не отправлено.",
|
||||||
"chat_toast_request_compose_failed": "Не удалось составить запрос контакта (неверный адрес / текст?).",
|
"chat_toast_request_compose_failed": "Не удалось составить запрос контакта (неверный адрес / текст?).",
|
||||||
"chat_toast_request_queued": "Запрос контакта поставлен в очередь.",
|
"chat_toast_request_queued": "Запрос контакта поставлен в очередь.",
|
||||||
|
"chat_toast_too_long": "Сообщение слишком длинное — сократите его и отправьте снова.",
|
||||||
"chat_toast_waiting_reply": "Ожидание ответа от контакта — вы сможете писать ему только после этого.",
|
"chat_toast_waiting_reply": "Ожидание ответа от контакта — вы сможете писать ему только после этого.",
|
||||||
"chat_today": "Сегодня",
|
"chat_today": "Сегодня",
|
||||||
"chat_ts_12h": "12 часов",
|
"chat_ts_12h": "12 часов",
|
||||||
"chat_ts_24h": "24 часа",
|
"chat_ts_24h": "24 часа",
|
||||||
"chat_ts_global": "Как глобально",
|
"chat_ts_global": "Как глобально",
|
||||||
"chat_ts_global_short": "Общий",
|
"chat_ts_global_short": "Общий",
|
||||||
|
"chat_txns_label": "транз.",
|
||||||
"chat_unblock": "Разблокировать",
|
"chat_unblock": "Разблокировать",
|
||||||
"chat_unblocked_toast": "Разговор разблокирован",
|
"chat_unblocked_toast": "Разговор разблокирован",
|
||||||
"chat_unhide": "Показать",
|
"chat_unhide": "Показать",
|
||||||
@@ -370,11 +420,20 @@
|
|||||||
"click_to_copy": "Нажмите для копирования",
|
"click_to_copy": "Нажмите для копирования",
|
||||||
"clock_format": "Формат времени",
|
"clock_format": "Формат времени",
|
||||||
"close": "Закрыть",
|
"close": "Закрыть",
|
||||||
|
"compact_needs_embedded": "Сжатие перезаписывает wallet.dat, а кошелёк может сделать это только с помощью встроенного узла. Вы подключены к dragonxd, который запустили сами — включите «Использовать встроенный dragonxd» в разделе «Настройки» или перезапустите свой dragonxd с параметром -salvagewallet, чтобы вернуть место.",
|
||||||
|
"compact_started": "Сжатие кошелька — узел перезапускается и заново просканирует блокчейн.",
|
||||||
|
"compact_switch_stop_failed": "Не удалось остановить ваш внешний узел — он всё ещё работает, поэтому ничего не изменено.",
|
||||||
|
"compact_switching": "Остановка вашего узла и переключение на встроенный…",
|
||||||
|
"compact_wallet_confirm": "Сжать",
|
||||||
"conf_count": "%d подтв.",
|
"conf_count": "%d подтв.",
|
||||||
"confirm_and_send": "Подтвердить и отправить",
|
"confirm_and_send": "Подтвердить и отправить",
|
||||||
"confirm_clear_ztx_title": "Подтвердить очистку истории Z-Tx",
|
"confirm_clear_ztx_title": "Подтвердить очистку истории Z-Tx",
|
||||||
"confirm_clear_ztx_warning1": "Очистка истории z-транзакций может привести к отображению защищённого баланса как 0, пока не будет выполнено пересканирование кошелька.",
|
"confirm_clear_ztx_warning1": "Очистка истории z-транзакций может привести к отображению защищённого баланса как 0, пока не будет выполнено пересканирование кошелька.",
|
||||||
"confirm_clear_ztx_warning2": "Если это произойдёт, вам потребуется повторно импортировать приватные ключи вашего z-адреса с включённым пересканированием для восстановления баланса.",
|
"confirm_clear_ztx_warning2": "Если это произойдёт, вам потребуется повторно импортировать приватные ключи вашего z-адреса с включённым пересканированием для восстановления баланса.",
|
||||||
|
"confirm_compact_switch_msg": "Вы подключены к dragonxd, который запустили сами. Чтобы выполнить сжатие, кошелёк остановит этот узел, переключится на собственный встроенный узел и перезапишет на нём wallet.dat — с перезапуском и повторным сканированием блокчейна, что может занять много времени. Обратите внимание: встроенный узел — это dragonxd из собственной папки кошелька, поэтому это меняет то, какой узел вы используете.",
|
||||||
|
"confirm_compact_wallet_msg": "Объединение слило ваши заметки, но файл кошелька сам по себе не уменьшается — чтобы вернуть освободившееся место, узел должен перезаписать его. Это перезапускает узел и заново сканирует блокчейн, что может занять много времени для большого кошелька.",
|
||||||
|
"confirm_compact_wallet_safe": "Ваши средства и баланс в безопасности — сохраняется резервная копия текущего кошелька с отметкой времени.",
|
||||||
|
"confirm_compact_wallet_title": "Сжать кошелёк",
|
||||||
"confirm_delete_blockchain_msg": "Это остановит демон, удалит все данные блокчейна (blocks, chainstate, peers) и начнёт синхронизацию с нуля. Это может занять несколько часов.",
|
"confirm_delete_blockchain_msg": "Это остановит демон, удалит все данные блокчейна (blocks, chainstate, peers) и начнёт синхронизацию с нуля. Это может занять несколько часов.",
|
||||||
"confirm_delete_blockchain_safe": "Ваш wallet.dat, конфигурация и история транзакций в безопасности и не будут удалены.",
|
"confirm_delete_blockchain_safe": "Ваш wallet.dat, конфигурация и история транзакций в безопасности и не будут удалены.",
|
||||||
"confirm_delete_blockchain_title": "Удалить данные блокчейна",
|
"confirm_delete_blockchain_title": "Удалить данные блокчейна",
|
||||||
@@ -505,7 +564,7 @@
|
|||||||
"console_welcome": "Добро пожаловать в консоль ObsidianDragon",
|
"console_welcome": "Добро пожаловать в консоль ObsidianDragon",
|
||||||
"console_zoom_in": "Увеличить",
|
"console_zoom_in": "Увеличить",
|
||||||
"console_zoom_out": "Уменьшить",
|
"console_zoom_out": "Уменьшить",
|
||||||
"consolidate_desc": "Объедините множество мелких входов в одну защищённую заметку. Меньше заметок означает файл кошелька меньшего размера и лучшую приватность.",
|
"consolidate_desc": "Каждый полученный платёж хранится как отдельная «заметка» — словно отдельная купюра в кошельке. Со временем они накапливаются, и каждая заметка добавляет данные свидетеля, из-за которых кошелёк медленнее синхронизируется и продолжает расти. Объединение сливает множество мелких заметок в одну на каждый адрес: баланс не меняется, но кошелёк сканируется быстрее, перестаёт расти и раскрывает меньше информации о ваших средствах.",
|
||||||
"consolidate_funds_btn": "Объединить",
|
"consolidate_funds_btn": "Объединить",
|
||||||
"consolidate_title": "Объединение средств",
|
"consolidate_title": "Объединение средств",
|
||||||
"contact_avatar": "АВАТАР",
|
"contact_avatar": "АВАТАР",
|
||||||
@@ -2048,6 +2107,7 @@
|
|||||||
"tt_addr_url": "Базовый URL для просмотра адресов в обозревателе блоков",
|
"tt_addr_url": "Базовый URL для просмотра адресов в обозревателе блоков",
|
||||||
"tt_address_book": "Управление сохранёнными адресами для быстрой отправки",
|
"tt_address_book": "Управление сохранёнными адресами для быстрой отправки",
|
||||||
"tt_animate_avatars": "Воспроизводить анимированные аватары контактов (GIF / WebP); при отключении показывается только первый кадр",
|
"tt_animate_avatars": "Воспроизводить анимированные аватары контактов (GIF / WebP); при отключении показывается только первый кадр",
|
||||||
|
"tt_auto_consolidate": "Незаметно объединять мелкие заметки в фоновом режиме, пока кошелёк простаивает, небольшими партиями. Медленнее ручного объединения, но почти не ощущается, а средства каждого адреса остаются раздельными. Подходит для кошельков майнинга/пула, где накапливаются тысячи заметок.",
|
||||||
"tt_auto_lock": "Заблокировать кошелёк после этого времени бездействия",
|
"tt_auto_lock": "Заблокировать кошелёк после этого времени бездействия",
|
||||||
"tt_auto_shield": "Автоматически перемещать прозрачный баланс на экранированные адреса для конфиденциальности",
|
"tt_auto_shield": "Автоматически перемещать прозрачный баланс на экранированные адреса для конфиденциальности",
|
||||||
"tt_backup": "Создать резервную копию вашего wallet.dat",
|
"tt_backup": "Создать резервную копию вашего wallet.dat",
|
||||||
@@ -2174,7 +2234,8 @@
|
|||||||
"tt_validate": "Проверить, действителен ли адрес DragonX",
|
"tt_validate": "Проверить, действителен ли адрес DragonX",
|
||||||
"tt_verbose": "Записывать подробную диагностику подключений,\\nсостояние демона и информацию о владельце порта\\nна вкладке Консоль",
|
"tt_verbose": "Записывать подробную диагностику подключений,\\nсостояние демона и информацию о владельце порта\\nна вкладке Консоль",
|
||||||
"tt_view_transactions": "Показать транзакции с этим адресом на вкладке «История»",
|
"tt_view_transactions": "Показать транзакции с этим адресом на вкладке «История»",
|
||||||
"tt_wallet_size_warn": "Защищённые кошельки растут за счёт данных witness каждой записи — объединение множества записей в один адрес уменьшает его размер. Сначала сделайте резервную копию.",
|
"tt_wallet_size_compact": "Вернуть место на диске, освобождённое после объединения. Berkeley DB не умеет уменьшать файл на месте, поэтому wallet.dat перезаписывается в файл меньшего размера, а узел перезапускается с полным пересканированием (это может занять много времени). Резервная копия текущего кошелька сохраняется.",
|
||||||
|
"tt_wallet_size_warn": "Каждый платёж поступает как отдельная экранированная заметка, и каждая заметка несёт данные свидетеля, которые замедляют сканирование и заставляют файл кошелька расти. Объединение множества заметок в одну на каждый адрес сохраняет быстродействие и останавливает рост — ваши средства и баланс не меняются. Освободившееся место файл возвращает только после сжатия (Berkeley DB не умеет уменьшать файл на месте). Сначала сделайте резервную копию seed-фразы. Также можно включить автообъединение заметок в разделе «Настройки».",
|
||||||
"tt_wallets_button": "Показать файлы кошельков и переключаться между ними",
|
"tt_wallets_button": "Показать файлы кошельков и переключаться между ними",
|
||||||
"tt_website": "Открыть сайт DragonX",
|
"tt_website": "Открыть сайт DragonX",
|
||||||
"tt_window_opacity": "Непрозрачность фона (ниже = рабочий стол виден сквозь окно)",
|
"tt_window_opacity": "Непрозрачность фона (ниже = рабочий стол виден сквозь окно)",
|
||||||
@@ -2303,8 +2364,10 @@
|
|||||||
"wallet_restore_ok": "Исходный кошелёк восстановлен. Узел сейчас его загружает.",
|
"wallet_restore_ok": "Исходный кошелёк восстановлен. Узел сейчас его загружает.",
|
||||||
"wallet_restore_started": "Восстановление вашего исходного файла — пожалуйста, не закрывайте это окно.",
|
"wallet_restore_started": "Восстановление вашего исходного файла — пожалуйста, не закрывайте это окно.",
|
||||||
"wallet_restore_stop_failed": "Узел не остановился вовремя, поэтому ничего не изменено. Попробуйте снова.",
|
"wallet_restore_stop_failed": "Узел не остановился вовремя, поэтому ничего не изменено. Попробуйте снова.",
|
||||||
|
"wallet_size_auto_info": "В вашем кошельке накопилось много заметок — отдельных частей полученных средств, которые замедляют синхронизацию и заставляют файл расти. Автообъединение незаметно объединяет их в фоновом режиме, чтобы кошелёк оставался быстрым и перестал расти, так что вам ничего делать не нужно.",
|
||||||
|
"wallet_size_compact": "Сжать кошелёк…",
|
||||||
"wallet_size_consolidate": "Объединить заметки…",
|
"wallet_size_consolidate": "Объединить заметки…",
|
||||||
"wallet_size_warn": "Файл этого кошелька велик. Объединение ваших заметок может сдержать дальнейший рост.",
|
"wallet_size_warn": "В вашем кошельке накопилось много заметок. Каждый полученный платёж хранится как отдельная «заметка»; по мере накопления они замедляют синхронизацию кошелька и заставляют его расти. Объединение сливает их в меньшее число заметок, чтобы кошелёк сканировался быстрее и перестал расти — ваш баланс при этом остаётся точно таким же.",
|
||||||
"wallets_active": "Активен",
|
"wallets_active": "Активен",
|
||||||
"wallets_add": "Добавить папку",
|
"wallets_add": "Добавить папку",
|
||||||
"wallets_add_folder": "Также сканировать другую папку на наличие файлов кошельков:",
|
"wallets_add_folder": "Также сканировать другую папку на наличие файлов кошельков:",
|
||||||
|
|||||||
+66
-3
@@ -141,6 +141,7 @@
|
|||||||
"appx_wallet_op_running": "密钥导入或重新扫描仍在进行中。请在完成后重试。",
|
"appx_wallet_op_running": "密钥导入或重新扫描仍在进行中。请在完成后重试。",
|
||||||
"appx_wallet_op_shutting_down": "钱包正在关闭。",
|
"appx_wallet_op_shutting_down": "钱包正在关闭。",
|
||||||
"appx_wallet_open_failed_prefix": "钱包打开失败:",
|
"appx_wallet_open_failed_prefix": "钱包打开失败:",
|
||||||
|
"auto_consolidate": "自动合并票据",
|
||||||
"auto_shield": "自动屏蔽挖矿",
|
"auto_shield": "自动屏蔽挖矿",
|
||||||
"autoshield_by_node": "您的节点会自动为您屏蔽挖出的 coinbase。",
|
"autoshield_by_node": "您的节点会自动为您屏蔽挖出的 coinbase。",
|
||||||
"autoshield_off_backup_seed": "挖出的 coinbase 尚未自动屏蔽。请备份您的助记词(设置 → 备份与数据),以便节点可以安全启用此功能。",
|
"autoshield_off_backup_seed": "挖出的 coinbase 尚未自动屏蔽。请备份您的助记词(设置 → 备份与数据),以便节点可以安全启用此功能。",
|
||||||
@@ -276,8 +277,12 @@
|
|||||||
"chat_delete_body_prefix": "删除与以下用户会话的本地副本:",
|
"chat_delete_body_prefix": "删除与以下用户会话的本地副本:",
|
||||||
"chat_delete_body_suffix": "?",
|
"chat_delete_body_suffix": "?",
|
||||||
"chat_delete_confirm": "删除",
|
"chat_delete_confirm": "删除",
|
||||||
|
"chat_delete_everywhere": "在所有设备删除",
|
||||||
|
"chat_delete_everywhere_note": "这还会告知您的其他设备——以及日后通过助记词恢复的钱包——隐藏此会话。此操作会发送一个微小的链上信号(需支付常规聊天手续费)。之后您可以在“恢复”中将其重新找回。",
|
||||||
|
"chat_delete_everywhere_toast": "正在所有设备上删除——信号已发送",
|
||||||
"chat_delete_failed": "无法删除会话——未做任何更改。",
|
"chat_delete_failed": "无法删除会话——未做任何更改。",
|
||||||
"chat_delete_local_note": "此操作仅影响此设备——消息仍保留在区块链上,对方也仍保有自己的副本。",
|
"chat_delete_local_note": "此操作仅影响此设备——消息仍保留在区块链上,对方也仍保有自己的副本。",
|
||||||
|
"chat_delete_needs_sync": "请等待钱包完全同步后,才能在所有设备删除。",
|
||||||
"chat_delete_revive_note": "“删除”会清除此设备上的记录。如果对方再次给您发消息,会话会重新出现。",
|
"chat_delete_revive_note": "“删除”会清除此设备上的记录。如果对方再次给您发消息,会话会重新出现。",
|
||||||
"chat_delete_title": "删除会话?",
|
"chat_delete_title": "删除会话?",
|
||||||
"chat_deleted_toast": "会话已删除",
|
"chat_deleted_toast": "会话已删除",
|
||||||
@@ -319,10 +324,24 @@
|
|||||||
"chat_opt_global_clock": "全局时间格式",
|
"chat_opt_global_clock": "全局时间格式",
|
||||||
"chat_opt_poll": "轮询频率",
|
"chat_opt_poll": "轮询频率",
|
||||||
"chat_opt_timestamp": "时间戳",
|
"chat_opt_timestamp": "时间戳",
|
||||||
|
"chat_parts_explain": "这条长消息会作为一笔交易发送,拆分成这些部分——它和普通消息一样快送达。",
|
||||||
|
"chat_parts_explain_multi": "这条消息太大,无法放入一笔交易,因此会拆分到多笔交易中发送。接收方需等所有部分都到达后才能看到,因此送达较慢,且每笔交易都会产生网络手续费。",
|
||||||
|
"chat_parts_label": "部分",
|
||||||
"chat_pick_contact": "从联系人中选择…",
|
"chat_pick_contact": "从联系人中选择…",
|
||||||
|
"chat_receiving_large": "正在接收大消息…",
|
||||||
"chat_rename": "重命名联系人",
|
"chat_rename": "重命名联系人",
|
||||||
"chat_rename_hint": "联系人名称",
|
"chat_rename_hint": "联系人名称",
|
||||||
"chat_renamed": "联系人已重命名",
|
"chat_renamed": "联系人已重命名",
|
||||||
|
"chat_restore_btn": "恢复",
|
||||||
|
"chat_restore_desc": "在所有设备删除的会话可以在此处找回——无论是在本设备上,还是在导入助记词之后。恢复会从链上重新导入消息,并通知您的其他设备再次显示该会话。",
|
||||||
|
"chat_restore_manage": "恢复",
|
||||||
|
"chat_restore_none": "尚未找到已删除的会话。如果您曾在其他设备上删除过某个会话,请尝试“快速扫描”,或在下方粘贴其聊天地址。",
|
||||||
|
"chat_restore_paste_hint": "粘贴该会话的聊天(z)地址",
|
||||||
|
"chat_restore_paste_label": "通过聊天地址恢复",
|
||||||
|
"chat_restore_scan": "快速扫描",
|
||||||
|
"chat_restore_scanning": "正在扫描区块链以查找已删除的会话…",
|
||||||
|
"chat_restore_title": "恢复会话",
|
||||||
|
"chat_restore_toast": "正在恢复会话——将在下次扫描后重新显示",
|
||||||
"chat_retry": "重试",
|
"chat_retry": "重试",
|
||||||
"chat_search": "搜索会话",
|
"chat_search": "搜索会话",
|
||||||
"chat_sec_appearance": "外观",
|
"chat_sec_appearance": "外观",
|
||||||
@@ -331,6 +350,35 @@
|
|||||||
"chat_send": "发送",
|
"chat_send": "发送",
|
||||||
"chat_send_failed": "未发送",
|
"chat_send_failed": "未发送",
|
||||||
"chat_sending": "发送中…",
|
"chat_sending": "发送中…",
|
||||||
|
"chat_sensitive_btn_mark": "设为私密",
|
||||||
|
"chat_sensitive_btn_setup": "设置并设为私密",
|
||||||
|
"chat_sensitive_btn_unlock": "解锁",
|
||||||
|
"chat_sensitive_btn_unlock_mark": "解锁并设为私密",
|
||||||
|
"chat_sensitive_btn_unlock_unmark": "解锁并设为普通",
|
||||||
|
"chat_sensitive_btn_unmark": "设为普通",
|
||||||
|
"chat_sensitive_err_create": "无法设置私密聊天保险库。",
|
||||||
|
"chat_sensitive_err_pass": "两次输入的密码短语不一致,或为空。",
|
||||||
|
"chat_sensitive_err_unlock": "密码短语错误。",
|
||||||
|
"chat_sensitive_lock_btn": "锁定私密会话",
|
||||||
|
"chat_sensitive_mark_tip": "设为私密",
|
||||||
|
"chat_sensitive_mark_title": "将此会话设为私密?",
|
||||||
|
"chat_sensitive_pass2_hint": "确认密码短语",
|
||||||
|
"chat_sensitive_pass_hint": "密码短语",
|
||||||
|
"chat_sensitive_setup_label": "为私密会话创建一个密码短语",
|
||||||
|
"chat_sensitive_toast_marked": "会话现已设为私密",
|
||||||
|
"chat_sensitive_toast_unlocked": "私密会话已解锁",
|
||||||
|
"chat_sensitive_toast_unmarked": "会话已设为普通",
|
||||||
|
"chat_sensitive_unlock_body": "输入您的私密聊天密码短语,以重新显示您的私密会话。",
|
||||||
|
"chat_sensitive_unlock_btn": "解锁私密会话",
|
||||||
|
"chat_sensitive_unlock_label": "输入您的私密聊天密码短语",
|
||||||
|
"chat_sensitive_unlock_title": "解锁私密会话",
|
||||||
|
"chat_sensitive_unmark_body": "此会话将不再使用您的私密聊天密码短语加密。新消息将像其他任何会话一样存储。",
|
||||||
|
"chat_sensitive_unmark_tip": "私密——点击可更改",
|
||||||
|
"chat_sensitive_unmark_title": "将此会话设为普通?",
|
||||||
|
"chat_sensitive_warn_1": "其消息将在此设备上以一个单独的密码短语加密——而非您的助记词。日后任何仅凭您的助记词恢复钱包的人,都将看不到此会话。",
|
||||||
|
"chat_sensitive_warn_dir": "您在此发送的消息受到完全保护。收到的消息仍保留在区块链上,理论上,拥有您助记词及其他软件的人有可能将其恢复——但没有您的密码短语,DragonX 本身绝不会显示它们。",
|
||||||
|
"chat_sensitive_warn_hist": "此会话中已有的消息仍可从链上恢复;此功能将完全保护此后的新消息。",
|
||||||
|
"chat_sensitive_warn_lose": "如果您丢失此密码短语,这些消息将永远消失——它们无法通过您的助记词恢复。",
|
||||||
"chat_settings_done": "完成",
|
"chat_settings_done": "完成",
|
||||||
"chat_settings_section": "聊天与联系人",
|
"chat_settings_section": "聊天与联系人",
|
||||||
"chat_settings_tip": "聊天自定义",
|
"chat_settings_tip": "聊天自定义",
|
||||||
@@ -344,12 +392,14 @@
|
|||||||
"chat_toast_not_connected": "未连接——聊天消息未发送。",
|
"chat_toast_not_connected": "未连接——聊天消息未发送。",
|
||||||
"chat_toast_request_compose_failed": "无法编写联系人请求(地址或文本无效?)。",
|
"chat_toast_request_compose_failed": "无法编写联系人请求(地址或文本无效?)。",
|
||||||
"chat_toast_request_queued": "联系人请求已排队。",
|
"chat_toast_request_queued": "联系人请求已排队。",
|
||||||
|
"chat_toast_too_long": "消息过长——请缩短后重新发送。",
|
||||||
"chat_toast_waiting_reply": "等待该联系人回复——对方回复后您即可向其发送消息。",
|
"chat_toast_waiting_reply": "等待该联系人回复——对方回复后您即可向其发送消息。",
|
||||||
"chat_today": "今天",
|
"chat_today": "今天",
|
||||||
"chat_ts_12h": "12小时",
|
"chat_ts_12h": "12小时",
|
||||||
"chat_ts_24h": "24小时",
|
"chat_ts_24h": "24小时",
|
||||||
"chat_ts_global": "跟随全局",
|
"chat_ts_global": "跟随全局",
|
||||||
"chat_ts_global_short": "全局",
|
"chat_ts_global_short": "全局",
|
||||||
|
"chat_txns_label": "笔",
|
||||||
"chat_unblock": "取消屏蔽",
|
"chat_unblock": "取消屏蔽",
|
||||||
"chat_unblocked_toast": "会话已取消屏蔽",
|
"chat_unblocked_toast": "会话已取消屏蔽",
|
||||||
"chat_unhide": "取消隐藏",
|
"chat_unhide": "取消隐藏",
|
||||||
@@ -370,11 +420,20 @@
|
|||||||
"click_to_copy": "点击复制",
|
"click_to_copy": "点击复制",
|
||||||
"clock_format": "时间格式",
|
"clock_format": "时间格式",
|
||||||
"close": "关闭",
|
"close": "关闭",
|
||||||
|
"compact_needs_embedded": "压缩会重写 wallet.dat,而钱包只有使用其内置节点才能做到这一点。您当前连接的是自己启动的 dragonxd——请在“设置”中开启\"使用内置 dragonxd\",或使用 -salvagewallet 重启您的 dragonxd 以回收空间。",
|
||||||
|
"compact_started": "正在压缩钱包——节点正在重启,并将重新扫描区块链。",
|
||||||
|
"compact_switch_stop_failed": "无法停止您的外部节点——它仍在运行,因此未做任何更改。",
|
||||||
|
"compact_switching": "正在停止您的节点并切换到内置节点…",
|
||||||
|
"compact_wallet_confirm": "压缩",
|
||||||
"conf_count": "%d 确认",
|
"conf_count": "%d 确认",
|
||||||
"confirm_and_send": "确认并发送",
|
"confirm_and_send": "确认并发送",
|
||||||
"confirm_clear_ztx_title": "确认清除 Z-Tx 历史",
|
"confirm_clear_ztx_title": "确认清除 Z-Tx 历史",
|
||||||
"confirm_clear_ztx_warning1": "清除 z-交易历史可能导致您的屏蔽余额显示为 0,直到执行钱包重新扫描。",
|
"confirm_clear_ztx_warning1": "清除 z-交易历史可能导致您的屏蔽余额显示为 0,直到执行钱包重新扫描。",
|
||||||
"confirm_clear_ztx_warning2": "如果发生这种情况,您需要在启用重新扫描的情况下重新导入 z-地址私钥以恢复余额。",
|
"confirm_clear_ztx_warning2": "如果发生这种情况,您需要在启用重新扫描的情况下重新导入 z-地址私钥以恢复余额。",
|
||||||
|
"confirm_compact_switch_msg": "您当前连接的是自己启动的 dragonxd。为了进行压缩,钱包将停止该节点,切换到自带的内置节点,并在其上重写 wallet.dat——这会重启节点并重新扫描区块链,可能需要一段时间。注意:内置节点是钱包自身文件夹中的 dragonxd,因此这会改变您所运行的节点。",
|
||||||
|
"confirm_compact_wallet_msg": "合并已将您的票据合成较少的票据,但钱包文件无法自行缩小——节点必须重写文件才能回收释放出的空间。这会重启节点并重新扫描区块链,钱包较大时可能需要一段时间。",
|
||||||
|
"confirm_compact_wallet_safe": "您的资金和余额是安全的——当前钱包的带时间戳备份会被保留。",
|
||||||
|
"confirm_compact_wallet_title": "压缩钱包",
|
||||||
"confirm_delete_blockchain_msg": "这将停止守护进程,删除所有区块链数据(blocks、chainstate、peers),并从头开始重新同步。这可能需要几个小时。",
|
"confirm_delete_blockchain_msg": "这将停止守护进程,删除所有区块链数据(blocks、chainstate、peers),并从头开始重新同步。这可能需要几个小时。",
|
||||||
"confirm_delete_blockchain_safe": "您的 wallet.dat、配置和交易历史是安全的,不会被删除。",
|
"confirm_delete_blockchain_safe": "您的 wallet.dat、配置和交易历史是安全的,不会被删除。",
|
||||||
"confirm_delete_blockchain_title": "删除区块链数据",
|
"confirm_delete_blockchain_title": "删除区块链数据",
|
||||||
@@ -505,7 +564,7 @@
|
|||||||
"console_welcome": "欢迎使用 ObsidianDragon 控制台",
|
"console_welcome": "欢迎使用 ObsidianDragon 控制台",
|
||||||
"console_zoom_in": "放大",
|
"console_zoom_in": "放大",
|
||||||
"console_zoom_out": "缩小",
|
"console_zoom_out": "缩小",
|
||||||
"consolidate_desc": "将多个小额输入合并为单个隐私票据。票据越少,钱包文件越小,隐私性也越好。",
|
"consolidate_desc": "您收到的每笔付款都会存为一张单独的“票据”——就像钱包里的一张张零钱。它们会随时间不断堆积,而且每张票据都会增加见证数据,使钱包同步变慢并不断变大。合并会将许多小额票据合成每个地址一张:您的余额不会改变,但钱包扫描更快、不再继续变大,并且泄露的资金信息更少。",
|
||||||
"consolidate_funds_btn": "合并",
|
"consolidate_funds_btn": "合并",
|
||||||
"consolidate_title": "合并资金",
|
"consolidate_title": "合并资金",
|
||||||
"contact_avatar": "头像",
|
"contact_avatar": "头像",
|
||||||
@@ -2048,6 +2107,7 @@
|
|||||||
"tt_addr_url": "在区块浏览器中查看地址的基础 URL",
|
"tt_addr_url": "在区块浏览器中查看地址的基础 URL",
|
||||||
"tt_address_book": "管理已保存的地址以快速发送",
|
"tt_address_book": "管理已保存的地址以快速发送",
|
||||||
"tt_animate_avatars": "播放动画联系人头像(GIF / WebP);关闭时仅显示第一帧",
|
"tt_animate_avatars": "播放动画联系人头像(GIF / WebP);关闭时仅显示第一帧",
|
||||||
|
"tt_auto_consolidate": "在钱包空闲时于后台悄悄地分小批合并零散的小额票据。比手动合并慢,但几乎无感,而且各地址的资金仍保持独立。适合会累积成千上万张票据的挖矿/矿池钱包。",
|
||||||
"tt_auto_lock": "在此不活动时间后锁定钱包",
|
"tt_auto_lock": "在此不活动时间后锁定钱包",
|
||||||
"tt_auto_shield": "自动将透明余额转移到屏蔽地址以增强隐私",
|
"tt_auto_shield": "自动将透明余额转移到屏蔽地址以增强隐私",
|
||||||
"tt_backup": "创建 wallet.dat 的备份",
|
"tt_backup": "创建 wallet.dat 的备份",
|
||||||
@@ -2174,7 +2234,8 @@
|
|||||||
"tt_validate": "检查 DragonX 地址是否有效",
|
"tt_validate": "检查 DragonX 地址是否有效",
|
||||||
"tt_verbose": "将详细连接诊断、守护进程状态\\n和端口所有者信息记录到控制台选项卡",
|
"tt_verbose": "将详细连接诊断、守护进程状态\\n和端口所有者信息记录到控制台选项卡",
|
||||||
"tt_view_transactions": "在历史记录标签页中显示涉及此地址的交易",
|
"tt_view_transactions": "在历史记录标签页中显示涉及此地址的交易",
|
||||||
"tt_wallet_size_warn": "屏蔽钱包会随每个票据的见证数据增长——将多个票据合并到一个地址可以缩小体积。请先备份。",
|
"tt_wallet_size_compact": "回收合并后释放出的磁盘空间。Berkeley DB 无法原地缩小文件,因此此操作会将 wallet.dat 重写为更小的文件,并重启节点进行完整重新扫描(可能需要一段时间)。当前钱包的备份会被保留。",
|
||||||
|
"tt_wallet_size_warn": "每笔付款都会作为一张单独的屏蔽票据到账,而每张票据都带有见证数据,会拖慢扫描并使钱包文件不断变大。将多张票据合并为每个地址一张,即可保持快速并停止变大——您的资金和余额不会改变。只有在您执行“压缩”后,文件才会回收释放出的空间(Berkeley DB 无法原地缩小)。请先备份您的助记词。您也可以在设置中开启“自动合并票据”。",
|
||||||
"tt_wallets_button": "列出您的钱包文件并在它们之间切换",
|
"tt_wallets_button": "列出您的钱包文件并在它们之间切换",
|
||||||
"tt_website": "打开 DragonX 网站",
|
"tt_website": "打开 DragonX 网站",
|
||||||
"tt_window_opacity": "背景不透明度(越低 = 桌面透过窗口可见)",
|
"tt_window_opacity": "背景不透明度(越低 = 桌面透过窗口可见)",
|
||||||
@@ -2303,8 +2364,10 @@
|
|||||||
"wallet_restore_ok": "原始钱包已还原。节点正在加载中。",
|
"wallet_restore_ok": "原始钱包已还原。节点正在加载中。",
|
||||||
"wallet_restore_started": "正在还原您的原始文件——请勿关闭此窗口。",
|
"wallet_restore_started": "正在还原您的原始文件——请勿关闭此窗口。",
|
||||||
"wallet_restore_stop_failed": "节点未能及时停止,因此未做任何更改。请重试。",
|
"wallet_restore_stop_failed": "节点未能及时停止,因此未做任何更改。请重试。",
|
||||||
|
"wallet_size_auto_info": "您的钱包已累积了大量票据——这些是分散的已收资金,会拖慢同步并使文件不断变大。自动合并票据正在后台悄悄地合并它们,让钱包保持快速并停止变大,您无需任何操作。",
|
||||||
|
"wallet_size_compact": "压缩钱包…",
|
||||||
"wallet_size_consolidate": "合并笔记…",
|
"wallet_size_consolidate": "合并笔记…",
|
||||||
"wallet_size_warn": "此钱包文件较大。合并您的笔记可以抑制其进一步增长。",
|
"wallet_size_warn": "您的钱包已累积了大量票据。您收到的每笔付款都会存为一张单独的“票据”;票据越积越多,钱包同步就会变慢,文件也会不断变大。合并会将它们合成较少的票据,让钱包扫描更快、不再继续变大——您的余额完全不变。",
|
||||||
"wallets_active": "活跃",
|
"wallets_active": "活跃",
|
||||||
"wallets_add": "添加文件夹",
|
"wallets_add": "添加文件夹",
|
||||||
"wallets_add_folder": "同时扫描另一个文件夹中的钱包文件:",
|
"wallets_add_folder": "同时扫描另一个文件夹中的钱包文件:",
|
||||||
|
|||||||
@@ -1225,6 +1225,8 @@ pill-margin-y = { size = 3.0 }
|
|||||||
pill-rounding = { size = 12.0 }
|
pill-rounding = { size = 12.0 }
|
||||||
padding-x = { size = 12.0 }
|
padding-x = { size = 12.0 }
|
||||||
glass-fill-alpha = { size = 18.0 }
|
glass-fill-alpha = { size = 18.0 }
|
||||||
|
accent-fill-alpha = { size = 0.22 } # severity-tint fill opacity (fraction of the accent alpha)
|
||||||
|
badge-fill-alpha = { size = 0.35 } # "+N more" badge fill opacity (fraction of the accent alpha)
|
||||||
glass-border-alpha = { size = 35.0 }
|
glass-border-alpha = { size = 35.0 }
|
||||||
progress-bar-height = { size = 2.0 }
|
progress-bar-height = { size = 2.0 }
|
||||||
duration-info = { size = 1.0 }
|
duration-info = { size = 1.0 }
|
||||||
|
|||||||
@@ -48,6 +48,36 @@ for f in "${files[@]}"; do
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# ── Content guards (UX-hygiene threads) ──────────────────────────────────────
|
||||||
|
# Low-false-positive regression checks for debt the UX audit cleared, so it can't
|
||||||
|
# silently regrow. Each is deliberately narrow; widen only with an allowlist.
|
||||||
|
for f in "${files[@]}"; do
|
||||||
|
[[ "$f" == *.cpp ]] || continue
|
||||||
|
[[ -f "$f" ]] || continue
|
||||||
|
# app_sweep.cpp is the dev screenshot-sweep harness: its fixture strings are never shipped UI.
|
||||||
|
[[ "$(basename "$f")" == "app_sweep.cpp" ]] && continue
|
||||||
|
|
||||||
|
# (1) Tactile button sized with a raw multi-digit pixel literal that ignores HiDPI.
|
||||||
|
# Real sizes multiply by ui::Layout::dpiScale() / dp / hScale, or come from schema.
|
||||||
|
if hits=$(grep -nE 'Tactile(Small)?Button\([^;]*ImVec2\([1-9][0-9]+(\.[0-9]+f?)?,' "$f" \
|
||||||
|
| grep -vE 'dpiScale|\* *dp\b|\* *hdpi|hScale|schema|Scale\('); then
|
||||||
|
while IFS= read -r line; do
|
||||||
|
echo "✗ unscaled button size (multiply the px by ui::Layout::dpiScale()): $f:$line" >&2
|
||||||
|
fail=1
|
||||||
|
done <<< "$hits"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# (2) User-visible migrate/sweep status assigned a bare English literal instead of TR(...).
|
||||||
|
# "__SENTINEL__" comparisons and empty strings are fine.
|
||||||
|
if hits=$(grep -nE '(sweep_status_|seed_migration_status_)[[:space:]]*=[[:space:]]*"[A-Za-z]' "$f" \
|
||||||
|
| grep -vE 'TR\(|"__'); then
|
||||||
|
while IFS= read -r line; do
|
||||||
|
echo "✗ untranslated status string (route through TR(...)): $f:$line" >&2
|
||||||
|
fail=1
|
||||||
|
done <<< "$hits"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
if (( fail )); then
|
if (( fail )); then
|
||||||
echo "" >&2
|
echo "" >&2
|
||||||
echo "Source hygiene check failed. See docs in scripts/check-source-hygiene.sh." >&2
|
echo "Source hygiene check failed. See docs in scripts/check-source-hygiene.sh." >&2
|
||||||
|
|||||||
+319
-77
@@ -36,11 +36,11 @@
|
|||||||
#include "ui/windows/explorer_tab.h"
|
#include "ui/windows/explorer_tab.h"
|
||||||
#include "ui/windows/seed_display.h"
|
#include "ui/windows/seed_display.h"
|
||||||
#include "ui/windows/market_tab.h"
|
#include "ui/windows/market_tab.h"
|
||||||
#include "ui/windows/settings_window.h"
|
|
||||||
#include "ui/windows/about_dialog.h"
|
#include "ui/windows/about_dialog.h"
|
||||||
#include "ui/windows/faq_dialog.h"
|
#include "ui/windows/faq_dialog.h"
|
||||||
#include "embedded/IconsMaterialDesign.h"
|
#include "embedded/IconsMaterialDesign.h"
|
||||||
#include "ui/windows/key_export_dialog.h"
|
#include "ui/windows/key_export_dialog.h"
|
||||||
|
#include "ui/imgui_secret_scrub.h" // scrubImGuiInputTextState — zero ImGui's retained secret buffers
|
||||||
#include "ui/windows/transaction_details_dialog.h"
|
#include "ui/windows/transaction_details_dialog.h"
|
||||||
#include "ui/windows/qr_popup_dialog.h"
|
#include "ui/windows/qr_popup_dialog.h"
|
||||||
#include "ui/windows/validate_address_dialog.h"
|
#include "ui/windows/validate_address_dialog.h"
|
||||||
@@ -144,6 +144,28 @@ void App::wipeSecrets()
|
|||||||
sodium_memzero(export_result_, sizeof(export_result_)); // exported WIF/z-key (SECRET)
|
sodium_memzero(export_result_, sizeof(export_result_)); // exported WIF/z-key (SECRET)
|
||||||
sodium_memzero(import_key_input_, sizeof(import_key_input_)); // pasted private key (SECRET)
|
sodium_memzero(import_key_input_, sizeof(import_key_input_)); // pasted private key (SECRET)
|
||||||
sodium_memzero(import_clip_preview_, sizeof(import_clip_preview_)); // clipboard preview (SECRET)
|
sodium_memzero(import_clip_preview_, sizeof(import_clip_preview_)); // clipboard preview (SECRET)
|
||||||
|
ui::scrubImGuiInputTextState(); // zero ImGui's retained copies of whatever secret was last typed
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scrub + close any secret that is currently ON SCREEN (revealed seed phrase, migration seed, exported
|
||||||
|
// key) when the wallet locks — locking shouldn't leave a phrase/key visible or resident. Called on every
|
||||||
|
// lock transition (manual, auto, lite). Separate from wipeSecrets(), which is the exit-path scrub.
|
||||||
|
void App::wipeDisplayedSecrets()
|
||||||
|
{
|
||||||
|
if (!seed_backup_phrase_.empty()) {
|
||||||
|
sodium_memzero(&seed_backup_phrase_[0], seed_backup_phrase_.size());
|
||||||
|
seed_backup_phrase_.clear();
|
||||||
|
}
|
||||||
|
if (!seed_migration_seed_.empty()) {
|
||||||
|
sodium_memzero(&seed_migration_seed_[0], seed_migration_seed_.size());
|
||||||
|
seed_migration_seed_.clear();
|
||||||
|
}
|
||||||
|
seed_backup_fetch_started_ = false;
|
||||||
|
show_seed_backup_ = false;
|
||||||
|
sodium_memzero(export_result_, sizeof(export_result_));
|
||||||
|
ui::KeyExportDialog::hide(); // zeros its static s_key via secureWipeLiteSecret
|
||||||
|
console_tab_.wipeSecretResult(); // drop any masked key-dump result held in the console
|
||||||
|
ui::scrubImGuiInputTextState(); // zero ImGui's retained copy of the last-typed passphrase/PIN
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enqueue a blocking xmrig start/stop op onto the dedicated serialized control thread so the render thread
|
// Enqueue a blocking xmrig start/stop op onto the dedicated serialized control thread so the render thread
|
||||||
@@ -943,6 +965,9 @@ void App::update()
|
|||||||
lite_wallet_->pumpAsyncOpen();
|
lite_wallet_->pumpAsyncOpen();
|
||||||
// Likewise finalize any completed async lifecycle request (Settings-page create/open/restore).
|
// Likewise finalize any completed async lifecycle request (Settings-page create/open/restore).
|
||||||
lite_wallet_->pumpLifecycleResult();
|
lite_wallet_->pumpLifecycleResult();
|
||||||
|
// A create/restore whose passphrase encryption failed left the wallet UNENCRYPTED (previously log-only): say so.
|
||||||
|
if (lite_wallet_->takeEncryptionFailureNotice())
|
||||||
|
ui::Notifications::instance().error(TR("lite_encrypt_failed_unencrypted"), 15.0f);
|
||||||
// Deliver finished wallet ops (new address / import / encrypt / decrypt) to their UI callbacks.
|
// Deliver finished wallet ops (new address / import / encrypt / decrypt) to their UI callbacks.
|
||||||
lite_wallet_->pumpWalletOps();
|
lite_wallet_->pumpWalletOps();
|
||||||
// A server switch deferred behind an in-flight wallet op: retry it. This may replace lite_wallet_; the rest of
|
// A server switch deferred behind an in-flight wallet op: retry it. This may replace lite_wallet_; the rest of
|
||||||
@@ -1028,7 +1053,11 @@ void App::update()
|
|||||||
case LiteOpKind::None:
|
case LiteOpKind::None:
|
||||||
default:
|
default:
|
||||||
if (lite_send_callback_) {
|
if (lite_send_callback_) {
|
||||||
lite_send_callback_(broadcast.ok, broadcast.ok ? broadcast.txid : broadcast.error);
|
// A transport-style failure may mean the server accepted the tx but the reply was
|
||||||
|
// lost: tag it so the Send tab says "check History" instead of inviting a resend.
|
||||||
|
const std::string liteErr = (!broadcast.ok && rpc::liteSendErrorMayBeInFlight(broadcast.error))
|
||||||
|
? rpc::markSendMaybeInFlight(broadcast.error) : broadcast.error;
|
||||||
|
lite_send_callback_(broadcast.ok, broadcast.ok ? broadcast.txid : liteErr);
|
||||||
lite_send_callback_ = nullptr;
|
lite_send_callback_ = nullptr;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -1233,14 +1262,18 @@ void App::update()
|
|||||||
if (xmrig_manager_ && state_.pool_mining.xmrig_running
|
if (xmrig_manager_ && state_.pool_mining.xmrig_running
|
||||||
&& xmrig_manager_->getState() == daemon::XmrigManager::State::Error) {
|
&& xmrig_manager_->getState() == daemon::XmrigManager::State::Error) {
|
||||||
state_.pool_mining.xmrig_running = false;
|
state_.pool_mining.xmrig_running = false;
|
||||||
|
state_.pool_mining.running_since = 0;
|
||||||
|
state_.pool_mining.last_error.clear();
|
||||||
state_.pool_mining.hashrate_10s = 0.0;
|
state_.pool_mining.hashrate_10s = 0.0;
|
||||||
state_.pool_mining.hashrate_60s = 0.0;
|
state_.pool_mining.hashrate_60s = 0.0;
|
||||||
state_.pool_mining.hashrate_15m = 0.0;
|
state_.pool_mining.hashrate_15m = 0.0;
|
||||||
pool_starting_.store(false, std::memory_order_relaxed);
|
pool_starting_.store(false, std::memory_order_relaxed);
|
||||||
pool_threads_target_.store(0, std::memory_order_relaxed); // that miner is gone
|
pool_threads_target_.store(0, std::memory_order_relaxed); // that miner is gone
|
||||||
const std::string err = xmrig_manager_->getLastError();
|
const std::string err = xmrig_manager_->getLastError();
|
||||||
ui::Notifications::instance().error(err.empty() ? TR("appx_miner_stopped_unexpectedly")
|
const std::string mmsg = err.empty() ? std::string(TR("appx_miner_stopped_unexpectedly"))
|
||||||
: (std::string(TR("appx_miner_stopped_prefix")) + err));
|
: (std::string(TR("appx_miner_stopped_prefix")) + err);
|
||||||
|
setMiningError(mmsg); // persist: a miner that crashed/was killed otherwise just "did nothing"
|
||||||
|
ui::Notifications::instance().error(mmsg);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Poll xmrig stats every ~2 seconds (use a simple toggle)
|
// Poll xmrig stats every ~2 seconds (use a simple toggle)
|
||||||
@@ -1273,6 +1306,19 @@ void App::update()
|
|||||||
ps.threads_active = xs.threads_active;
|
ps.threads_active = xs.threads_active;
|
||||||
ps.pool_hashrate = xs.pool_hashrate;
|
ps.pool_hashrate = xs.pool_hashrate;
|
||||||
ps.log_lines = xmrig_manager_->getRecentLines(30);
|
ps.log_lines = xmrig_manager_->getRecentLines(30);
|
||||||
|
if (ps.running_since == 0) ps.running_since = std::time(nullptr);
|
||||||
|
// While not yet connected, surface the newest error-looking log line so "Connecting" can flip
|
||||||
|
// to a real "can't reach pool" after a timeout instead of sitting on red forever.
|
||||||
|
ps.last_error.clear();
|
||||||
|
if (!ps.connected) {
|
||||||
|
for (auto it = ps.log_lines.rbegin(); it != ps.log_lines.rend(); ++it) {
|
||||||
|
std::string l = *it;
|
||||||
|
std::transform(l.begin(), l.end(), l.begin(), [](unsigned char c){ return (char)std::tolower(c); });
|
||||||
|
if (l.find("error") != std::string::npos || l.find("refused") != std::string::npos ||
|
||||||
|
l.find("timed out") != std::string::npos || l.find("unreachable") != std::string::npos ||
|
||||||
|
l.find("dns") != std::string::npos) { ps.last_error = *it; break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Record hashrate sample for the chart
|
// Record hashrate sample for the chart
|
||||||
ps.hashrate_history.push_back(ps.hashrate_10s);
|
ps.hashrate_history.push_back(ps.hashrate_10s);
|
||||||
@@ -1283,6 +1329,8 @@ void App::update()
|
|||||||
}
|
}
|
||||||
} else if (xmrig_manager_ && !xmrig_manager_->isRunning()) {
|
} else if (xmrig_manager_ && !xmrig_manager_->isRunning()) {
|
||||||
state_.pool_mining.xmrig_running = false;
|
state_.pool_mining.xmrig_running = false;
|
||||||
|
state_.pool_mining.running_since = 0;
|
||||||
|
state_.pool_mining.last_error.clear();
|
||||||
pool_starting_.store(false, std::memory_order_relaxed); // stopped / never came up
|
pool_starting_.store(false, std::memory_order_relaxed); // stopped / never came up
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1544,6 +1592,13 @@ void App::update()
|
|||||||
if (send_progress_active_ && !addressScanCanRunNow()) maybeFinishTransactionSendProgress();
|
if (send_progress_active_ && !addressScanCanRunNow()) maybeFinishTransactionSendProgress();
|
||||||
// C210: the "Keep in each address" consolidation run, if one is going (paced; one RPC at a time).
|
// C210: the "Keep in each address" consolidation run, if one is going (paced; one RPC at a time).
|
||||||
pumpConsolidation();
|
pumpConsolidation();
|
||||||
|
// Background auto-consolidation (settings toggle): gently trickle-merge notes when the wallet is idle.
|
||||||
|
pumpAutoConsolidation();
|
||||||
|
// While a USER-started consolidation is running, pause the heavy routine wallet scans (balance /
|
||||||
|
// addresses / history / recent-tx) so its merges get more of the node's cs_main budget and finish
|
||||||
|
// sooner — the end-of-run refresh repaints. An auto (background) run stays gentle and is left alone, and
|
||||||
|
// explicit needs (the user's own send, a never-loaded list) still go through. Idempotent per frame.
|
||||||
|
scan_governor_.suppressRoutineWalletScans(consolidation_.active() && !auto_consolidate_run_);
|
||||||
|
|
||||||
// A Settings bootstrap that finished while its dialog wasn't drawn (clicked away, minimized): process it
|
// A Settings bootstrap that finished while its dialog wasn't drawn (clicked away, minimized): process it
|
||||||
// here so the connect and reconcile gates release and the post-bootstrap rescan is armed.
|
// here so the connect and reconcile gates release and the post-bootstrap rescan is armed.
|
||||||
@@ -1944,6 +1999,12 @@ void App::render()
|
|||||||
}
|
}
|
||||||
settings_->setWizardCompleted(true);
|
settings_->setWizardCompleted(true);
|
||||||
settings_->save();
|
settings_->save();
|
||||||
|
// Soften the abrupt handoff: tell the user the node is starting + will keep syncing in the
|
||||||
|
// background, instead of dropping them onto a busy/blank main window with no explanation.
|
||||||
|
if (!state_.connected)
|
||||||
|
ui::Notifications::instance().info(
|
||||||
|
settings_->getEncryptionPending() ? TR("wiz_handoff_encrypting") : TR("wiz_handoff_starting"),
|
||||||
|
10.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process deferred encryption from wizard (runs in background)
|
// Process deferred encryption from wizard (runs in background)
|
||||||
@@ -2368,6 +2429,8 @@ void App::render()
|
|||||||
console_exec_ = std::make_unique<ui::LiteConsoleExecutor>(this);
|
console_exec_ = std::make_unique<ui::LiteConsoleExecutor>(this);
|
||||||
else
|
else
|
||||||
console_exec_ = std::make_unique<ui::FullNodeConsoleExecutor>(this);
|
console_exec_ = std::make_unique<ui::FullNodeConsoleExecutor>(this);
|
||||||
|
// One-time: route the console's "Copy secret" through the auto-clearing clipboard.
|
||||||
|
console_tab_.setCopySecretHandler([this](const std::string& s) { copySecretToClipboard(s); });
|
||||||
}
|
}
|
||||||
console_tab_.render(*console_exec_);
|
console_tab_.render(*console_exec_);
|
||||||
// Persist the console toolbar toggles (ConsoleTab has no settings pointer). Save only when
|
// Persist the console toolbar toggles (ConsoleTab has no settings pointer). Save only when
|
||||||
@@ -2509,9 +2572,6 @@ void App::render()
|
|||||||
// content area, status bar, and every other widget in this window.
|
// content area, status bar, and every other widget in this window.
|
||||||
// ====================================================================
|
// ====================================================================
|
||||||
PERF_BEGIN(_perfDialogs);
|
PERF_BEGIN(_perfDialogs);
|
||||||
if (show_settings_) {
|
|
||||||
ui::RenderSettingsWindow(this, &show_settings_);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (show_about_) {
|
if (show_about_) {
|
||||||
ui::RenderAboutDialog(this, &show_about_);
|
ui::RenderAboutDialog(this, &show_about_);
|
||||||
@@ -2530,6 +2590,10 @@ void App::render()
|
|||||||
renderImportKeyDialog();
|
renderImportKeyDialog();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (show_import_wallet_file_) {
|
||||||
|
renderImportWalletFileDialog();
|
||||||
|
}
|
||||||
|
|
||||||
if (show_export_key_) {
|
if (show_export_key_) {
|
||||||
renderExportKeyDialog();
|
renderExportKeyDialog();
|
||||||
}
|
}
|
||||||
@@ -2662,6 +2726,11 @@ void App::renderNodeStatusBanner()
|
|||||||
const NodeBusyText busy = nodeBusyText();
|
const NodeBusyText busy = nodeBusyText();
|
||||||
in.node_busy = busy.busy;
|
in.node_busy = busy.busy;
|
||||||
in.node_busy_detail = busy.compact; // one line; the overlay and the quit prompt explain more
|
in.node_busy_detail = busy.compact; // one line; the overlay and the quit prompt explain more
|
||||||
|
in.auth_failed = rpc_auth_failed_;
|
||||||
|
// Corrected trigger: ONLY a degraded wallet (wallet_degraded_ — z_getnewaddress fails, can't shield) that
|
||||||
|
// actually holds transparent funds. A merely-non-mnemonic but functional wallet has wallet_degraded_ ==
|
||||||
|
// false and is never flagged here. Full-node only (in.lite returns early regardless).
|
||||||
|
in.legacy_funds_stuck = !isLiteBuild() && wallet_degraded_ && state_.transparentBalance > 0.0001;
|
||||||
|
|
||||||
const ui::NodeBannerState banner = ui::evaluateNodeStatusBanner(in);
|
const ui::NodeBannerState banner = ui::evaluateNodeStatusBanner(in);
|
||||||
if (!banner.show) return;
|
if (!banner.show) return;
|
||||||
@@ -2698,6 +2767,10 @@ void App::renderNodeStatusBanner()
|
|||||||
title = TR("node_banner_no_peers_title"); icon = ICON_MD_CLOUD_OFF; break;
|
title = TR("node_banner_no_peers_title"); icon = ICON_MD_CLOUD_OFF; break;
|
||||||
case ui::NodeBannerReason::NodeBusy:
|
case ui::NodeBannerReason::NodeBusy:
|
||||||
title = busy.title.c_str(); icon = ICON_MD_HOURGLASS_EMPTY; break;
|
title = busy.title.c_str(); icon = ICON_MD_HOURGLASS_EMPTY; break;
|
||||||
|
case ui::NodeBannerReason::AuthFailed:
|
||||||
|
title = TR("node_banner_auth_title"); icon = ICON_MD_LOCK; break;
|
||||||
|
case ui::NodeBannerReason::LegacyFundsStuck:
|
||||||
|
title = TR("node_banner_legacy_title"); icon = ICON_MD_KEY; break;
|
||||||
case ui::NodeBannerReason::FullNodeOffline:
|
case ui::NodeBannerReason::FullNodeOffline:
|
||||||
default:
|
default:
|
||||||
title = TR("node_banner_offline_title"); icon = ICON_MD_CLOUD_OFF; break;
|
title = TR("node_banner_offline_title"); icon = ICON_MD_CLOUD_OFF; break;
|
||||||
@@ -2706,13 +2779,17 @@ void App::renderNodeStatusBanner()
|
|||||||
const char* actionLabel = nullptr;
|
const char* actionLabel = nullptr;
|
||||||
if (banner.action == ui::NodeBannerAction::Reconnect) actionLabel = TR("node_banner_reconnect");
|
if (banner.action == ui::NodeBannerAction::Reconnect) actionLabel = TR("node_banner_reconnect");
|
||||||
else if (banner.action == ui::NodeBannerAction::RestartNode) actionLabel = TR("node_banner_restart");
|
else if (banner.action == ui::NodeBannerAction::RestartNode) actionLabel = TR("node_banner_restart");
|
||||||
|
else if (banner.action == ui::NodeBannerAction::OpenConfigFolder) actionLabel = TR("node_banner_open_config");
|
||||||
|
else if (banner.action == ui::NodeBannerAction::RecoverLegacyFunds) actionLabel = TR("node_banner_recover");
|
||||||
|
|
||||||
// The NoPeers case deliberately carries no passthrough detail (connection_status is just
|
// The NoPeers case deliberately carries no passthrough detail (connection_status is just
|
||||||
// "Connected" there, which reads as a contradiction under the "no peers" headline). Show a
|
// "Connected" there, which reads as a contradiction under the "no peers" headline). Show a
|
||||||
// translated, actionable hint instead; every other reason keeps its live status/error text.
|
// translated, actionable hint instead; every other reason keeps its live status/error text.
|
||||||
const std::string detailText = (banner.reason == ui::NodeBannerReason::NoPeers)
|
std::string detailText;
|
||||||
? std::string(TR("node_banner_no_peers_detail"))
|
if (banner.reason == ui::NodeBannerReason::NoPeers) detailText = TR("node_banner_no_peers_detail");
|
||||||
: banner.detail;
|
else if (banner.reason == ui::NodeBannerReason::AuthFailed) detailText = TR("node_banner_auth_detail");
|
||||||
|
else if (banner.reason == ui::NodeBannerReason::LegacyFundsStuck) detailText = TR("node_banner_legacy_detail");
|
||||||
|
else detailText = banner.detail;
|
||||||
|
|
||||||
const float padX = ui::Layout::spacingLg();
|
const float padX = ui::Layout::spacingLg();
|
||||||
|
|
||||||
@@ -2801,6 +2878,12 @@ void App::renderNodeStatusBanner()
|
|||||||
if (m::TactileButton(actionLabel, ImVec2(btnW, btnH))) {
|
if (m::TactileButton(actionLabel, ImVec2(btnW, btnH))) {
|
||||||
if (banner.action == ui::NodeBannerAction::RestartNode) restartDaemon();
|
if (banner.action == ui::NodeBannerAction::RestartNode) restartDaemon();
|
||||||
else if (banner.action == ui::NodeBannerAction::Reconnect) tryConnect();
|
else if (banner.action == ui::NodeBannerAction::Reconnect) tryConnect();
|
||||||
|
else if (banner.action == ui::NodeBannerAction::RecoverLegacyFunds) showSeedMigrationDialog();
|
||||||
|
else if (banner.action == ui::NodeBannerAction::OpenConfigFolder) {
|
||||||
|
const std::string conf = rpc::Connection::getDefaultConfPath();
|
||||||
|
util::Platform::openFolder(std::filesystem::path(conf).parent_path().string(),
|
||||||
|
/*createIfMissing=*/false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3050,50 +3133,28 @@ void App::renderStatusBar()
|
|||||||
} else if (rpc_busy_gate_->busy()) {
|
} else if (rpc_busy_gate_->busy()) {
|
||||||
// The node holds back our requests (a long operation holds its cs_main): data may be stale.
|
// The node holds back our requests (a long operation holds its cs_main): data may be stale.
|
||||||
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(ui::material::Warning()), "%s", TR("sb_node_busy"));
|
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(ui::material::Warning()), "%s", TR("sb_node_busy"));
|
||||||
|
} else if (isLiteBuild() && !state_.sync.lite_error.empty()) {
|
||||||
|
// Lite scan/server failure: show the error instead of a frozen 'Syncing 99.9%'.
|
||||||
|
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(ui::material::Error()), TR("sb_lite_sync_error"),
|
||||||
|
state_.sync.lite_error.c_str());
|
||||||
} else if (state_.sync.syncing) {
|
} else if (state_.sync.syncing) {
|
||||||
int chainTip = state_.longestchain > 0 ? state_.longestchain : state_.sync.headers;
|
int chainTip = state_.longestchain > 0 ? state_.longestchain : state_.sync.headers;
|
||||||
int blocksLeft = chainTip - state_.sync.blocks;
|
int blocksLeft = chainTip - state_.sync.blocks;
|
||||||
if (blocksLeft < 0) blocksLeft = 0;
|
if (blocksLeft < 0) blocksLeft = 0;
|
||||||
|
|
||||||
// Calculate sync speed (blocks/sec) using a smoothed rolling average
|
// App-owned smoothed sync-rate estimator: self-resets after a rescan/restart height drop (and on
|
||||||
static int s_prev_blocks = 0;
|
// connect/disconnect via the pending flag), so the ETA never goes stale/absurd ("412h"); days-aware.
|
||||||
static double s_prev_time = 0.0;
|
if (sync_eta_reset_pending_.exchange(false)) sync_eta_.reset();
|
||||||
static double s_blocks_per_sec = 0.0;
|
sync_eta_.update(state_.sync.blocks, ImGui::GetTime());
|
||||||
double now = ImGui::GetTime();
|
const ImVec4 syncCol = ImGui::ColorConvertU32ToFloat4(ui::material::Warning());
|
||||||
if (s_prev_time == 0.0) {
|
const long long etaSec = sync_eta_.etaSeconds(blocksLeft);
|
||||||
// First sample — seed baseline, don't compute rate yet
|
if (etaSec >= 0) {
|
||||||
s_prev_blocks = state_.sync.blocks;
|
|
||||||
s_prev_time = now;
|
|
||||||
} else if (state_.sync.blocks > s_prev_blocks) {
|
|
||||||
double dt = now - s_prev_time;
|
|
||||||
if (dt > 0.5) {
|
|
||||||
double raw = (state_.sync.blocks - s_prev_blocks) / dt;
|
|
||||||
// Exponential smoothing (alpha ~0.3 per update)
|
|
||||||
s_blocks_per_sec = s_blocks_per_sec > 0.0
|
|
||||||
? s_blocks_per_sec * 0.7 + raw * 0.3
|
|
||||||
: raw;
|
|
||||||
s_prev_blocks = state_.sync.blocks;
|
|
||||||
s_prev_time = now;
|
|
||||||
}
|
|
||||||
} else if (now - s_prev_time > 10.0) {
|
|
||||||
// No new blocks for 10s — decay the displayed rate
|
|
||||||
s_blocks_per_sec *= 0.5;
|
|
||||||
s_prev_time = now;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (s_blocks_per_sec > 0.1) {
|
|
||||||
int eta_sec = (int)(blocksLeft / s_blocks_per_sec);
|
|
||||||
char eta[32];
|
char eta[32];
|
||||||
if (eta_sec >= 3600)
|
util::formatEta(etaSec, eta, sizeof(eta));
|
||||||
snprintf(eta, sizeof(eta), "%dh %dm", eta_sec / 3600, (eta_sec % 3600) / 60);
|
ImGui::TextColored(syncCol, TR("sb_syncing_eta"),
|
||||||
else if (eta_sec >= 60)
|
state_.sync.verification_progress * 100.0, blocksLeft, sync_eta_.rate(), eta);
|
||||||
snprintf(eta, sizeof(eta), "%dm %ds", eta_sec / 60, eta_sec % 60);
|
|
||||||
else
|
|
||||||
snprintf(eta, sizeof(eta), "%ds", eta_sec);
|
|
||||||
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), TR("sb_syncing_eta"),
|
|
||||||
state_.sync.verification_progress * 100.0, blocksLeft, s_blocks_per_sec, eta);
|
|
||||||
} else {
|
} else {
|
||||||
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), TR("sb_syncing_basic"),
|
ImGui::TextColored(syncCol, TR("sb_syncing_basic"),
|
||||||
state_.sync.verification_progress * 100.0, blocksLeft);
|
state_.sync.verification_progress * 100.0, blocksLeft);
|
||||||
}
|
}
|
||||||
} else if (state_.connected && !isLiteBuild()) {
|
} else if (state_.connected && !isLiteBuild()) {
|
||||||
@@ -3312,7 +3373,7 @@ void App::renderStatusBar()
|
|||||||
ImVec2(bellMax.x - r, bellMin.y + r), r, dotCol);
|
ImVec2(bellMax.x - r, bellMin.y + r), r, dotCol);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (clicked) {
|
if (clicked || notes.consumeHistoryRequest()) { // bell click, or a click on the toast itself
|
||||||
alerts_seen_total_ = notes.totalPushed(); // mark everything currently shown as seen
|
alerts_seen_total_ = notes.totalPushed(); // mark everything currently shown as seen
|
||||||
ImGui::OpenPopup("##AlertHistoryPopup");
|
ImGui::OpenPopup("##AlertHistoryPopup");
|
||||||
}
|
}
|
||||||
@@ -3459,14 +3520,20 @@ void App::renderLiteFirstRunPrompt()
|
|||||||
ImGui::SetNextWindowPos(ImVec2(vp->Pos.x + vp->Size.x * 0.5f, vp->Pos.y + vp->Size.y * 0.5f),
|
ImGui::SetNextWindowPos(ImVec2(vp->Pos.x + vp->Size.x * 0.5f, vp->Pos.y + vp->Size.y * 0.5f),
|
||||||
ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
|
ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
|
||||||
|
|
||||||
|
// Best-effort zero of the seed-word statics (they hold the whole phrase) before they're dropped.
|
||||||
|
auto wipeStr = [](std::string& s) { if (!s.empty()) sodium_memzero(&s[0], s.size()); s.clear(); };
|
||||||
|
auto wipeWords = [&](std::vector<std::string>& v) { for (auto& w : v) wipeStr(w); v.clear(); };
|
||||||
|
auto wipeChips = [&](std::vector<std::pair<std::string, bool>>& v) {
|
||||||
|
for (auto& c : v) { wipeStr(c.first); c.second = false; } v.clear();
|
||||||
|
};
|
||||||
auto finish = [&]() {
|
auto finish = [&]() {
|
||||||
wallet::secureWipeLiteSecret(seed);
|
wallet::secureWipeLiteSecret(seed);
|
||||||
sodium_memzero(restoreSeed, sizeof(restoreSeed));
|
sodium_memzero(restoreSeed, sizeof(restoreSeed));
|
||||||
restoreBirthday = 0;
|
restoreBirthday = 0;
|
||||||
restoring = false;
|
restoring = false;
|
||||||
restoreErr.clear();
|
restoreErr.clear();
|
||||||
words.clear();
|
wipeWords(words);
|
||||||
chips.clear();
|
wipeChips(chips);
|
||||||
progress = 0;
|
progress = 0;
|
||||||
step = 0;
|
step = 0;
|
||||||
skipConfirm = false;
|
skipConfirm = false;
|
||||||
@@ -3574,7 +3641,7 @@ void App::renderLiteFirstRunPrompt()
|
|||||||
ImGui::Spacing(); ImGui::Spacing();
|
ImGui::Spacing(); ImGui::Spacing();
|
||||||
|
|
||||||
if (ui::material::TactileButton(TR("appx_ive_written_it_down"), ImVec2(btnW, 0))) {
|
if (ui::material::TactileButton(TR("appx_ive_written_it_down"), ImVec2(btnW, 0))) {
|
||||||
chips.clear();
|
wipeChips(chips);
|
||||||
for (const auto& w : words) chips.emplace_back(w, false);
|
for (const auto& w : words) chips.emplace_back(w, false);
|
||||||
std::mt19937 rng{std::random_device{}()};
|
std::mt19937 rng{std::random_device{}()};
|
||||||
std::shuffle(chips.begin(), chips.end(), rng);
|
std::shuffle(chips.begin(), chips.end(), rng);
|
||||||
@@ -3585,7 +3652,7 @@ void App::renderLiteFirstRunPrompt()
|
|||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
if (ui::material::TactileButton(TR("appx_copy"), ImVec2(80 * dp, 0))) copySecretToClipboard(seed);
|
if (ui::material::TactileButton(TR("appx_copy"), ImVec2(80 * dp, 0))) copySecretToClipboard(seed);
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
if (ui::material::TactileButton(skipConfirm ? TR("appx_skip_anyway") : TR("appx_skip"), ImVec2(120, 0))) {
|
if (ui::material::TactileButton(skipConfirm ? TR("appx_skip_anyway") : TR("appx_skip"), ImVec2(120.0f * ui::Layout::dpiScale(), 0))) {
|
||||||
if (!skipConfirm) {
|
if (!skipConfirm) {
|
||||||
skipConfirm = true; // require a second, deliberate click
|
skipConfirm = true; // require a second, deliberate click
|
||||||
} else {
|
} else {
|
||||||
@@ -3648,9 +3715,9 @@ void App::renderLiteFirstRunPrompt()
|
|||||||
}
|
}
|
||||||
if (!verified) ImGui::EndDisabled();
|
if (!verified) ImGui::EndDisabled();
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
if (ui::material::TactileButton(TR("appx_back"), ImVec2(80, 0))) { skipConfirm = false; step = 1; }
|
if (ui::material::TactileButton(TR("appx_back"), ImVec2(80.0f * ui::Layout::dpiScale(), 0))) { skipConfirm = false; step = 1; }
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
if (ui::material::TactileButton(skipConfirm ? TR("appx_skip_anyway") : TR("appx_skip"), ImVec2(120, 0))) {
|
if (ui::material::TactileButton(skipConfirm ? TR("appx_skip_anyway") : TR("appx_skip"), ImVec2(120.0f * ui::Layout::dpiScale(), 0))) {
|
||||||
if (!skipConfirm) {
|
if (!skipConfirm) {
|
||||||
skipConfirm = true; // require a second, deliberate click
|
skipConfirm = true; // require a second, deliberate click
|
||||||
} else {
|
} else {
|
||||||
@@ -3747,7 +3814,7 @@ void App::renderLiteFirstRunPrompt()
|
|||||||
}
|
}
|
||||||
ImGui::EndDisabled();
|
ImGui::EndDisabled();
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
if (ui::material::TactileButton(TR("appx_back"), ImVec2(80, 0))) {
|
if (ui::material::TactileButton(TR("appx_back"), ImVec2(80.0f * ui::Layout::dpiScale(), 0))) {
|
||||||
sodium_memzero(restoreSeed, sizeof(restoreSeed));
|
sodium_memzero(restoreSeed, sizeof(restoreSeed));
|
||||||
restoreErr.clear();
|
restoreErr.clear();
|
||||||
step = 0;
|
step = 0;
|
||||||
@@ -3785,12 +3852,13 @@ void App::renderLiteUnlockPrompt()
|
|||||||
const float btnW = 130.0f * dp;
|
const float btnW = 130.0f * dp;
|
||||||
bool doUnlock = ui::material::TactileButton(TR("lite_unlock_btn"), ImVec2(btnW, 0)) || entered;
|
bool doUnlock = ui::material::TactileButton(TR("lite_unlock_btn"), ImVec2(btnW, 0)) || entered;
|
||||||
if (doUnlock) {
|
if (doUnlock) {
|
||||||
const bool ok = lite_wallet_->unlockWallet(pass);
|
std::string unlockErr;
|
||||||
|
const bool ok = unlockLiteWallet(std::string(pass), &unlockErr);
|
||||||
sodium_memzero(pass, sizeof(pass));
|
sodium_memzero(pass, sizeof(pass));
|
||||||
if (ok) ui::Notifications::instance().success(TR("lite_unlock_ok"), 5.0f);
|
if (ok) ui::Notifications::instance().success(TR("lite_unlock_ok"), 5.0f);
|
||||||
else {
|
else {
|
||||||
wallet::liteLog(std::string("Unlock failed: ") + TR("lite_unlock_failed"));
|
wallet::liteLog(std::string("Unlock failed: ") + unlockErr);
|
||||||
ui::Notifications::instance().error(TR("lite_unlock_failed"));
|
ui::Notifications::instance().error(unlockErr);
|
||||||
}
|
}
|
||||||
lite_unlock_prompt_ = false;
|
lite_unlock_prompt_ = false;
|
||||||
ImGui::CloseCurrentPopup();
|
ImGui::CloseCurrentPopup();
|
||||||
@@ -3892,6 +3960,85 @@ static void RenderScanHeightControl(char* buf, size_t bufSize, int tipHeight, bo
|
|||||||
ImGui::EndDisabled();
|
ImGui::EndDisabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void App::renderImportWalletFileDialog()
|
||||||
|
{
|
||||||
|
namespace m = ui::material;
|
||||||
|
float dp = ui::Layout::dpiScale();
|
||||||
|
|
||||||
|
m::OverlayDialogSpec ov;
|
||||||
|
ov.title = TR("import_wallet_file_title");
|
||||||
|
ov.p_open = &show_import_wallet_file_;
|
||||||
|
ov.style = m::OverlayStyle::BlurFloat;
|
||||||
|
ov.cardWidth = 640.0f;
|
||||||
|
ov.idSuffix = "importwalletfile";
|
||||||
|
if (!m::BeginOverlayDialog(ov)) return;
|
||||||
|
|
||||||
|
if (ImGui::IsWindowAppearing()) {
|
||||||
|
import_wallet_file_done_ = false;
|
||||||
|
import_wallet_file_ok_ = false;
|
||||||
|
import_wallet_file_status_.clear();
|
||||||
|
}
|
||||||
|
if (ImGui::IsKeyPressed(ImGuiKey_Escape) && !import_wallet_file_busy_) show_import_wallet_file_ = false;
|
||||||
|
|
||||||
|
m::DialogWarningHeader(TR("import_wallet_file_note"));
|
||||||
|
ImGui::Spacing();
|
||||||
|
if (!state_.connected) {
|
||||||
|
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(m::OnSurfaceMedium()), "%s", TR("import_key_need_node"));
|
||||||
|
ImGui::Spacing();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::TextUnformatted(TR("import_wallet_file_path_label"));
|
||||||
|
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x);
|
||||||
|
ImGui::BeginDisabled(import_wallet_file_busy_);
|
||||||
|
ImGui::InputText("##importwalletpath", import_wallet_file_path_, sizeof(import_wallet_file_path_));
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
|
||||||
|
if (!import_wallet_file_busy_) {
|
||||||
|
if (m::TactileButton(TR("paste"), ImVec2(104 * dp, 0))) {
|
||||||
|
const char* clip = ImGui::GetClipboardText();
|
||||||
|
if (clip) {
|
||||||
|
std::string t = dragonx::util::trimmedClipboardText(clip);
|
||||||
|
snprintf(import_wallet_file_path_, sizeof(import_wallet_file_path_), "%s", t.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (m::TactileButton(TR("clear"), ImVec2(104 * dp, 0))) import_wallet_file_path_[0] = '\0';
|
||||||
|
}
|
||||||
|
ImGui::Spacing();
|
||||||
|
|
||||||
|
if (import_wallet_file_busy_) {
|
||||||
|
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(m::OnSurfaceMedium()), "%s%s",
|
||||||
|
TR("import_wallet_file_importing"), m::LoadingDots());
|
||||||
|
ImGui::Spacing();
|
||||||
|
} else if (import_wallet_file_done_) {
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(
|
||||||
|
import_wallet_file_ok_ ? m::Success() : m::ReadableError()));
|
||||||
|
ImGui::TextWrapped("%s", import_wallet_file_status_.c_str());
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
ImGui::Spacing();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::BeginDisabled(import_wallet_file_busy_ || !state_.connected || import_wallet_file_path_[0] == '\0');
|
||||||
|
if (m::TactileButton(TR("import_wallet_file_import_btn"), ImVec2(190 * dp, 0))) {
|
||||||
|
import_wallet_file_busy_ = true;
|
||||||
|
import_wallet_file_done_ = false;
|
||||||
|
import_wallet_file_status_.clear();
|
||||||
|
importWalletFile(import_wallet_file_path_, [this](bool ok, const std::string& err) {
|
||||||
|
import_wallet_file_busy_ = false;
|
||||||
|
import_wallet_file_done_ = true;
|
||||||
|
import_wallet_file_ok_ = ok;
|
||||||
|
import_wallet_file_status_ = ok ? std::string(TR("import_wallet_file_ok")) : err;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::BeginDisabled(import_wallet_file_busy_);
|
||||||
|
if (m::TactileButton(TR("close"), ImVec2(110 * dp, 0))) show_import_wallet_file_ = false;
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
|
||||||
|
m::EndOverlayDialog();
|
||||||
|
}
|
||||||
|
|
||||||
void App::renderImportKeyDialog()
|
void App::renderImportKeyDialog()
|
||||||
{
|
{
|
||||||
namespace m = ui::material;
|
namespace m = ui::material;
|
||||||
@@ -4319,8 +4466,11 @@ void App::renderExportKeyDialog()
|
|||||||
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(m::OnSurfaceMedium()), "%s", TR("key_export_private_key"));
|
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(m::OnSurfaceMedium()), "%s", TR("key_export_private_key"));
|
||||||
ImGui::InputTextMultiline("##exportresult", export_result_, sizeof(export_result_),
|
ImGui::InputTextMultiline("##exportresult", export_result_, sizeof(export_result_),
|
||||||
ImVec2(-1, 60.0f * dp), ImGuiInputTextFlags_ReadOnly);
|
ImVec2(-1, 60.0f * dp), ImGuiInputTextFlags_ReadOnly);
|
||||||
if (m::TactileButton(TR("copy")))
|
if (m::TactileButton(TR("copy"))) {
|
||||||
ImGui::SetClipboardText(export_result_);
|
// A private key is a secret — route through the auto-clearing clipboard path (45s armed wipe
|
||||||
|
// + its own "copied, auto-clears" toast) instead of a plain, permanent clipboard write.
|
||||||
|
copySecretToClipboard(export_result_);
|
||||||
|
}
|
||||||
} else if (export_error_) {
|
} else if (export_error_) {
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::PushTextWrapPos(0.0f);
|
ImGui::PushTextWrapPos(0.0f);
|
||||||
@@ -4704,7 +4854,7 @@ void App::renderSeedBackupDialog()
|
|||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::TextWrapped("%s", TR("seed_backup_intro"));
|
ImGui::TextWrapped("%s", TR("seed_backup_intro"));
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ui::RenderSeedWordGrid(ui::SplitSeedWords(seed_backup_phrase_));
|
{ auto words = ui::SplitSeedWords(seed_backup_phrase_); ui::RenderSeedWordGrid(words); ui::WipeSeedWords(words); }
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
if (!seed_backup_status_.empty()) {
|
if (!seed_backup_status_.empty()) {
|
||||||
ImGui::PushTextWrapPos(0.0f);
|
ImGui::PushTextWrapPos(0.0f);
|
||||||
@@ -4759,6 +4909,9 @@ void App::renderSeedMigrationDialog()
|
|||||||
if (!seed_migration_seed_.empty())
|
if (!seed_migration_seed_.empty())
|
||||||
sodium_memzero(&seed_migration_seed_[0], seed_migration_seed_.size());
|
sodium_memzero(&seed_migration_seed_[0], seed_migration_seed_.size());
|
||||||
seed_migration_seed_.clear();
|
seed_migration_seed_.clear();
|
||||||
|
// Disarm the inline confirms so an armed state can never survive a close/reopen.
|
||||||
|
seed_migration_confirm_discard_ = false;
|
||||||
|
seed_migration_confirm_sweep_ = false;
|
||||||
};
|
};
|
||||||
auto close = [this, &wipeSeed]() { wipeSeed(); show_seed_migration_ = false; };
|
auto close = [this, &wipeSeed]() { wipeSeed(); show_seed_migration_ = false; };
|
||||||
|
|
||||||
@@ -4871,7 +5024,7 @@ void App::renderSeedMigrationDialog()
|
|||||||
// distinct artifact, not body copy. Purely visual — the words/derivation/logic are untouched.
|
// distinct artifact, not body copy. Purely visual — the words/derivation/logic are untouched.
|
||||||
{
|
{
|
||||||
ui::material::GlassSectionScope seedPanel;
|
ui::material::GlassSectionScope seedPanel;
|
||||||
ui::RenderSeedWordGrid(ui::SplitSeedWords(seed_migration_seed_));
|
{ auto words = ui::SplitSeedWords(seed_migration_seed_); ui::RenderSeedWordGrid(words); ui::WipeSeedWords(words); }
|
||||||
}
|
}
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::TextColored(kMedium, "%s", TR("mig_receive_addr"));
|
ImGui::TextColored(kMedium, "%s", TR("mig_receive_addr"));
|
||||||
@@ -4892,11 +5045,15 @@ void App::renderSeedMigrationDialog()
|
|||||||
}
|
}
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::Checkbox(TR("mig_backed_up"), &seed_migration_backed_up_);
|
ImGui::Checkbox(TR("mig_backed_up"), &seed_migration_backed_up_);
|
||||||
|
ImGui::Checkbox(TR("mig_understood"), &seed_migration_understood_);
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::OnSurfaceMedium());
|
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::OnSurfaceMedium());
|
||||||
ImGui::TextWrapped("%s", TR("mig_step1_done"));
|
ImGui::TextWrapped("%s", TR("mig_step1_done"));
|
||||||
ImGui::PopStyleColor();
|
ImGui::PopStyleColor();
|
||||||
if (!seed_migration_backed_up_) ImGui::BeginDisabled();
|
// Continue requires BOTH acknowledgements, and is suppressed while the Discard confirm is armed.
|
||||||
|
const bool ackOk = seed_migration_backed_up_ && seed_migration_understood_;
|
||||||
|
const bool continueDisabled = !ackOk || seed_migration_confirm_discard_;
|
||||||
|
if (continueDisabled) ImGui::BeginDisabled();
|
||||||
if (ui::material::TactileButton(TR("mig_continue_sweep"), ImVec2(170 * dp, 0))) {
|
if (ui::material::TactileButton(TR("mig_continue_sweep"), ImVec2(170 * dp, 0))) {
|
||||||
wipeSeed(); // the sweep/adopt steps only use the address, never the seed itself
|
wipeSeed(); // the sweep/adopt steps only use the address, never the seed itself
|
||||||
seed_migration_step_ = SeedMigrationStep::Sweep;
|
seed_migration_step_ = SeedMigrationStep::Sweep;
|
||||||
@@ -4904,10 +5061,22 @@ void App::renderSeedMigrationDialog()
|
|||||||
seed_migration_nofunds_confirmed_ = false;
|
seed_migration_nofunds_confirmed_ = false;
|
||||||
refreshSeedMigrationBalance();
|
refreshSeedMigrationBalance();
|
||||||
}
|
}
|
||||||
if (!seed_migration_backed_up_) ImGui::EndDisabled();
|
if (continueDisabled) ImGui::EndDisabled();
|
||||||
|
if (!ackOk && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
|
||||||
|
ui::material::Tooltip("%s", TR("mig_ack_required"));
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
if (ui::material::TactileButton(TR("mig_discard"), ImVec2(160 * dp, 0))) {
|
// Discard now takes a second click: first click arms a confirm, since it deletes the new wallet.
|
||||||
// Throw away the isolated new wallet + clear the pending state.
|
if (!seed_migration_confirm_discard_) {
|
||||||
|
if (ui::material::TactileButton(TR("mig_discard"), ImVec2(160 * dp, 0)))
|
||||||
|
seed_migration_confirm_discard_ = true;
|
||||||
|
}
|
||||||
|
if (seed_migration_confirm_discard_) {
|
||||||
|
ImGui::Spacing();
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::ReadableError()));
|
||||||
|
ImGui::TextWrapped("%s", TR("mig_discard_confirm"));
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
if (ui::material::TactileButton(TR("mig_discard_yes"), ImVec2(160 * dp, 0))) {
|
||||||
|
// Throw away the isolated new wallet + clear the pending state. (body unchanged)
|
||||||
if (!seed_migration_temp_dir_.empty()) {
|
if (!seed_migration_temp_dir_.empty()) {
|
||||||
std::error_code ec;
|
std::error_code ec;
|
||||||
std::filesystem::remove_all(seed_migration_temp_dir_, ec);
|
std::filesystem::remove_all(seed_migration_temp_dir_, ec);
|
||||||
@@ -4920,8 +5089,13 @@ void App::renderSeedMigrationDialog()
|
|||||||
}
|
}
|
||||||
seed_migration_temp_dir_.clear();
|
seed_migration_temp_dir_.clear();
|
||||||
seed_migration_dest_.clear();
|
seed_migration_dest_.clear();
|
||||||
|
seed_migration_confirm_discard_ = false;
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ui::material::TactileButton(TR("cancel"), ImVec2(110 * dp, 0)))
|
||||||
|
seed_migration_confirm_discard_ = false;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4972,7 +5146,7 @@ void App::renderSeedMigrationDialog()
|
|||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
if (!seed_migration_nofunds_confirmed_) ImGui::BeginDisabled();
|
if (!seed_migration_nofunds_confirmed_) ImGui::BeginDisabled();
|
||||||
if (ui::material::TactileButton(TR("mig_adopt_now"), ImVec2(190 * dp, 0)))
|
if (ui::material::TactileButton(TR("mig_adopt_now"), ImVec2(190 * dp, 0)))
|
||||||
beginAdoptSeedWallet();
|
verifyThenAdoptSeedWallet(); // fresh minconf-0, sync-aware balance check before the swap
|
||||||
if (!seed_migration_nofunds_confirmed_) ImGui::EndDisabled();
|
if (!seed_migration_nofunds_confirmed_) ImGui::EndDisabled();
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
if (ui::material::TactileButton(TR("refresh"), ImVec2(110 * dp, 0)))
|
if (ui::material::TactileButton(TR("refresh"), ImVec2(110 * dp, 0)))
|
||||||
@@ -4996,17 +5170,51 @@ void App::renderSeedMigrationDialog()
|
|||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
{
|
{
|
||||||
const bool canSweep = !state_.isLocked() && seed_migration_balance_ > 0.0;
|
const bool canSweep = !state_.isLocked() && seed_migration_balance_ > 0.0;
|
||||||
|
if (!canSweep) seed_migration_confirm_sweep_ = false; // never keep a stale armed state
|
||||||
|
// The sweep (z_shieldcoinbase / z_mergetoaddress) needs an unlocked wallet, so an encrypted
|
||||||
|
// wallet silently disabled the button. Offer an inline unlock instead of a dead end.
|
||||||
|
if (state_.isLocked()) {
|
||||||
|
static char s_mig_unlock_pass[128] = {0};
|
||||||
|
ImGui::TextWrapped("%s", TR("mig_unlock_prompt"));
|
||||||
|
ImGui::SetNextItemWidth(220 * dp);
|
||||||
|
const bool submit = ImGui::InputText("##mig_unlock", s_mig_unlock_pass, sizeof(s_mig_unlock_pass),
|
||||||
|
ImGuiInputTextFlags_Password | ImGuiInputTextFlags_EnterReturnsTrue);
|
||||||
|
ImGui::SameLine();
|
||||||
|
if ((ui::material::TactileButton(TR("mig_unlock_btn"), ImVec2(120 * dp, 0)) || submit)
|
||||||
|
&& s_mig_unlock_pass[0]) {
|
||||||
|
unlockWallet(s_mig_unlock_pass, 600);
|
||||||
|
sodium_memzero(s_mig_unlock_pass, sizeof(s_mig_unlock_pass)); // don't retain the secret
|
||||||
|
}
|
||||||
|
ImGui::Spacing();
|
||||||
|
}
|
||||||
|
if (!seed_migration_confirm_sweep_) {
|
||||||
if (!canSweep) ImGui::BeginDisabled();
|
if (!canSweep) ImGui::BeginDisabled();
|
||||||
if (ui::material::TactileButton(TR("mig_sweep_all"), ImVec2(170 * dp, 0)))
|
if (ui::material::TactileButton(TR("mig_sweep_all"), ImVec2(170 * dp, 0)))
|
||||||
beginSweepToSeedWallet();
|
seed_migration_confirm_sweep_ = true; // first click arms the confirm
|
||||||
if (!canSweep) ImGui::EndDisabled();
|
if (!canSweep) ImGui::EndDisabled();
|
||||||
}
|
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
if (ui::material::TactileButton(TR("refresh"), ImVec2(110 * dp, 0)))
|
if (ui::material::TactileButton(TR("refresh"), ImVec2(110 * dp, 0)))
|
||||||
refreshSeedMigrationBalance();
|
refreshSeedMigrationBalance();
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
if (ui::material::TactileButton(TR("mig_later"), ImVec2(100 * dp, 0)))
|
if (ui::material::TactileButton(TR("mig_later"), ImVec2(100 * dp, 0)))
|
||||||
close(); // pending state persists; resumes here next time
|
close(); // pending state persists; resumes here next time
|
||||||
|
} else {
|
||||||
|
// Armed: state the amount being moved before the single sweep transaction fires.
|
||||||
|
char cbuf[160]; snprintf(cbuf, sizeof(cbuf), TR("mig_sweep_confirm"), seed_migration_balance_);
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::ReadableError()));
|
||||||
|
ImGui::TextWrapped("%s", cbuf);
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
if (!canSweep) ImGui::BeginDisabled();
|
||||||
|
if (ui::material::TactileButton(TR("mig_sweep_confirm_yes"), ImVec2(170 * dp, 0))) {
|
||||||
|
seed_migration_confirm_sweep_ = false;
|
||||||
|
beginSweepToSeedWallet(); // unchanged
|
||||||
|
}
|
||||||
|
if (!canSweep) ImGui::EndDisabled();
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ui::material::TactileButton(TR("cancel"), ImVec2(110 * dp, 0)))
|
||||||
|
seed_migration_confirm_sweep_ = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5018,7 +5226,8 @@ void App::renderSeedMigrationDialog()
|
|||||||
|
|
||||||
case SeedMigrationStep::Confirming: {
|
case SeedMigrationStep::Confirming: {
|
||||||
const double dust = DRAGONX_DEFAULT_FEE; // treat <= one fee as "empty"
|
const double dust = DRAGONX_DEFAULT_FEE; // treat <= one fee as "empty"
|
||||||
const bool confirmed = seed_migration_sweep_confs_ >= 1;
|
const bool mined = seed_migration_sweep_confs_ >= 1; // remainder re-sweep stays offered once mined
|
||||||
|
const bool confirmed = dragonx::sweepConfsAreReorgSafe(seed_migration_sweep_confs_); // adopt needs reorg-safe depth
|
||||||
const bool legacyEmpty = seed_migration_legacy_remaining_ >= 0.0 &&
|
const bool legacyEmpty = seed_migration_legacy_remaining_ >= 0.0 &&
|
||||||
seed_migration_legacy_remaining_ <= dust;
|
seed_migration_legacy_remaining_ <= dust;
|
||||||
ImGui::TextWrapped("%s", TR("mig_confirming"));
|
ImGui::TextWrapped("%s", TR("mig_confirming"));
|
||||||
@@ -5038,25 +5247,47 @@ void App::renderSeedMigrationDialog()
|
|||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
if (confirmed && legacyEmpty) {
|
if (confirmed && legacyEmpty) {
|
||||||
if (ui::material::TactileButton(TR("mig_adopt"), ImVec2(180 * dp, 0)))
|
if (ui::material::TactileButton(TR("mig_adopt"), ImVec2(180 * dp, 0)))
|
||||||
beginAdoptSeedWallet();
|
verifyThenAdoptSeedWallet(); // re-reads confs + balance right before the daemon stop
|
||||||
} else if (confirmed && seed_migration_legacy_remaining_ >= 0.0 && !legacyEmpty) {
|
} else if (mined && seed_migration_legacy_remaining_ >= 0.0 && !legacyEmpty) {
|
||||||
// Only claim a remainder once the legacy balance was actually measured; while a sweep is
|
// Only claim a remainder once the legacy balance was actually measured; while a sweep is
|
||||||
// mined but the balance scan is still gated (node syncing), remaining stays at the -1.0
|
// mined but the balance scan is still gated (node syncing), remaining stays at the -1.0
|
||||||
// sentinel, so fall through to the waiting state rather than offering a sweep on an unknown.
|
// sentinel, so fall through to the waiting state rather than offering a sweep on an unknown.
|
||||||
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::ReadableError());
|
ImGui::PushStyleColor(ImGuiCol_Text, ui::material::ReadableError());
|
||||||
ImGui::TextWrapped("%s", TR("mig_remainder"));
|
ImGui::TextWrapped("%s", TR("mig_remainder"));
|
||||||
ImGui::PopStyleColor();
|
ImGui::PopStyleColor();
|
||||||
|
if (!seed_migration_confirm_sweep_) {
|
||||||
if (ui::material::TactileButton(TR("mig_sweep_remaining"), ImVec2(180 * dp, 0)))
|
if (ui::material::TactileButton(TR("mig_sweep_remaining"), ImVec2(180 * dp, 0)))
|
||||||
beginSweepToSeedWallet();
|
seed_migration_confirm_sweep_ = true; // first click arms
|
||||||
|
} else {
|
||||||
|
char rcbuf[160];
|
||||||
|
snprintf(rcbuf, sizeof(rcbuf), TR("mig_sweep_confirm"), seed_migration_legacy_remaining_);
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::ReadableError()));
|
||||||
|
ImGui::TextWrapped("%s", rcbuf);
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
if (ui::material::TactileButton(TR("mig_sweep_confirm_yes"), ImVec2(180 * dp, 0))) {
|
||||||
|
seed_migration_confirm_sweep_ = false;
|
||||||
|
beginSweepToSeedWallet(); // unchanged
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ui::material::TactileButton(TR("cancel"), ImVec2(110 * dp, 0)))
|
||||||
|
seed_migration_confirm_sweep_ = false;
|
||||||
|
}
|
||||||
|
} else if (mined && legacyEmpty) {
|
||||||
|
// Mined but not yet reorg-safe deep: say so instead of the misleading "waiting to be mined".
|
||||||
|
char dbuf[200]; snprintf(dbuf, sizeof(dbuf), TR("mig_waiting_depth"), dragonx::kSweepAdoptMinConfs);
|
||||||
|
ImGui::TextWrapped("%s", dbuf);
|
||||||
} else {
|
} else {
|
||||||
ImGui::TextColored(kMedium, "%s", TR("mig_waiting_mine"));
|
ImGui::TextColored(kMedium, "%s", TR("mig_waiting_mine"));
|
||||||
}
|
}
|
||||||
|
// Refresh / Later are hidden while the sweep confirm is armed, to keep that row uncluttered.
|
||||||
|
if (!seed_migration_confirm_sweep_) {
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
if (ui::material::TactileButton(TR("refresh"), ImVec2(110 * dp, 0)))
|
if (ui::material::TactileButton(TR("refresh"), ImVec2(110 * dp, 0)))
|
||||||
pollSweepStatus(/*forceBalance=*/true);
|
pollSweepStatus(/*forceBalance=*/true);
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
if (ui::material::TactileButton(TR("mig_later"), ImVec2(100 * dp, 0)))
|
if (ui::material::TactileButton(TR("mig_later"), ImVec2(100 * dp, 0)))
|
||||||
close(); // pending state + txid persist; resumes here next time
|
close(); // pending state + txid persist; resumes here next time
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5973,16 +6204,16 @@ bool App::startEmbeddedDaemon()
|
|||||||
// NB: this callback runs on EmbeddedDaemon's monitor thread — write via the locked setter.
|
// NB: this callback runs on EmbeddedDaemon's monitor thread — write via the locked setter.
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case daemon::EmbeddedDaemon::State::Starting:
|
case daemon::EmbeddedDaemon::State::Starting:
|
||||||
setDaemonStatus(TR("sb_starting_daemon"));
|
setDaemonStatus(util::trCopy("sb_starting_daemon"));
|
||||||
break;
|
break;
|
||||||
case daemon::EmbeddedDaemon::State::Running:
|
case daemon::EmbeddedDaemon::State::Running:
|
||||||
setDaemonStatus(TR("sb_dragonxd_running"));
|
setDaemonStatus(util::trCopy("sb_dragonxd_running"));
|
||||||
break;
|
break;
|
||||||
case daemon::EmbeddedDaemon::State::Stopping:
|
case daemon::EmbeddedDaemon::State::Stopping:
|
||||||
setDaemonStatus(TR("sb_dragonxd_stopping"));
|
setDaemonStatus(util::trCopy("sb_dragonxd_stopping"));
|
||||||
break;
|
break;
|
||||||
case daemon::EmbeddedDaemon::State::Stopped:
|
case daemon::EmbeddedDaemon::State::Stopped:
|
||||||
setDaemonStatus(TR("sb_dragonxd_stopped"));
|
setDaemonStatus(util::trCopy("sb_dragonxd_stopped"));
|
||||||
break;
|
break;
|
||||||
case daemon::EmbeddedDaemon::State::Error:
|
case daemon::EmbeddedDaemon::State::Error:
|
||||||
setDaemonStatus("Error: " + msg);
|
setDaemonStatus("Error: " + msg);
|
||||||
@@ -5991,6 +6222,14 @@ bool App::startEmbeddedDaemon()
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Compact-via-switch: the user chose to compact after switching from an external node to this built-in
|
||||||
|
// one. Arm -salvagewallet on this start so the fresh node rewrites wallet.dat into a compact file
|
||||||
|
// (-salvagewallet implies a rescan). One-shot: it fires only for the start that follows the switch.
|
||||||
|
if (compact_on_embedded_start_.exchange(false)) {
|
||||||
|
daemon_controller_->setSalvageOnNextStart(true);
|
||||||
|
user_initiated_rescan_ = true; // the implied rescan is wallet-triggered → its completion should toast
|
||||||
|
}
|
||||||
|
|
||||||
// Only a process we actually launched starts a new story: a start that finds a node already running (and
|
// Only a process we actually launched starts a new story: a start that finds a node already running (and
|
||||||
// launches nothing) must not wipe what the tracker knows about that node from its debug.log.
|
// launches nothing) must not wipe what the tracker knows about that node from its debug.log.
|
||||||
const bool launched = daemon_controller_->start(
|
const bool launched = daemon_controller_->start(
|
||||||
@@ -6308,6 +6547,7 @@ void App::rescanBlockchain()
|
|||||||
// actually observe the restarted daemon rescanning — so the first poll (which may still reach the
|
// actually observe the restarted daemon rescanning — so the first poll (which may still reach the
|
||||||
// pre-restart daemon and see rescanning=false) can't be misread as instant completion.
|
// pre-restart daemon and see rescanning=false) can't be misread as instant completion.
|
||||||
state_.sync.rescanning = true;
|
state_.sync.rescanning = true;
|
||||||
|
sync_eta_reset_pending_.store(true, std::memory_order_relaxed); // rescan drops height → reset ETA
|
||||||
rescan_confirmed_active_ = false;
|
rescan_confirmed_active_ = false;
|
||||||
user_initiated_rescan_ = true; // user-triggered rescan → its completion should toast
|
user_initiated_rescan_ = true; // user-triggered rescan → its completion should toast
|
||||||
state_.sync.rescan_progress = 0.0f;
|
state_.sync.rescan_progress = 0.0f;
|
||||||
@@ -8107,6 +8347,7 @@ std::string App::buildDiagnosticsReport()
|
|||||||
|
|
||||||
void App::restartDaemon()
|
void App::restartDaemon()
|
||||||
{
|
{
|
||||||
|
sync_eta_reset_pending_.store(true, std::memory_order_relaxed);
|
||||||
if (!supportsFullNodeLifecycleActions()) {
|
if (!supportsFullNodeLifecycleActions()) {
|
||||||
ui::Notifications::instance().warning(TR("appx_fullnode_lifecycle_unavailable_lite"));
|
ui::Notifications::instance().warning(TR("appx_fullnode_lifecycle_unavailable_lite"));
|
||||||
return;
|
return;
|
||||||
@@ -8117,6 +8358,7 @@ void App::restartDaemon()
|
|||||||
isUsingEmbeddedDaemon(), daemon_controller_ != nullptr, isEmbeddedDaemonRunning(), daemon_restarting_.load());
|
isUsingEmbeddedDaemon(), daemon_controller_ != nullptr, isEmbeddedDaemonRunning(), daemon_restarting_.load());
|
||||||
if (!decision.allowed) return;
|
if (!decision.allowed) return;
|
||||||
if (refuseIfNodeBusy()) return;
|
if (refuseIfNodeBusy()) return;
|
||||||
|
daemon_restart_pending_.store(false); // a restart is actually happening — clear the reminder
|
||||||
daemon_restarting_ = true;
|
daemon_restarting_ = true;
|
||||||
|
|
||||||
daemon_controller_->prepareLifecycleOperation(decision, settings_.get());
|
daemon_controller_->prepareLifecycleOperation(decision, settings_.get());
|
||||||
|
|||||||
@@ -42,12 +42,14 @@
|
|||||||
#include "resources/provision_state.h"
|
#include "resources/provision_state.h"
|
||||||
#include "util/background_sampler.h"
|
#include "util/background_sampler.h"
|
||||||
#include "util/coalescing_writer.h"
|
#include "util/coalescing_writer.h"
|
||||||
|
#include "util/sync_eta.h"
|
||||||
#include "util/fetch_backoff.h"
|
#include "util/fetch_backoff.h"
|
||||||
#include "util/idle_tracker.h"
|
#include "util/idle_tracker.h"
|
||||||
#include "util/pool_stats_service.h"
|
#include "util/pool_stats_service.h"
|
||||||
#include "wallet/wallet_capabilities.h"
|
#include "wallet/wallet_capabilities.h"
|
||||||
#include "chat/chat_service.h"
|
#include "chat/chat_service.h"
|
||||||
#include "chat/chat_database.h"
|
#include "chat/chat_database.h"
|
||||||
|
#include "chat/chat_sensitive_vault.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"
|
||||||
@@ -113,6 +115,7 @@ class App {
|
|||||||
public:
|
public:
|
||||||
App();
|
App();
|
||||||
void wipeSecrets(); // scrub all resident secret buffers; called from ~App() AND the forced-exit path (L-05)
|
void wipeSecrets(); // scrub all resident secret buffers; called from ~App() AND the forced-exit path (L-05)
|
||||||
|
void wipeDisplayedSecrets(); // scrub + close any on-screen secret (seed/key displays) on wallet lock
|
||||||
~App();
|
~App();
|
||||||
|
|
||||||
// Non-copyable
|
// Non-copyable
|
||||||
@@ -302,6 +305,9 @@ public:
|
|||||||
wallet::LiteWalletController* liteWallet() { return lite_wallet_.get(); }
|
wallet::LiteWalletController* liteWallet() { return lite_wallet_.get(); }
|
||||||
// HushChat service (identity + in-memory message store); the Chat tab reads its store.
|
// HushChat service (identity + in-memory message store); the Chat tab reads its store.
|
||||||
chat::ChatService& chatService() { return chat_service_; }
|
chat::ChatService& chatService() { return chat_service_; }
|
||||||
|
// Public forwarder for the UI: the wallet z-address others use to reach this user in chat. Call on
|
||||||
|
// click (not per-frame) — the underlying chatReplyZaddr() may persist a freshly chosen address.
|
||||||
|
std::string chatMyAddress() { return chatReplyZaddr(); }
|
||||||
// Why the Chat tab has no identity yet, so it can show the right prompt instead of always
|
// Why the Chat tab has no identity yet, so it can show the right prompt instead of always
|
||||||
// "unlock your wallet". Locked = encrypted & locked (seed unreadable); Preparing = unlocked and
|
// "unlock your wallet". Locked = encrypted & locked (seed unreadable); Preparing = unlocked and
|
||||||
// deriving the identity (fetch in flight, or waiting on the node / address list); Unavailable =
|
// deriving the identity (fetch in flight, or waiting on the node / address list); Unavailable =
|
||||||
@@ -316,6 +322,25 @@ public:
|
|||||||
// Send a contact request into an existing conversation (used to retry a failed request in place).
|
// Send a contact request into an existing conversation (used to retry a failed request in place).
|
||||||
void sendContactRequestForCid(const std::string& cid, const std::string& peerZaddr,
|
void sendContactRequestForCid(const std::string& cid, const std::string& peerZaddr,
|
||||||
const std::string& text);
|
const std::string& text);
|
||||||
|
// Broadcast a self-addressed Control memo (op "del"/"res") so every device and a fresh seed import
|
||||||
|
// hides/restores this conversation. `label` is a plaintext hint (peer address/name) carried on-chain.
|
||||||
|
// Reuses the chat send coordinator + the 0-value kChatMinFeeDrgx z_sendmany path; no UI echo bubble.
|
||||||
|
void sendChatControl(const std::string& conversationId, const std::string& op, const std::string& label);
|
||||||
|
|
||||||
|
// --- Sensitive conversations (per-conversation local-key encryption) ---
|
||||||
|
enum class ChatVaultState { NotConfigured, Locked, Unlocked };
|
||||||
|
ChatVaultState chatSensitiveVaultState() const;
|
||||||
|
bool chatSensitiveVaultConfigured() const; // a vault file exists (regardless of lock state)
|
||||||
|
// Create the vault (first-time setup) and unlock it. Returns false if one already exists or on error.
|
||||||
|
bool createChatSensitiveVault(const std::string& passphrase);
|
||||||
|
// Provide the vault key to the chat DB so sensitive rows decrypt; also pulls them into the store.
|
||||||
|
bool unlockChatSensitiveVault(const std::string& passphrase);
|
||||||
|
void lockChatSensitiveVault(); // drop the key; sensitive threads vanish from the view
|
||||||
|
// Mark/unmark a conversation sensitive. Requires the vault UNLOCKED (migrates its rows to/from the
|
||||||
|
// vault key) and broadcasts the on-chain "sensitive"/"unsensitive" signal. `peerAddr` labels it.
|
||||||
|
bool markChatConversationSensitive(const std::string& cid, const std::string& peerAddr);
|
||||||
|
void unmarkChatConversationSensitive(const std::string& cid);
|
||||||
|
|
||||||
// Debug/sweep convenience: give the Chat tab a demo identity + a few sample conversations
|
// Debug/sweep convenience: give the Chat tab a demo identity + a few sample conversations
|
||||||
// (in-memory only, not persisted) so the screenshot sweep captures the populated UI. No-op when
|
// (in-memory only, not persisted) so the screenshot sweep captures the populated UI. No-op when
|
||||||
// the chat feature is off. Returns true when it had to fabricate the (random) demo identity
|
// the chat feature is off. Returns true when it had to fabricate the (random) demo identity
|
||||||
@@ -328,6 +353,10 @@ public:
|
|||||||
// Lock the lite wallet AND immediately tear down the chat session (the lite backend `lock`
|
// Lock the lite wallet AND immediately tear down the chat session (the lite backend `lock`
|
||||||
// doesn't update state_.locked until the next poll, so chat secrets would otherwise linger).
|
// doesn't update state_.locked until the next poll, so chat secrets would otherwise linger).
|
||||||
bool lockLiteWallet();
|
bool lockLiteWallet();
|
||||||
|
// Unlock the lite wallet with the same escalating attempt-lockout as the full node (persisted across restarts)
|
||||||
|
// and set state_.locked=false right away on success (the poll lags). Takes the passphrase by value and wipes
|
||||||
|
// it. On failure *err (if given) holds a user-facing reason ("Unlock failed..." or the lockout wait).
|
||||||
|
bool unlockLiteWallet(std::string passphrase, std::string* err = nullptr);
|
||||||
// (Re)build the lite controller from current settings so a changed lite-server selection
|
// (Re)build the lite controller from current settings so a changed lite-server selection
|
||||||
// takes effect. No-op on non-lite/unlinked builds; preserves a live wallet (see app.cpp).
|
// takes effect. No-op on non-lite/unlinked builds; preserves a live wallet (see app.cpp).
|
||||||
void rebuildLiteWallet(bool force = false);
|
void rebuildLiteWallet(bool force = false);
|
||||||
@@ -386,6 +415,11 @@ public:
|
|||||||
void startMining(int threads);
|
void startMining(int threads);
|
||||||
void stopMining();
|
void stopMining();
|
||||||
bool isMiningToggleInProgress() const { return mining_toggle_in_progress_.load(std::memory_order_relaxed); }
|
bool isMiningToggleInProgress() const { return mining_toggle_in_progress_.load(std::memory_order_relaxed); }
|
||||||
|
// Persistent mining error (last start/stop/pool failure) so a failed miner isn't just a fading toast.
|
||||||
|
const std::string& miningLastError() const { return mining_last_error_; }
|
||||||
|
bool miningErrorIsAntivirus() const { return mining_error_av_; }
|
||||||
|
void clearMiningError() { mining_last_error_.clear(); mining_error_av_ = false; }
|
||||||
|
void showAntivirusHelp() { pending_antivirus_dialog_ = true; } // Windows-only dialog; inert elsewhere
|
||||||
|
|
||||||
// Pool mining (xmrig)
|
// Pool mining (xmrig)
|
||||||
void startPoolMining(int threads);
|
void startPoolMining(int threads);
|
||||||
@@ -439,6 +473,9 @@ public:
|
|||||||
// Address operations
|
// Address operations
|
||||||
void createNewZAddress(std::function<void(const std::string&)> callback = nullptr);
|
void createNewZAddress(std::function<void(const std::string&)> callback = nullptr);
|
||||||
void createNewTAddress(std::function<void(const std::string&)> callback = nullptr);
|
void createNewTAddress(std::function<void(const std::string&)> callback = nullptr);
|
||||||
|
// The daemon's actual reason the last new-address RPC failed (friendly-mapped), or "" on success. Lets
|
||||||
|
// the UI show e.g. "HD seed not found" (legacy/degraded wallet) instead of a generic "couldn't create".
|
||||||
|
const std::string& lastAddressError() const { return last_address_error_; }
|
||||||
|
|
||||||
// Hide/unhide addresses from the address list (persisted in settings)
|
// Hide/unhide addresses from the address list (persisted in settings)
|
||||||
void hideAddress(const std::string& addr);
|
void hideAddress(const std::string& addr);
|
||||||
@@ -548,10 +585,11 @@ public:
|
|||||||
private:
|
private:
|
||||||
App& app_;
|
App& app_;
|
||||||
};
|
};
|
||||||
// Main thread, right before rpc_->disconnect(): abort rpc_'s in-flight call if it is a maintenance
|
// Main thread, right before rpc_->disconnect(): abort rpc_'s in-flight call (any call, not just a
|
||||||
// call, so the disconnect doesn't freeze the UI for up to its 120 s, and refuse maintenance calls that
|
// maintenance one: disconnect() needs the curl lock the call holds, which would freeze the UI), and
|
||||||
// start before the next connect (onConnected() lifts that). Ordinary calls are left to finish (at most
|
// refuse maintenance calls that start before the next connect (onConnected() lifts that). The abort
|
||||||
// 30 s, as before), so a user's own send is never cut off mid-call.
|
// flag is cleared by the next tryConnect(). A user send aborted mid-flight is handled as a lost reply
|
||||||
|
// ("outcome unknown — check History"), never a double-send.
|
||||||
void abortMaintenanceCalls();
|
void abortMaintenanceCalls();
|
||||||
// Whether a whole-wallet scan the user didn't ask for may be sent now: a connected, unlocked, synced
|
// Whether a whole-wallet scan the user didn't ask for may be sent now: a connected, unlocked, synced
|
||||||
// full node that is past warmup, not rescanning and not busy.
|
// full node that is past warmup, not rescanning and not busy.
|
||||||
@@ -632,6 +670,9 @@ public:
|
|||||||
// Dialog triggers (used by settings page to open modal dialogs)
|
// Dialog triggers (used by settings page to open modal dialogs)
|
||||||
void showImportKeyDialog() { import_view_mode_ = false; show_import_key_ = true; } // spending
|
void showImportKeyDialog() { import_view_mode_ = false; show_import_key_ = true; } // spending
|
||||||
void showImportViewingKeyDialog() { import_view_mode_ = true; show_import_key_ = true; } // watch-only
|
void showImportViewingKeyDialog() { import_view_mode_ = true; show_import_key_ = true; } // watch-only
|
||||||
|
void showImportWalletFileDialog() { show_import_wallet_file_ = true; } // import a whole legacy key dump
|
||||||
|
// Import every key from an old wallet's z_exportwallet/dumpwallet dump file (z_importwallet, one rescan).
|
||||||
|
void importWalletFile(const std::string& path, std::function<void(bool, const std::string&)> callback);
|
||||||
void showExportKeyDialog() { show_export_key_ = true; }
|
void showExportKeyDialog() { show_export_key_ = true; }
|
||||||
void showBackupDialog() { show_backup_ = true; }
|
void showBackupDialog() { show_backup_ = true; }
|
||||||
void showSeedBackupDialog() { show_seed_backup_ = true; }
|
void showSeedBackupDialog() { show_seed_backup_ = true; }
|
||||||
@@ -760,6 +801,23 @@ public:
|
|||||||
* Shows "Restarting daemon..." in the loading overlay while the daemon cycles.
|
* Shows "Restarting daemon..." in the loading overlay while the daemon cycles.
|
||||||
*/
|
*/
|
||||||
void restartDaemon();
|
void restartDaemon();
|
||||||
|
// Compact wallet.dat to reclaim the space freed by consolidating notes: Berkeley DB never shrinks a
|
||||||
|
// file in place, so a restart with -salvagewallet rewrites it into a fresh, compact file (keeping a
|
||||||
|
// backup). -salvagewallet implies a full rescan, so this restarts the node and re-scans the chain.
|
||||||
|
void compactWallet();
|
||||||
|
// True when Compact can actually run: a full-node build whose EMBEDDED node the wallet manages (it
|
||||||
|
// restarts it with -salvagewallet). False for lite or when connected to an external/standalone dragonxd.
|
||||||
|
bool canCompactWallet() const {
|
||||||
|
return supportsFullNodeLifecycleActions() && isUsingEmbeddedDaemon() && daemon_controller_ != nullptr;
|
||||||
|
}
|
||||||
|
// Compact when connected to an EXTERNAL node: stop it (graceful RPC "stop"), switch the wallet to its
|
||||||
|
// built-in node, and start that with -salvagewallet so it compacts. Needs a full-node build with
|
||||||
|
// embedded-daemon support. A one-shot flag arms the salvage on the resulting embedded start.
|
||||||
|
void compactViaEmbeddedSwitch();
|
||||||
|
std::atomic<bool> compact_on_embedded_start_{false};
|
||||||
|
// A newer daemon binary is installed but the running node is still the old one until a restart.
|
||||||
|
void markDaemonRestartPending() { daemon_restart_pending_.store(true); }
|
||||||
|
bool daemonRestartPending() const { return daemon_restart_pending_.load(); }
|
||||||
// Switch the active wallet: persist the new -wallet=<name>, stop the node, restart on it
|
// Switch the active wallet: persist the new -wallet=<name>, stop the node, restart on it
|
||||||
// (rescan only if it was never synced in this datadir). Per-wallet data follows automatically
|
// (rescan only if it was never synced in this datadir). Per-wallet data follows automatically
|
||||||
// via the identity-scoped caches (P1). No-op if that wallet is already active.
|
// via the identity-scoped caches (P1). No-op if that wallet is already active.
|
||||||
@@ -918,6 +976,8 @@ private:
|
|||||||
// curve on every failed path (a PIN RPC failure used to skip it).
|
// curve on every failed path (a PIN RPC failure used to skip it).
|
||||||
void applyUnlockSuccess(const std::string& passphrase, int timeout);
|
void applyUnlockSuccess(const std::string& passphrase, int timeout);
|
||||||
void applyUnlockFailure(const std::string& errorMessage);
|
void applyUnlockFailure(const std::string& errorMessage);
|
||||||
|
// Restore the persisted unlock-failure count + lockout (once per lock-screen session, per wallet).
|
||||||
|
void restoreLockoutState();
|
||||||
// Non-credential failure (busy node, unknown error): message only, no attempt counted.
|
// Non-credential failure (busy node, unknown error): message only, no attempt counted.
|
||||||
void applyUnlockNotCounted(const std::string& errorMessage);
|
void applyUnlockNotCounted(const std::string& errorMessage);
|
||||||
// Clear all rescan + witness-rebuild progress/accumulator state. Shared by the four
|
// Clear all rescan + witness-rebuild progress/accumulator state. Shared by the four
|
||||||
@@ -994,6 +1054,8 @@ private:
|
|||||||
// z_exportmnemonic / lite exportSeed → the same KDF), so it is portable across both variants.
|
// z_exportmnemonic / lite exportSeed → the same KDF), so it is portable across both variants.
|
||||||
chat::ChatService chat_service_;
|
chat::ChatService chat_service_;
|
||||||
chat::ChatDatabase chat_db_; // persistent backing (seed-derived encryption at rest)
|
chat::ChatDatabase chat_db_; // persistent backing (seed-derived encryption at rest)
|
||||||
|
chat::ChatSensitiveVault chat_sensitive_vault_; // local key vault for "sensitive" conversations
|
||||||
|
std::string chatSensitiveVaultPath() const; // <configDir>/chat_sensitive.vault
|
||||||
bool chat_identity_provisioned_ = false; // identity set on the service this session
|
bool chat_identity_provisioned_ = false; // identity set on the service this session
|
||||||
bool chat_identity_fetch_in_flight_ = false; // a z_exportmnemonic worker job is pending
|
bool chat_identity_fetch_in_flight_ = false; // a z_exportmnemonic worker job is pending
|
||||||
std::chrono::steady_clock::time_point chat_identity_retry_at_{}; // after a transient failure
|
std::chrono::steady_clock::time_point chat_identity_retry_at_{}; // after a transient failure
|
||||||
@@ -1047,6 +1109,10 @@ private:
|
|||||||
};
|
};
|
||||||
LiteInflightOp inflight_op_; // the single chat/split op currently on the send channel
|
LiteInflightOp inflight_op_; // the single chat/split op currently on the send channel
|
||||||
std::deque<QueuedChatOp> chat_send_queue_; // chat/contact sends awaiting a free channel + verified note
|
std::deque<QueuedChatOp> chat_send_queue_; // chat/contact sends awaiting a free channel + verified note
|
||||||
|
// Larger-messages tier 3: a message split across several transactions shares one echo. This counts the
|
||||||
|
// not-yet-broadcast parts per echo id; the echo flips to Sent only when the last part lands, and to
|
||||||
|
// Failed (with the remaining parts purged) if any part fails. Absent/one ⇒ an ordinary single-tx send.
|
||||||
|
std::unordered_map<std::string, int> chat_send_parts_remaining_;
|
||||||
// Note-availability estimate between refreshes/scans: reset from a fresh count, decremented on each chat
|
// Note-availability estimate between refreshes/scans: reset from a fresh count, decremented on each chat
|
||||||
// submit (the count lags a spend by a cycle, but the wallet still picks a fresh note per send). Zeroed on
|
// submit (the count lags a spend by a cycle, but the wallet still picks a fresh note per send). Zeroed on
|
||||||
// a transient-funds failure so we stop draining until the next refresh/scan restores the truth.
|
// a transient-funds failure so we stop draining until the next refresh/scan restores the truth.
|
||||||
@@ -1173,6 +1239,7 @@ private:
|
|||||||
void pollSweepStatus(bool forceBalance = false); // Confirming step: confs always; balance only when it can move the gate
|
void pollSweepStatus(bool forceBalance = false); // Confirming step: confs always; balance only when it can move the gate
|
||||||
void locateSweepTxid(); // recover a lost sweep txid (send to dest) -> Confirming gate
|
void locateSweepTxid(); // recover a lost sweep txid (send to dest) -> Confirming gate
|
||||||
void beginAdoptSeedWallet(); // stop daemon -> swap wallet.dat -> restart with -rescan
|
void beginAdoptSeedWallet(); // stop daemon -> swap wallet.dat -> restart with -rescan
|
||||||
|
void verifyThenAdoptSeedWallet(); // re-read sweep confs + minconf-0 balance, then beginAdoptSeedWallet() iff safe
|
||||||
void provisionChatIdentityFromSecret(std::string secret);
|
void provisionChatIdentityFromSecret(std::string secret);
|
||||||
std::string chatReplyZaddr(); // a stable (persisted) wallet z-addr — chat IDENTITY (reply-to)
|
std::string chatReplyZaddr(); // a stable (persisted) wallet z-addr — chat IDENTITY (reply-to)
|
||||||
// A spendable z-address that can actually PAY `need` (a fee, or a split's notes plus its fee), preferring
|
// A spendable z-address that can actually PAY `need` (a fee, or a split's notes plus its fee), preferring
|
||||||
@@ -1300,6 +1367,7 @@ private:
|
|||||||
|
|
||||||
// Daemon restart (e.g. after changing debug log categories)
|
// Daemon restart (e.g. after changing debug log categories)
|
||||||
std::atomic<bool> daemon_restarting_{false};
|
std::atomic<bool> daemon_restarting_{false};
|
||||||
|
std::atomic<bool> daemon_restart_pending_{false}; // updated binary installed, awaiting a restart
|
||||||
|
|
||||||
// Wallet-switch failure recovery: the switch worker sets wallet_switch_failed_ when the new
|
// Wallet-switch failure recovery: the switch worker sets wallet_switch_failed_ when the new
|
||||||
// wallet's daemon won't start/stay up; the main loop then reverts active_wallet_file to
|
// wallet's daemon won't start/stay up; the main loop then reverts active_wallet_file to
|
||||||
@@ -1328,6 +1396,11 @@ private:
|
|||||||
// so a possibly-incomplete salvaged wallet isn't mistaken for fund loss. Warned once per session.
|
// so a possibly-incomplete salvaged wallet isn't mistaken for fund loss. Warned once per session.
|
||||||
bool wallet_auto_recovered_ = false; // a salvage happened this session
|
bool wallet_auto_recovered_ = false; // a salvage happened this session
|
||||||
bool wallet_auto_recovered_warned_ = false; // guard: only surface it once per session
|
bool wallet_auto_recovered_warned_ = false; // guard: only surface it once per session
|
||||||
|
// We armed -salvagewallet ourselves to COMPACT a healthy wallet (not to recover a corrupt one), so the
|
||||||
|
// resulting salvage must NOT raise the "needs a quick repair" dialog — detectWalletAutoRecovery consumes
|
||||||
|
// this and shows a calm "compacted" toast instead. Atomic: compaction can be armed off the UI thread.
|
||||||
|
std::atomic<bool> compaction_salvage_expected_{false};
|
||||||
|
std::string last_address_error_; // daemon reason the last z_getnewaddress/getnewaddress failed ("" = ok)
|
||||||
bool wallet_degraded_ = false; // v1.3.0+ opened the wallet in DEGRADED mode (no new HD keys)
|
bool wallet_degraded_ = false; // v1.3.0+ opened the wallet in DEGRADED mode (no new HD keys)
|
||||||
bool wallet_degraded_warned_ = false; // guard: surface the degraded-mode notice once per session
|
bool wallet_degraded_warned_ = false; // guard: surface the degraded-mode notice once per session
|
||||||
bool show_wallet_recovered_dialog_ = false; // auto-shown warning dialog
|
bool show_wallet_recovered_dialog_ = false; // auto-shown warning dialog
|
||||||
@@ -1390,10 +1463,15 @@ private:
|
|||||||
// UI State
|
// UI State
|
||||||
bool quit_requested_ = false;
|
bool quit_requested_ = false;
|
||||||
bool show_demo_window_ = false;
|
bool show_demo_window_ = false;
|
||||||
bool show_settings_ = false;
|
|
||||||
bool show_about_ = false;
|
bool show_about_ = false;
|
||||||
bool show_faq_ = false;
|
bool show_faq_ = false;
|
||||||
bool show_import_key_ = false;
|
bool show_import_key_ = false;
|
||||||
|
bool show_import_wallet_file_ = false; // the "import keys from a backup file" modal is open
|
||||||
|
char import_wallet_file_path_[1024] = {0}; // typed path to the old wallet's key dump (not a secret)
|
||||||
|
bool import_wallet_file_busy_ = false; // a z_importwallet long call is in flight
|
||||||
|
bool import_wallet_file_done_ = false; // terminal result reached (ok/err in import_wallet_file_ok_)
|
||||||
|
bool import_wallet_file_ok_ = false;
|
||||||
|
std::string import_wallet_file_status_; // result / error text shown in the modal
|
||||||
bool show_export_key_ = false;
|
bool show_export_key_ = false;
|
||||||
bool show_backup_ = false;
|
bool show_backup_ = false;
|
||||||
bool show_seed_backup_ = false;
|
bool show_seed_backup_ = false;
|
||||||
@@ -1444,6 +1522,9 @@ private:
|
|||||||
std::string seed_migration_dest_; // new shielded z-address (Phase 2 sweep target)
|
std::string seed_migration_dest_; // new shielded z-address (Phase 2 sweep target)
|
||||||
std::string seed_migration_temp_dir_; // temp datadir root holding the new wallet (kept)
|
std::string seed_migration_temp_dir_; // temp datadir root holding the new wallet (kept)
|
||||||
bool seed_migration_backed_up_ = false; // "I've written it down" confirmation
|
bool seed_migration_backed_up_ = false; // "I've written it down" confirmation
|
||||||
|
bool seed_migration_understood_ = false; // 2nd ack: funds recoverable only via the phrase
|
||||||
|
bool seed_migration_confirm_discard_ = false; // inline "are you sure" gate for Discard (never persisted)
|
||||||
|
bool seed_migration_confirm_sweep_ = false; // inline "are you sure" gate for Sweep (never persisted)
|
||||||
std::string seed_migration_status_; // main-thread display (progress / error text)
|
std::string seed_migration_status_; // main-thread display (progress / error text)
|
||||||
double seed_migration_balance_ = 0.0; // legacy total to sweep (shown on the Sweep step)
|
double seed_migration_balance_ = 0.0; // legacy total to sweep (shown on the Sweep step)
|
||||||
std::string seed_migration_sweep_txid_; // the z_mergetoaddress sweep transaction id
|
std::string seed_migration_sweep_txid_; // the z_mergetoaddress sweep transaction id
|
||||||
@@ -1556,6 +1637,7 @@ private:
|
|||||||
|
|
||||||
// Connection
|
// Connection
|
||||||
std::string connection_status_ = "Disconnected";
|
std::string connection_status_ = "Disconnected";
|
||||||
|
bool rpc_auth_failed_ = false; // last connect hit an auth failure (bad rpcuser/rpcpassword); drives the banner
|
||||||
bool connection_in_progress_ = false;
|
bool connection_in_progress_ = false;
|
||||||
bool remote_rpc_plaintext_warning_shown_ = false;
|
bool remote_rpc_plaintext_warning_shown_ = false;
|
||||||
// Startup daemon-launch diagnostics: bound the "RPC port busy, no config" wait before warning,
|
// Startup daemon-launch diagnostics: bound the "RPC port busy, no config" wait before warning,
|
||||||
@@ -1741,6 +1823,14 @@ private:
|
|||||||
|
|
||||||
// Mining toggle guard (prevents concurrent setgenerate calls)
|
// Mining toggle guard (prevents concurrent setgenerate calls)
|
||||||
std::atomic<bool> mining_toggle_in_progress_{false};
|
std::atomic<bool> mining_toggle_in_progress_{false};
|
||||||
|
// Mining error state (main-thread only — every writer is in a MainCb or render).
|
||||||
|
std::string mining_last_error_;
|
||||||
|
// Sync-ETA estimator (UI thread only). onConnected/onDisconnected may run off-thread, so they set
|
||||||
|
// the atomic flag and the status bar consumes it before update().
|
||||||
|
util::SyncRateEstimator sync_eta_;
|
||||||
|
std::atomic<bool> sync_eta_reset_pending_{false};
|
||||||
|
bool mining_error_av_ = false;
|
||||||
|
void setMiningError(std::string msg, bool av = false) { mining_last_error_ = std::move(msg); mining_error_av_ = av; }
|
||||||
|
|
||||||
// True from a successful startPoolMining() until the miner is confirmed connected/hashing in the
|
// True from a successful startPoolMining() until the miner is confirmed connected/hashing in the
|
||||||
// poll — drives the "connecting…" → "connected" feedback for pool mining (which has a connect delay).
|
// poll — drives the "connecting…" → "connected" feedback for pool mining (which has a connect delay).
|
||||||
@@ -1870,6 +1960,15 @@ private:
|
|||||||
void postConsolidationCall(int addrIdx);
|
void postConsolidationCall(int addrIdx);
|
||||||
void postConsolidationPoll();
|
void postConsolidationPoll();
|
||||||
void finishConsolidation();
|
void finishConsolidation();
|
||||||
|
// Automatic background consolidation (settings-gated): drives the SAME paced ConsolidationRun in tiny,
|
||||||
|
// gentle cycles when the wallet is idle, so a mining/pool wallet sheds notes without the user ever
|
||||||
|
// clicking Consolidate. pumpAutoConsolidation() is pumped from update() right after pumpConsolidation().
|
||||||
|
void pumpAutoConsolidation();
|
||||||
|
void postAutoConsolidationScan(); // a quiet z_listunspent scan → startConsolidationQuiet on candidates
|
||||||
|
bool startConsolidationQuiet(const std::vector<services::NoteGroup>& groups, services::RunConfig cfg);
|
||||||
|
bool auto_consolidate_run_ = false; // the active run is ours (silent; gentle config)
|
||||||
|
bool auto_consolidate_scan_in_flight_ = false; // an auto note scan is posted
|
||||||
|
double auto_consolidate_next_at_ = 0.0; // steadySeconds() before which no new cycle/scan starts
|
||||||
// Consecutive Core-refresh cycles where BOTH core RPCs failed → likely a dead
|
// Consecutive Core-refresh cycles where BOTH core RPCs failed → likely a dead
|
||||||
// connection. After kCoreFailuresBeforeDisconnect, tear down and reconnect.
|
// connection. After kCoreFailuresBeforeDisconnect, tear down and reconnect.
|
||||||
int consecutive_core_failures_ = 0;
|
int consecutive_core_failures_ = 0;
|
||||||
@@ -1909,6 +2008,7 @@ private:
|
|||||||
// 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_;
|
||||||
|
std::string wizard_bootstrap_url_; // source the wizard last started ("" = default); Retry keeps it
|
||||||
bool bootstrap_downloading_ = false; // true while settings bootstrap dialog is active
|
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
|
||||||
@@ -1954,6 +2054,7 @@ private:
|
|||||||
int lock_attempts_ = 0;
|
int lock_attempts_ = 0;
|
||||||
float lock_lockout_timer_ = 0.0f;
|
float lock_lockout_timer_ = 0.0f;
|
||||||
std::chrono::steady_clock::time_point lock_timer_armed_at_{}; // when lock_error/lockout timers were last armed
|
std::chrono::steady_clock::time_point lock_timer_armed_at_{}; // when lock_error/lockout timers were last armed
|
||||||
|
bool lock_state_restored_ = false; // one-shot: persisted lockout restored for this lock-screen session
|
||||||
bool lock_unlock_in_progress_ = false;
|
bool lock_unlock_in_progress_ = false;
|
||||||
|
|
||||||
// Encrypt wallet dialog state
|
// Encrypt wallet dialog state
|
||||||
@@ -2021,6 +2122,7 @@ private:
|
|||||||
void renderLiteFirstRunPrompt(); // lite-only welcome modal when no wallet exists yet
|
void renderLiteFirstRunPrompt(); // lite-only welcome modal when no wallet exists yet
|
||||||
void renderLiteUnlockPrompt(); // lite-only send-time unlock modal
|
void renderLiteUnlockPrompt(); // lite-only send-time unlock modal
|
||||||
void renderImportKeyDialog();
|
void renderImportKeyDialog();
|
||||||
|
void renderImportWalletFileDialog(); // typed-path import of a legacy key dump (z_importwallet)
|
||||||
void renderExportKeyDialog();
|
void renderExportKeyDialog();
|
||||||
void renderBackupDialog();
|
void renderBackupDialog();
|
||||||
void renderSeedBackupDialog(); // full-node "Back up seed phrase" modal (z_exportmnemonic)
|
void renderSeedBackupDialog(); // full-node "Back up seed phrase" modal (z_exportmnemonic)
|
||||||
|
|||||||
+717
-64
File diff suppressed because it is too large
Load Diff
+85
-34
@@ -33,6 +33,7 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <ctime>
|
#include <ctime>
|
||||||
|
#include <cmath>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
@@ -476,6 +477,12 @@ void App::applyUnlockSuccess(const std::string& passphrase, int timeout) {
|
|||||||
lock_unlock_in_progress_ = false;
|
lock_unlock_in_progress_ = false;
|
||||||
lock_error_msg_.clear();
|
lock_error_msg_.clear();
|
||||||
lock_attempts_ = 0;
|
lock_attempts_ = 0;
|
||||||
|
// Clear the persisted lockout on a successful unlock (guarded so a normal unlock isn't a disk write).
|
||||||
|
if (settings_ && settings_->getLockFailedAttempts() != 0) {
|
||||||
|
settings_->setLockFailedAttempts(0);
|
||||||
|
settings_->setLockoutUntilEpoch(0);
|
||||||
|
settings_->save();
|
||||||
|
}
|
||||||
memset(lock_passphrase_buf_, 0, sizeof(lock_passphrase_buf_));
|
memset(lock_passphrase_buf_, 0, sizeof(lock_passphrase_buf_));
|
||||||
idle_.noteActivity();
|
idle_.noteActivity();
|
||||||
state_.encrypted = true;
|
state_.encrypted = true;
|
||||||
@@ -499,9 +506,37 @@ void App::applyUnlockFailure(const std::string& errorMessage) {
|
|||||||
if (lock_attempts_ >= maxAttempts) {
|
if (lock_attempts_ >= maxAttempts) {
|
||||||
lock_lockout_timer_ = baseDelay * (float)(1 << std::min(lock_attempts_ - maxAttempts, 8));
|
lock_lockout_timer_ = baseDelay * (float)(1 << std::min(lock_attempts_ - maxAttempts, 8));
|
||||||
}
|
}
|
||||||
|
// Persist the attempt count + lockout expiry (keyed to this wallet) so killing and relaunching the
|
||||||
|
// app can't reset the escalating lockout. The PIN/passphrase itself is never stored.
|
||||||
|
if (settings_) {
|
||||||
|
settings_->setLockStateWallet(settings_->getActiveWalletFile());
|
||||||
|
settings_->setLockFailedAttempts(lock_attempts_);
|
||||||
|
settings_->setLockoutUntilEpoch(lock_lockout_timer_ > 0.0f
|
||||||
|
? (long long)std::time(nullptr) + (long long)std::ceil(lock_lockout_timer_) : 0);
|
||||||
|
settings_->save();
|
||||||
|
}
|
||||||
DEBUG_LOGF("[App] Wallet unlock failed (attempt %d)\n", lock_attempts_);
|
DEBUG_LOGF("[App] Wallet unlock failed (attempt %d)\n", lock_attempts_);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Restore the persisted lockout at the start of a lock-screen session so a kill-and-relaunch can't reset
|
||||||
|
// the escalating failure curve. Only applies when the persisted state is for the active wallet.
|
||||||
|
void App::restoreLockoutState() {
|
||||||
|
if (!settings_) return;
|
||||||
|
if (settings_->getLockStateWallet() != settings_->getActiveWalletFile()) return;
|
||||||
|
lock_attempts_ = std::max(0, std::min(settings_->getLockFailedAttempts(), 64));
|
||||||
|
long long remaining = settings_->getLockoutUntilEpoch() - (long long)std::time(nullptr);
|
||||||
|
// Cap the restored countdown so a forward clock change can't create an absurd wait. The attempt count
|
||||||
|
// still persists, so rolling the clock back only buys a short window before the next failure re-arms
|
||||||
|
// the full curve.
|
||||||
|
const float baseDelay = ui::schema::UI().drawElement("security", "lockout-base-delay").sizeOr(2.0f);
|
||||||
|
const long long maxLock = (long long)(baseDelay * (float)(1 << 8));
|
||||||
|
if (remaining > maxLock) remaining = maxLock;
|
||||||
|
if (remaining > 0) {
|
||||||
|
lock_lockout_timer_ = (float)remaining;
|
||||||
|
lock_timer_armed_at_ = std::chrono::steady_clock::now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Failure that is NOT a wrong credential (busy node, not-encrypted, unknown error): surface the
|
// Failure that is NOT a wrong credential (busy node, not-encrypted, unknown error): surface the
|
||||||
// message and clear the buffer, but do not bump lock_attempts_ or arm the lockout.
|
// message and clear the buffer, but do not bump lock_attempts_ or arm the lockout.
|
||||||
void App::applyUnlockNotCounted(const std::string& msg) {
|
void App::applyUnlockNotCounted(const std::string& msg) {
|
||||||
@@ -513,6 +548,15 @@ void App::applyUnlockNotCounted(const std::string& msg) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void App::unlockWallet(const std::string& passphrase, int timeout) {
|
void App::unlockWallet(const std::string& passphrase, int timeout) {
|
||||||
|
if (lite_wallet_) { // lite: backend unlock with the shared attempt-lockout (no RPC/daemon)
|
||||||
|
std::string liteErr;
|
||||||
|
if (!unlockLiteWallet(passphrase, &liteErr)) {
|
||||||
|
lock_error_msg_ = liteErr;
|
||||||
|
lock_error_timer_ = 3.0f;
|
||||||
|
lock_timer_armed_at_ = std::chrono::steady_clock::now();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!rpc_ || !rpc_->isConnected() || !worker_) return;
|
if (!rpc_ || !rpc_->isConnected() || !worker_) return;
|
||||||
lock_unlock_in_progress_ = true;
|
lock_unlock_in_progress_ = true;
|
||||||
|
|
||||||
@@ -563,6 +607,7 @@ void App::unlockWallet(const std::string& passphrase, int timeout) {
|
|||||||
void App::lockWallet() {
|
void App::lockWallet() {
|
||||||
if (!rpc_ || !rpc_->isConnected() || !worker_) return;
|
if (!rpc_ || !rpc_->isConnected() || !worker_) return;
|
||||||
if (lock_unlock_in_progress_) return; // Prevent duplicate async calls
|
if (lock_unlock_in_progress_) return; // Prevent duplicate async calls
|
||||||
|
wipeDisplayedSecrets(); // hide any revealed seed/key immediately, before the async lock round-trip
|
||||||
lock_unlock_in_progress_ = true;
|
lock_unlock_in_progress_ = true;
|
||||||
|
|
||||||
// Use fast-lane worker to avoid blocking behind refreshData.
|
// Use fast-lane worker to avoid blocking behind refreshData.
|
||||||
@@ -755,12 +800,11 @@ void App::checkAutoLock() {
|
|||||||
if (capture_mode_) return; // don't auto-lock while a UI sweep forces the encrypted/locked state
|
if (capture_mode_) return; // don't auto-lock while a UI sweep forces the encrypted/locked state
|
||||||
if (!state_.isEncrypted() || state_.isLocked()) return;
|
if (!state_.isEncrypted() || state_.isLocked()) return;
|
||||||
|
|
||||||
// Don't auto-lock while mining — mining is a long-running intentional
|
// Don't auto-lock while SOLO mining — it runs in the daemon and locking the wallet there would stop
|
||||||
// operation and locking the wallet on the daemon side stops solo mining.
|
// it (and it shields matured rewards, which needs the wallet). Pool mining pays an external address
|
||||||
bool miningActive = state_.mining.generate
|
// and needs no unlocked wallet, so it must NOT hold the wallet open indefinitely.
|
||||||
|| (xmrig_manager_ && xmrig_manager_->isRunning());
|
if (state_.mining.generate) {
|
||||||
if (miningActive) {
|
// Keep resetting the idle timer so we don't lock the instant solo mining stops.
|
||||||
// Keep resetting the idle timer so we don't lock the instant mining stops
|
|
||||||
idle_.noteActivity();
|
idle_.noteActivity();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1040,9 +1084,15 @@ void App::tickMiningThreadCommit()
|
|||||||
void App::renderLockScreen() {
|
void App::renderLockScreen() {
|
||||||
using namespace ui::material;
|
using namespace ui::material;
|
||||||
|
|
||||||
|
// Restore any persisted lockout once per lock-screen session (re-armed on wallet switch).
|
||||||
|
if (!lock_state_restored_) {
|
||||||
|
lock_state_restored_ = true;
|
||||||
|
restoreLockoutState();
|
||||||
|
}
|
||||||
|
|
||||||
// Keep frames coming while something on the lock screen is moving without input: the unlock in
|
// Keep frames coming while something on the lock screen is moving without input: the unlock in
|
||||||
// flight (its dots), the lockout countdown, an error message about to clear.
|
// flight (its dots), the lockout countdown, an error message about to clear.
|
||||||
if (lock_unlock_in_progress_ || lock_lockout_timer_ > 0.0f || lock_error_timer_ > 0.0f) {
|
if (lock_unlock_in_progress_ || lock_lockout_timer_ > 0.0f) {
|
||||||
RequestProgressFrame();
|
RequestProgressFrame();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1264,9 +1314,9 @@ void App::renderLockScreen() {
|
|||||||
lock_screen_was_visible_ = true;
|
lock_screen_was_visible_ = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error message
|
// Error message — stays visible until the next unlock attempt / mode switch / success, instead of
|
||||||
if (!lock_error_msg_.empty() && lock_error_timer_ > 0) {
|
// fading after a few seconds (a failed-unlock reason was often gone before it could be read).
|
||||||
lock_error_timer_ -= lockDt;
|
if (!lock_error_msg_.empty()) {
|
||||||
ImVec2 es = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, 0, lock_error_msg_.c_str());
|
ImVec2 es = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, 0, lock_error_msg_.c_str());
|
||||||
dl->AddText(captionFont, captionFont->LegacySize,
|
dl->AddText(captionFont, captionFont->LegacySize,
|
||||||
ImVec2(cardX + (cardW - es.x) * 0.5f, cy), ui::material::Error(),
|
ImVec2(cardX + (cardW - es.x) * 0.5f, cy), ui::material::Error(),
|
||||||
@@ -1312,6 +1362,7 @@ void App::renderLockScreen() {
|
|||||||
|
|
||||||
// Handle submit (Enter key or button click)
|
// Handle submit (Enter key or button click)
|
||||||
if ((submitted || btnClicked) && canSubmit) {
|
if ((submitted || btnClicked) && canSubmit) {
|
||||||
|
lock_error_msg_.clear(); // the prior error clears only when a new attempt actually starts
|
||||||
// Derive daemon unlock timeout from auto-lock setting:
|
// Derive daemon unlock timeout from auto-lock setting:
|
||||||
// 2x the idle timeout so the daemon stays unlocked longer than
|
// 2x the idle timeout so the daemon stays unlocked longer than
|
||||||
// the GUI auto-lock, acting as a safety net if the GUI crashes.
|
// the GUI auto-lock, acting as a safety net if the GUI crashes.
|
||||||
@@ -1505,7 +1556,7 @@ void App::renderEncryptWalletDialog() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!encrypt_status_.empty()) {
|
if (!encrypt_status_.empty()) {
|
||||||
ImGui::TextColored(ImVec4(1, 0.7f, 0, 1), "%s", encrypt_status_.c_str());
|
ImGui::TextColored(ui::material::WarningVec4(), "%s", encrypt_status_.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
@@ -1516,12 +1567,12 @@ void App::renderEncryptWalletDialog() {
|
|||||||
if (!valid && (encPassLen > 0 || strlen(encrypt_confirm_buf_) > 0)) {
|
if (!valid && (encPassLen > 0 || strlen(encrypt_confirm_buf_) > 0)) {
|
||||||
const char* why = (encPassLen < 8) ? TR("enc_min_len")
|
const char* why = (encPassLen < 8) ? TR("enc_min_len")
|
||||||
: (!encMatch ? TR("enc_mismatch") : "");
|
: (!encMatch ? TR("enc_mismatch") : "");
|
||||||
if (*why) ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.3f, 1.0f), "%s", why);
|
if (*why) ImGui::TextColored(ui::material::WarningVec4(), "%s", why);
|
||||||
}
|
}
|
||||||
|
|
||||||
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
||||||
ImGui::BeginDisabled(!valid || encrypt_in_progress_);
|
ImGui::BeginDisabled(!valid || encrypt_in_progress_);
|
||||||
if (ui::material::TactileButton(TR("settings_encrypt_wallet"), ImVec2(btnW, 40))) {
|
if (ui::material::TactileButton(TR("settings_encrypt_wallet"), ImVec2(btnW, 40.0f * ui::Layout::dpiScale()))) {
|
||||||
std::string pass(encrypt_pass_buf_);
|
std::string pass(encrypt_pass_buf_);
|
||||||
enc_dlg_saved_passphrase_ = pass;
|
enc_dlg_saved_passphrase_ = pass;
|
||||||
memset(encrypt_pass_buf_, 0, sizeof(encrypt_pass_buf_));
|
memset(encrypt_pass_buf_, 0, sizeof(encrypt_pass_buf_));
|
||||||
@@ -1533,7 +1584,7 @@ void App::renderEncryptWalletDialog() {
|
|||||||
ImGui::EndDisabled();
|
ImGui::EndDisabled();
|
||||||
|
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
if (ui::material::TactileButton(TR("cancel"), ImVec2(btnW, 40))) {
|
if (ui::material::TactileButton(TR("cancel"), ImVec2(btnW, 40.0f * ui::Layout::dpiScale()))) {
|
||||||
memset(encrypt_pass_buf_, 0, sizeof(encrypt_pass_buf_));
|
memset(encrypt_pass_buf_, 0, sizeof(encrypt_pass_buf_));
|
||||||
memset(encrypt_confirm_buf_, 0, sizeof(encrypt_confirm_buf_));
|
memset(encrypt_confirm_buf_, 0, sizeof(encrypt_confirm_buf_));
|
||||||
show_encrypt_dialog_ = false;
|
show_encrypt_dialog_ = false;
|
||||||
@@ -1600,7 +1651,7 @@ void App::renderEncryptWalletDialog() {
|
|||||||
ImGui::PopItemWidth();
|
ImGui::PopItemWidth();
|
||||||
|
|
||||||
if (!enc_dlg_pin_status_.empty()) {
|
if (!enc_dlg_pin_status_.empty()) {
|
||||||
ImGui::TextColored(ImVec4(1, 0.7f, 0, 1), "%s", enc_dlg_pin_status_.c_str());
|
ImGui::TextColored(ui::material::WarningVec4(), "%s", enc_dlg_pin_status_.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
@@ -1612,7 +1663,7 @@ void App::renderEncryptWalletDialog() {
|
|||||||
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
||||||
|
|
||||||
ImGui::BeginDisabled(!pinValid || !hasPassphrase || pin_in_progress_);
|
ImGui::BeginDisabled(!pinValid || !hasPassphrase || pin_in_progress_);
|
||||||
if (ui::material::TactileButton(TR("settings_set_pin"), ImVec2(btnW, 40))) {
|
if (ui::material::TactileButton(TR("settings_set_pin"), ImVec2(btnW, 40.0f * ui::Layout::dpiScale()))) {
|
||||||
pin_in_progress_ = true;
|
pin_in_progress_ = true;
|
||||||
enc_dlg_pin_status_.clear();
|
enc_dlg_pin_status_.clear();
|
||||||
std::string savedPass = enc_dlg_saved_passphrase_;
|
std::string savedPass = enc_dlg_saved_passphrase_;
|
||||||
@@ -1655,7 +1706,7 @@ void App::renderEncryptWalletDialog() {
|
|||||||
ImGui::EndDisabled();
|
ImGui::EndDisabled();
|
||||||
|
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
if (ui::material::TactileButton(TR("wiz_skip"), ImVec2(btnW, 40))) {
|
if (ui::material::TactileButton(TR("wiz_skip"), ImVec2(btnW, 40.0f * ui::Layout::dpiScale()))) {
|
||||||
if (!enc_dlg_saved_passphrase_.empty()) {
|
if (!enc_dlg_saved_passphrase_.empty()) {
|
||||||
util::SecureVault::secureZero(&enc_dlg_saved_passphrase_[0],
|
util::SecureVault::secureZero(&enc_dlg_saved_passphrase_[0],
|
||||||
enc_dlg_saved_passphrase_.size());
|
enc_dlg_saved_passphrase_.size());
|
||||||
@@ -1718,7 +1769,7 @@ void App::renderEncryptWalletDialog() {
|
|||||||
ImGui::PopItemWidth();
|
ImGui::PopItemWidth();
|
||||||
|
|
||||||
if (!encrypt_status_.empty()) {
|
if (!encrypt_status_.empty()) {
|
||||||
ImGui::TextColored(ImVec4(1, 0.7f, 0, 1), "%s", encrypt_status_.c_str());
|
ImGui::TextColored(ui::material::WarningVec4(), "%s", encrypt_status_.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
@@ -1730,7 +1781,7 @@ void App::renderEncryptWalletDialog() {
|
|||||||
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
||||||
// Not while a PIN operation runs: PIN setup could verify and seal the OLD passphrase.
|
// Not while a PIN operation runs: PIN setup could verify and seal the OLD passphrase.
|
||||||
ImGui::BeginDisabled(!valid || encrypt_in_progress_ || pin_in_progress_);
|
ImGui::BeginDisabled(!valid || encrypt_in_progress_ || pin_in_progress_);
|
||||||
if (ui::material::TactileButton(TR("change_pass_title"), ImVec2(btnW, 40))) {
|
if (ui::material::TactileButton(TR("change_pass_title"), ImVec2(btnW, 40.0f * ui::Layout::dpiScale()))) {
|
||||||
changePassphrase(std::string(change_old_pass_buf_),
|
changePassphrase(std::string(change_old_pass_buf_),
|
||||||
std::string(change_new_pass_buf_));
|
std::string(change_new_pass_buf_));
|
||||||
}
|
}
|
||||||
@@ -1739,7 +1790,7 @@ void App::renderEncryptWalletDialog() {
|
|||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
// Cancel does what Esc/close does — dismiss without applying. Buffers are wiped by
|
// Cancel does what Esc/close does — dismiss without applying. Buffers are wiped by
|
||||||
// the !show_change_passphrase_ cleanup block below.
|
// the !show_change_passphrase_ cleanup block below.
|
||||||
if (ui::material::TactileButton(TR("cancel"), ImVec2(btnW, 40))) {
|
if (ui::material::TactileButton(TR("cancel"), ImVec2(btnW, 40.0f * ui::Layout::dpiScale()))) {
|
||||||
show_change_passphrase_ = false;
|
show_change_passphrase_ = false;
|
||||||
}
|
}
|
||||||
EndOverlayDialog();
|
EndOverlayDialog();
|
||||||
@@ -1799,7 +1850,7 @@ void App::renderDecryptWalletDialog() {
|
|||||||
ImGui::PopItemWidth();
|
ImGui::PopItemWidth();
|
||||||
|
|
||||||
if (!decryptState.status.empty()) {
|
if (!decryptState.status.empty()) {
|
||||||
ImGui::TextColored(ImVec4(1, 0.4f, 0.4f, 1), "%s", decryptState.status.c_str());
|
ImGui::TextColored(ui::material::ErrorVec4(), "%s", decryptState.status.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
@@ -1808,7 +1859,7 @@ void App::renderDecryptWalletDialog() {
|
|||||||
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
||||||
ImGui::BeginDisabled(!valid || decryptState.inProgress);
|
ImGui::BeginDisabled(!valid || decryptState.inProgress);
|
||||||
// Decrypting stops and restarts the node: not while it is busy (refuseIfNodeBusy() only on a click).
|
// Decrypting stops and restarts the node: not while it is busy (refuseIfNodeBusy() only on a click).
|
||||||
if ((ui::material::TactileButton(TR("settings_remove_encryption"), ImVec2(btnW, 40)) || (enterPressed && valid)) &&
|
if ((ui::material::TactileButton(TR("settings_remove_encryption"), ImVec2(btnW, 40.0f * ui::Layout::dpiScale())) || (enterPressed && valid)) &&
|
||||||
!refuseIfNodeBusy()) {
|
!refuseIfNodeBusy()) {
|
||||||
std::string passphrase(decrypt_pass_buf_);
|
std::string passphrase(decrypt_pass_buf_);
|
||||||
memset(decrypt_pass_buf_, 0, sizeof(decrypt_pass_buf_));
|
memset(decrypt_pass_buf_, 0, sizeof(decrypt_pass_buf_));
|
||||||
@@ -2017,7 +2068,7 @@ void App::renderDecryptWalletDialog() {
|
|||||||
ImGui::EndDisabled();
|
ImGui::EndDisabled();
|
||||||
|
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
if (ui::material::TactileButton(TR("cancel"), ImVec2(btnW, 40))) {
|
if (ui::material::TactileButton(TR("cancel"), ImVec2(btnW, 40.0f * ui::Layout::dpiScale()))) {
|
||||||
memset(decrypt_pass_buf_, 0, sizeof(decrypt_pass_buf_));
|
memset(decrypt_pass_buf_, 0, sizeof(decrypt_pass_buf_));
|
||||||
show_decrypt_dialog_ = false;
|
show_decrypt_dialog_ = false;
|
||||||
}
|
}
|
||||||
@@ -2047,7 +2098,7 @@ void App::renderDecryptWalletDialog() {
|
|||||||
if (services::WalletSecurityWorkflow::stepIsComplete(decryptState.step,
|
if (services::WalletSecurityWorkflow::stepIsComplete(decryptState.step,
|
||||||
services::WalletSecurityWorkflow::stepFromIndex(i))) {
|
services::WalletSecurityWorkflow::stepFromIndex(i))) {
|
||||||
// Completed
|
// Completed
|
||||||
ImGui::TextColored(ImVec4(0.3f, 0.9f, 0.4f, 1.0f), ICON_MD_CHECK_CIRCLE);
|
ImGui::TextColored(ui::material::SuccessVec4(), ICON_MD_CHECK_CIRCLE);
|
||||||
} else if (i == services::WalletSecurityWorkflow::stepIndex(decryptState.step)) {
|
} else if (i == services::WalletSecurityWorkflow::stepIndex(decryptState.step)) {
|
||||||
// In progress - animate
|
// In progress - animate
|
||||||
float alpha = 0.5f + 0.5f * sinf((float)ImGui::GetTime() * 4.0f);
|
float alpha = 0.5f + 0.5f * sinf((float)ImGui::GetTime() * 4.0f);
|
||||||
@@ -2132,7 +2183,7 @@ void App::renderDecryptWalletDialog() {
|
|||||||
ImGui::TextWrapped("%s", TR("decrypt_success_desc"));
|
ImGui::TextWrapped("%s", TR("decrypt_success_desc"));
|
||||||
|
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
if (ui::material::TactileButton(TR("close"), ImVec2(-1, 40))) {
|
if (ui::material::TactileButton(TR("close"), ImVec2(-1, 40.0f * ui::Layout::dpiScale()))) {
|
||||||
show_decrypt_dialog_ = false;
|
show_decrypt_dialog_ = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2149,11 +2200,11 @@ void App::renderDecryptWalletDialog() {
|
|||||||
|
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
||||||
if (ui::material::TactileButton(TR("try_again"), ImVec2(btnW, 40))) {
|
if (ui::material::TactileButton(TR("try_again"), ImVec2(btnW, 40.0f * ui::Layout::dpiScale()))) {
|
||||||
wallet_security_workflow_.reset();
|
wallet_security_workflow_.reset();
|
||||||
}
|
}
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
if (ui::material::TactileButton(TR("close"), ImVec2(btnW, 40))) {
|
if (ui::material::TactileButton(TR("close"), ImVec2(btnW, 40.0f * ui::Layout::dpiScale()))) {
|
||||||
show_decrypt_dialog_ = false;
|
show_decrypt_dialog_ = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2206,7 +2257,7 @@ void App::renderPinDialogs() {
|
|||||||
ImGui::PopItemWidth();
|
ImGui::PopItemWidth();
|
||||||
|
|
||||||
if (!pin_status_.empty()) {
|
if (!pin_status_.empty()) {
|
||||||
ImGui::TextColored(ImVec4(1, 0.7f, 0, 1), "%s", pin_status_.c_str());
|
ImGui::TextColored(ui::material::WarningVec4(), "%s", pin_status_.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
@@ -2219,7 +2270,7 @@ void App::renderPinDialogs() {
|
|||||||
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
||||||
// Not while a passphrase change runs: it could land after the old passphrase is verified.
|
// Not while a passphrase change runs: it could land after the old passphrase is verified.
|
||||||
ImGui::BeginDisabled(!valid || pin_in_progress_ || encrypt_in_progress_);
|
ImGui::BeginDisabled(!valid || pin_in_progress_ || encrypt_in_progress_);
|
||||||
if (ui::material::TactileButton(TR("settings_set_pin"), ImVec2(btnW, 40))) {
|
if (ui::material::TactileButton(TR("settings_set_pin"), ImVec2(btnW, 40.0f * ui::Layout::dpiScale()))) {
|
||||||
pin_in_progress_ = true;
|
pin_in_progress_ = true;
|
||||||
pin_status_ = TR("sec_verifying_passphrase");
|
pin_status_ = TR("sec_verifying_passphrase");
|
||||||
|
|
||||||
@@ -2292,7 +2343,7 @@ void App::renderPinDialogs() {
|
|||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
// Cancel does what Esc/close does — dismiss without applying. Buffers are wiped by
|
// Cancel does what Esc/close does — dismiss without applying. Buffers are wiped by
|
||||||
// the !show_pin_setup_ cleanup block below.
|
// the !show_pin_setup_ cleanup block below.
|
||||||
if (ui::material::TactileButton(TR("cancel"), ImVec2(btnW, 40))) {
|
if (ui::material::TactileButton(TR("cancel"), ImVec2(btnW, 40.0f * ui::Layout::dpiScale()))) {
|
||||||
show_pin_setup_ = false;
|
show_pin_setup_ = false;
|
||||||
}
|
}
|
||||||
EndOverlayDialog();
|
EndOverlayDialog();
|
||||||
@@ -2336,7 +2387,7 @@ void App::renderPinDialogs() {
|
|||||||
ImGui::PopItemWidth();
|
ImGui::PopItemWidth();
|
||||||
|
|
||||||
if (!pin_status_.empty()) {
|
if (!pin_status_.empty()) {
|
||||||
ImGui::TextColored(ImVec4(1, 0.7f, 0, 1), "%s", pin_status_.c_str());
|
ImGui::TextColored(ui::material::WarningVec4(), "%s", pin_status_.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
@@ -2346,7 +2397,7 @@ void App::renderPinDialogs() {
|
|||||||
strcmp(pin_buf_, pin_confirm_buf_) == 0;
|
strcmp(pin_buf_, pin_confirm_buf_) == 0;
|
||||||
|
|
||||||
ImGui::BeginDisabled(!valid || pin_in_progress_);
|
ImGui::BeginDisabled(!valid || pin_in_progress_);
|
||||||
if (ui::material::TactileButton(TR("settings_change_pin"), ImVec2(-1, 40))) {
|
if (ui::material::TactileButton(TR("settings_change_pin"), ImVec2(-1, 40.0f * ui::Layout::dpiScale()))) {
|
||||||
pin_in_progress_ = true;
|
pin_in_progress_ = true;
|
||||||
pin_status_ = TR("sec_changing_pin");
|
pin_status_ = TR("sec_changing_pin");
|
||||||
std::string oldPin(pin_old_buf_);
|
std::string oldPin(pin_old_buf_);
|
||||||
@@ -2405,13 +2456,13 @@ void App::renderPinDialogs() {
|
|||||||
ImGui::PopItemWidth();
|
ImGui::PopItemWidth();
|
||||||
|
|
||||||
if (!pin_status_.empty()) {
|
if (!pin_status_.empty()) {
|
||||||
ImGui::TextColored(ImVec4(1, 0.7f, 0, 1), "%s", pin_status_.c_str());
|
ImGui::TextColored(ui::material::WarningVec4(), "%s", pin_status_.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
bool valid = strlen(pin_old_buf_) >= 4;
|
bool valid = strlen(pin_old_buf_) >= 4;
|
||||||
ImGui::BeginDisabled(!valid || pin_in_progress_);
|
ImGui::BeginDisabled(!valid || pin_in_progress_);
|
||||||
if (ui::material::TactileButton(TR("settings_remove_pin"), ImVec2(-1, 40))) {
|
if (ui::material::TactileButton(TR("settings_remove_pin"), ImVec2(-1, 40.0f * ui::Layout::dpiScale()))) {
|
||||||
pin_in_progress_ = true;
|
pin_in_progress_ = true;
|
||||||
pin_status_ = TR("sec_verifying_pin");
|
pin_status_ = TR("sec_verifying_pin");
|
||||||
std::string oldPin(pin_old_buf_);
|
std::string oldPin(pin_old_buf_);
|
||||||
|
|||||||
+4
-3
@@ -354,6 +354,9 @@ void App::clearDemoWalletData()
|
|||||||
seed_migration_precheck_ = SeedMigrationPrecheck::Pending;
|
seed_migration_precheck_ = SeedMigrationPrecheck::Pending;
|
||||||
seed_migration_precheck_started_ = false;
|
seed_migration_precheck_started_ = false;
|
||||||
seed_migration_backed_up_ = false;
|
seed_migration_backed_up_ = false;
|
||||||
|
seed_migration_understood_ = false;
|
||||||
|
seed_migration_confirm_discard_ = false;
|
||||||
|
seed_migration_confirm_sweep_ = false;
|
||||||
seed_migration_balance_ = 0.0;
|
seed_migration_balance_ = 0.0;
|
||||||
seed_migration_balance_loaded_ = false;
|
seed_migration_balance_loaded_ = false;
|
||||||
seed_migration_nofunds_confirmed_ = false;
|
seed_migration_nofunds_confirmed_ = false;
|
||||||
@@ -567,8 +570,6 @@ void App::buildSweepCatalog()
|
|||||||
[](App& a) { ui::ContactsSweepCloseDialog(); restoreSweepContacts(a); });
|
[](App& a) { ui::ContactsSweepCloseDialog(); restoreSweepContacts(a); });
|
||||||
add("modal-about", ui::NavPage::Overview,
|
add("modal-about", ui::NavPage::Overview,
|
||||||
[](App& a) { a.show_about_ = true; }, [](App& a) { a.show_about_ = false; });
|
[](App& a) { a.show_about_ = true; }, [](App& a) { a.show_about_ = false; });
|
||||||
add("modal-settings", ui::NavPage::Settings,
|
|
||||||
[](App& a) { a.show_settings_ = true; }, [](App& a) { a.show_settings_ = false; });
|
|
||||||
|
|
||||||
// Seed-backup: pre-set fetch + a demo phrase so it renders the word grid without any RPC.
|
// Seed-backup: pre-set fetch + a demo phrase so it renders the word grid without any RPC.
|
||||||
add("modal-seed-backup", ui::NavPage::Overview,
|
add("modal-seed-backup", ui::NavPage::Overview,
|
||||||
@@ -623,7 +624,7 @@ void App::buildSweepCatalog()
|
|||||||
a.seed_migration_precheck_started_ = true;
|
a.seed_migration_precheck_started_ = true;
|
||||||
});
|
});
|
||||||
mig("modal-migrate-showseed", SeedMigrationStep::ShowSeed,
|
mig("modal-migrate-showseed", SeedMigrationStep::ShowSeed,
|
||||||
[](App& a) { a.seed_migration_seed_ = kDemoMnemonic; a.seed_migration_backed_up_ = false; });
|
[](App& a) { a.seed_migration_seed_ = kDemoMnemonic; a.seed_migration_backed_up_ = false; a.seed_migration_understood_ = false; });
|
||||||
mig("modal-migrate-sweep", SeedMigrationStep::Sweep,
|
mig("modal-migrate-sweep", SeedMigrationStep::Sweep,
|
||||||
[](App& a) { a.seed_migration_balance_ = 15.75000000; a.seed_migration_balance_loaded_ = true; });
|
[](App& a) { a.seed_migration_balance_ = 15.75000000; a.seed_migration_balance_loaded_ = true; });
|
||||||
mig("modal-migrate-nofunds", SeedMigrationStep::Sweep,
|
mig("modal-migrate-nofunds", SeedMigrationStep::Sweep,
|
||||||
|
|||||||
+63
-27
@@ -800,21 +800,22 @@ void App::renderFirstRunWizard() {
|
|||||||
// Daemon status indicator
|
// Daemon status indicator
|
||||||
{
|
{
|
||||||
bool daemonUp = isEmbeddedDaemonRunning();
|
bool daemonUp = isEmbeddedDaemonRunning();
|
||||||
const std::string& dStatus = getDaemonStatus();
|
// Key the dot + label off the daemon state ENUM, not an English substring of the status
|
||||||
ImU32 dotCol = daemonUp ? IM_COL32(76, 175, 80, 200) // green
|
// text — the substring match silently failed under every non-English language.
|
||||||
: IM_COL32(120, 120, 120, 160); // gray
|
const bool daemonStopping = daemon_controller_ &&
|
||||||
if (dStatus.find("Stopping") != std::string::npos)
|
daemon_controller_->state() == daemon::EmbeddedDaemon::State::Stopping;
|
||||||
dotCol = IM_COL32(255, 167, 38, 200); // orange
|
ImU32 dotCol = daemonStopping ? IM_COL32(255, 167, 38, 230) // orange
|
||||||
|
: daemonUp ? IM_COL32(76, 175, 80, 230) // green
|
||||||
|
: IM_COL32(120, 120, 120, 180); // gray
|
||||||
float dotR = 3.5f * dp;
|
float dotR = 3.5f * dp;
|
||||||
dl->AddCircleFilled(ImVec2(cx + dotR, cy + captionFont->LegacySize * 0.5f),
|
dl->AddCircleFilled(ImVec2(cx + dotR, cy + captionFont->LegacySize * 0.5f),
|
||||||
dotR, dotCol);
|
dotR, dotCol);
|
||||||
const char* label = daemonUp ? (dStatus.find("Stopping") != std::string::npos
|
const char* label = daemonStopping ? TR("bootstrap_daemon_stopping")
|
||||||
? TR("bootstrap_daemon_stopping")
|
: daemonUp ? TR("bootstrap_daemon_running")
|
||||||
: TR("bootstrap_daemon_running"))
|
|
||||||
: TR("bootstrap_daemon_stopped");
|
: TR("bootstrap_daemon_stopped");
|
||||||
dl->AddText(captionFont, captionFont->LegacySize,
|
dl->AddText(captionFont, captionFont->LegacySize,
|
||||||
ImVec2(cx + dotR * 2.0f + 6.0f * dp, cy),
|
ImVec2(cx + dotR * 2.0f + 6.0f * dp, cy),
|
||||||
(dimCol & 0x00FFFFFF) | IM_COL32(0,0,0,140), label);
|
(dimCol & 0x00FFFFFF) | IM_COL32(0,0,0,220), label);
|
||||||
cy += captionFont->LegacySize + 6.0f * dp;
|
cy += captionFont->LegacySize + 6.0f * dp;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -862,14 +863,19 @@ void App::renderFirstRunWizard() {
|
|||||||
ui::material::Error(), TR("wiz_download_failed"));
|
ui::material::Error(), TR("wiz_download_failed"));
|
||||||
cy += bodyFont->LegacySize + 8.0f * dp;
|
cy += bodyFont->LegacySize + 8.0f * dp;
|
||||||
|
|
||||||
|
// Wrap the (often long) error within the card instead of letting it clip off the edge.
|
||||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), textCol,
|
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), textCol,
|
||||||
errMsg.c_str());
|
errMsg.c_str(), nullptr, contentW);
|
||||||
cy += captionFont->LegacySize + 16.0f * dp;
|
ImVec2 es = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, contentW, errMsg.c_str());
|
||||||
|
cy += es.y + 16.0f * dp;
|
||||||
|
|
||||||
// Retry / Skip
|
// Retry / (Try mirror) / Skip. Offer the mirror unless the last attempt already used it.
|
||||||
|
const std::string mirrorUrl = std::string(util::Bootstrap::kMirrorUrl) + "/" + util::Bootstrap::kZipName;
|
||||||
|
const bool showMirror = (wizard_bootstrap_url_ != mirrorUrl);
|
||||||
float btnW2 = 120.0f * dp;
|
float btnW2 = 120.0f * dp;
|
||||||
float btnH2 = 40.0f * dp;
|
float btnH2 = 40.0f * dp;
|
||||||
float totalBW = btnW2 * 2 + 12.0f * dp;
|
const int nBtn = showMirror ? 3 : 2;
|
||||||
|
float totalBW = btnW2 * nBtn + 12.0f * dp * (nBtn - 1);
|
||||||
float bx = rightX + (colW - totalBW) * 0.5f;
|
float bx = rightX + (colW - totalBW) * 0.5f;
|
||||||
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(bx, cy));
|
ImGui::SetCursorScreenPos(ImVec2(bx, cy));
|
||||||
@@ -883,14 +889,33 @@ void App::renderFirstRunWizard() {
|
|||||||
stopDaemonForBootstrap();
|
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);
|
if (wizard_bootstrap_url_.empty()) bootstrap_->start(dataDir);
|
||||||
|
else bootstrap_->start(dataDir, wizard_bootstrap_url_); // keep the user's chosen source
|
||||||
wizard_phase_ = WizardPhase::BootstrapInProgress;
|
wizard_phase_ = WizardPhase::BootstrapInProgress;
|
||||||
}
|
}
|
||||||
ImGui::EndDisabled();
|
ImGui::EndDisabled();
|
||||||
ImGui::PopStyleVar();
|
ImGui::PopStyleVar();
|
||||||
ImGui::PopStyleColor(3);
|
ImGui::PopStyleColor(3);
|
||||||
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(bx + btnW2 + 12.0f * dp, cy));
|
float nextBx = bx + btnW2 + 12.0f * dp;
|
||||||
|
if (showMirror) {
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(nextBx, cy));
|
||||||
|
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||||
|
ImGui::BeginDisabled(!supportsFullNodeLifecycleActions());
|
||||||
|
if (ui::material::TactileButton(TR("bootstrap_try_mirror"), ImVec2(btnW2, btnH2)) && !refuseIfNodeBusy()) {
|
||||||
|
stopDaemonForBootstrap();
|
||||||
|
bootstrap_ = std::make_unique<util::Bootstrap>();
|
||||||
|
bootstrap_->start(util::Platform::getDragonXDataDir(), mirrorUrl);
|
||||||
|
wizard_bootstrap_url_ = mirrorUrl;
|
||||||
|
wizard_phase_ = WizardPhase::BootstrapInProgress;
|
||||||
|
}
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
if (ImGui::IsItemHovered()) ui::material::Tooltip(TR("bootstrap_use_mirror_tooltip"));
|
||||||
|
ImGui::PopStyleVar();
|
||||||
|
nextBx += btnW2 + 12.0f * dp;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(nextBx, cy));
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||||
if (ui::material::TactileButton(TR("wiz_skip"), ImVec2(btnW2, btnH2))) {
|
if (ui::material::TactileButton(TR("wiz_skip"), ImVec2(btnW2, btnH2))) {
|
||||||
wizard_phase_ = WizardPhase::EncryptOffer;
|
wizard_phase_ = WizardPhase::EncryptOffer;
|
||||||
@@ -1014,6 +1039,15 @@ void App::renderFirstRunWizard() {
|
|||||||
cy += bsSize.y + 8.0f * dp;
|
cy += bsSize.y + 8.0f * dp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Honest size/time hint (no number is known until the download HEADs the remote file).
|
||||||
|
{
|
||||||
|
const char* hintText = TR("wiz_bootstrap_size_hint");
|
||||||
|
ImVec2 hs = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, contentW, hintText);
|
||||||
|
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy),
|
||||||
|
(dimCol & 0x00FFFFFF) | IM_COL32(0, 0, 0, 200), hintText, nullptr, contentW);
|
||||||
|
cy += hs.y + 8.0f * dp;
|
||||||
|
}
|
||||||
|
|
||||||
// Trust warning
|
// Trust warning
|
||||||
{
|
{
|
||||||
float warnOpacity = S.drawElement("screens.first-run", "trust-warning").opacity;
|
float warnOpacity = S.drawElement("screens.first-run", "trust-warning").opacity;
|
||||||
@@ -1031,21 +1065,20 @@ void App::renderFirstRunWizard() {
|
|||||||
// Daemon status indicator (subtle, before buttons)
|
// Daemon status indicator (subtle, before buttons)
|
||||||
{
|
{
|
||||||
bool daemonUp = isEmbeddedDaemonRunning();
|
bool daemonUp = isEmbeddedDaemonRunning();
|
||||||
const std::string& dStatus = getDaemonStatus();
|
const bool daemonStopping = daemon_controller_ &&
|
||||||
ImU32 dotCol = daemonUp ? IM_COL32(76, 175, 80, 200)
|
daemon_controller_->state() == daemon::EmbeddedDaemon::State::Stopping;
|
||||||
: IM_COL32(120, 120, 120, 160);
|
ImU32 dotCol = daemonStopping ? IM_COL32(255, 167, 38, 230)
|
||||||
if (dStatus.find("Stopping") != std::string::npos)
|
: daemonUp ? IM_COL32(76, 175, 80, 230)
|
||||||
dotCol = IM_COL32(255, 167, 38, 200);
|
: IM_COL32(120, 120, 120, 180);
|
||||||
float dotR = 3.5f * dp;
|
float dotR = 3.5f * dp;
|
||||||
dl->AddCircleFilled(ImVec2(cx + dotR, cy + captionFont->LegacySize * 0.5f),
|
dl->AddCircleFilled(ImVec2(cx + dotR, cy + captionFont->LegacySize * 0.5f),
|
||||||
dotR, dotCol);
|
dotR, dotCol);
|
||||||
const char* label = daemonUp ? (dStatus.find("Stopping") != std::string::npos
|
const char* label = daemonStopping ? TR("bootstrap_daemon_stopping")
|
||||||
? TR("bootstrap_daemon_stopping")
|
: daemonUp ? TR("bootstrap_daemon_running")
|
||||||
: TR("bootstrap_daemon_running"))
|
|
||||||
: TR("bootstrap_daemon_stopped");
|
: TR("bootstrap_daemon_stopped");
|
||||||
dl->AddText(captionFont, captionFont->LegacySize,
|
dl->AddText(captionFont, captionFont->LegacySize,
|
||||||
ImVec2(cx + dotR * 2.0f + 6.0f * dp, cy),
|
ImVec2(cx + dotR * 2.0f + 6.0f * dp, cy),
|
||||||
(dimCol & 0x00FFFFFF) | IM_COL32(0,0,0,140), label);
|
(dimCol & 0x00FFFFFF) | IM_COL32(0,0,0,220), label);
|
||||||
cy += captionFont->LegacySize + 10.0f * dp;
|
cy += captionFont->LegacySize + 10.0f * dp;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1071,6 +1104,7 @@ void App::renderFirstRunWizard() {
|
|||||||
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);
|
||||||
|
wizard_bootstrap_url_.clear(); // default source; Retry won't force the mirror
|
||||||
wizard_phase_ = WizardPhase::BootstrapInProgress;
|
wizard_phase_ = WizardPhase::BootstrapInProgress;
|
||||||
}
|
}
|
||||||
ImGui::EndDisabled();
|
ImGui::EndDisabled();
|
||||||
@@ -1084,17 +1118,18 @@ void App::renderFirstRunWizard() {
|
|||||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnSurface()));
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnSurface()));
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||||
ImGui::BeginDisabled(!supportsFullNodeLifecycleActions());
|
ImGui::BeginDisabled(!supportsFullNodeLifecycleActions());
|
||||||
if (ui::material::TactileButton(TR("bootstrap_mirror"), ImVec2(mirrorW, btnH2)) && !refuseIfNodeBusy()) {
|
if (ui::material::TactileButton(TR("bootstrap_use_mirror"), ImVec2(mirrorW, btnH2)) && !refuseIfNodeBusy()) {
|
||||||
stopDaemonForBootstrap();
|
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();
|
||||||
std::string mirrorUrl = std::string(util::Bootstrap::kMirrorUrl) + "/" + util::Bootstrap::kZipName;
|
std::string mirrorUrl = std::string(util::Bootstrap::kMirrorUrl) + "/" + util::Bootstrap::kZipName;
|
||||||
bootstrap_->start(dataDir, mirrorUrl);
|
bootstrap_->start(dataDir, mirrorUrl);
|
||||||
|
wizard_bootstrap_url_ = mirrorUrl; // Retry on the failed card keeps this source
|
||||||
wizard_phase_ = WizardPhase::BootstrapInProgress;
|
wizard_phase_ = WizardPhase::BootstrapInProgress;
|
||||||
}
|
}
|
||||||
ImGui::EndDisabled();
|
ImGui::EndDisabled();
|
||||||
if (ImGui::IsItemHovered()) {
|
if (ImGui::IsItemHovered()) {
|
||||||
ui::material::Tooltip(TR("bootstrap_mirror_tooltip"));
|
ui::material::Tooltip(TR("bootstrap_use_mirror_tooltip"));
|
||||||
}
|
}
|
||||||
ImGui::PopStyleVar();
|
ImGui::PopStyleVar();
|
||||||
ImGui::PopStyleColor(3);
|
ImGui::PopStyleColor(3);
|
||||||
@@ -1421,7 +1456,8 @@ void App::renderFirstRunWizard() {
|
|||||||
wizard_phase_ = WizardPhase::Done;
|
wizard_phase_ = WizardPhase::Done;
|
||||||
settings_->setWizardCompleted(true);
|
settings_->setWizardCompleted(true);
|
||||||
settings_->save();
|
settings_->save();
|
||||||
ui::Notifications::instance().info(TR("wiz_encrypt_bg"));
|
// The handoff toast is now emitted once at the completion point in app.cpp
|
||||||
|
// (wiz_handoff_encrypting), so don't also toast here.
|
||||||
}
|
}
|
||||||
ImGui::EndDisabled();
|
ImGui::EndDisabled();
|
||||||
ImGui::PopStyleVar();
|
ImGui::PopStyleVar();
|
||||||
|
|||||||
+73
-13
@@ -97,6 +97,7 @@ void ChatDatabase::lock()
|
|||||||
{
|
{
|
||||||
sodium_memzero(key_.data(), key_.size());
|
sodium_memzero(key_.data(), key_.size());
|
||||||
key_ready_ = false;
|
key_ready_ = false;
|
||||||
|
lockSensitive(); // also drop the sensitive-conversation vault key
|
||||||
wallet_tag_.clear();
|
wallet_tag_.clear();
|
||||||
tombstones_.clear();
|
tombstones_.clear();
|
||||||
}
|
}
|
||||||
@@ -106,10 +107,24 @@ bool ChatDatabase::append(const ChatMessage& message)
|
|||||||
if (!key_ready_ || !ensureOpen()) return false;
|
if (!key_ready_ || !ensureOpen()) return false;
|
||||||
if (isTombstoned(message)) return false; // locally deleted — don't re-persist on a chain re-scan
|
if (isTombstoned(message)) return false; // locally deleted — don't re-persist on a chain re-scan
|
||||||
|
|
||||||
|
bool skip = false;
|
||||||
|
const unsigned char* wkey = writeKeyFor(message.conversation_id, skip);
|
||||||
|
if (skip) return false; // sensitive conversation, vault locked — don't persist (re-imports when unlocked)
|
||||||
|
|
||||||
std::vector<unsigned char> nonce;
|
std::vector<unsigned char> nonce;
|
||||||
std::vector<unsigned char> cipher;
|
std::vector<unsigned char> cipher;
|
||||||
std::string plain = serialize(message); // full plaintext (decrypted body + metadata)
|
std::string plain;
|
||||||
const bool encrypted = encrypt(plain, nonce, cipher);
|
bool encrypted = false;
|
||||||
|
try {
|
||||||
|
// serialize() dumps strictly, so an invalid-UTF-8 (attacker-influenceable) body THROWS here;
|
||||||
|
// skip the row instead of letting it escape the write path and crash (appendBatch already does).
|
||||||
|
plain = serialize(message);
|
||||||
|
encrypted = encrypt(wkey, plain, nonce, cipher);
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
if (!plain.empty()) sodium_memzero(&plain[0], plain.size());
|
||||||
|
DEBUG_LOGF("[chat] append skipped a malformed message: %s\n", e.what());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (!plain.empty()) sodium_memzero(&plain[0], plain.size()); // don't leave it on the heap
|
if (!plain.empty()) sodium_memzero(&plain[0], plain.size()); // don't leave it on the heap
|
||||||
if (!encrypted) return false;
|
if (!encrypted) return false;
|
||||||
|
|
||||||
@@ -142,10 +157,13 @@ int ChatDatabase::appendBatch(const std::vector<ChatMessage>& messages)
|
|||||||
rows.reserve(messages.size());
|
rows.reserve(messages.size());
|
||||||
for (const auto& m : messages) {
|
for (const auto& m : messages) {
|
||||||
if (isTombstoned(m)) continue; // locally deleted — don't re-persist
|
if (isTombstoned(m)) continue; // locally deleted — don't re-persist
|
||||||
|
bool skip = false;
|
||||||
|
const unsigned char* wkey = writeKeyFor(m.conversation_id, skip);
|
||||||
|
if (skip) continue; // sensitive + vault locked — don't persist
|
||||||
try {
|
try {
|
||||||
std::string plain = serialize(m); // strict json.dump() may throw on non-UTF-8 body
|
std::string plain = serialize(m); // strict json.dump() may throw on non-UTF-8 body
|
||||||
std::vector<unsigned char> nonce, cipher;
|
std::vector<unsigned char> nonce, cipher;
|
||||||
const bool ok = encrypt(plain, nonce, cipher);
|
const bool ok = encrypt(wkey, plain, nonce, cipher);
|
||||||
if (!plain.empty()) sodium_memzero(&plain[0], plain.size());
|
if (!plain.empty()) sodium_memzero(&plain[0], plain.size());
|
||||||
if (!ok) continue;
|
if (!ok) continue;
|
||||||
rows.push_back({ dedupHash(m.txid, m.payload_position), std::move(nonce), std::move(cipher) });
|
rows.push_back({ dedupHash(m.txid, m.payload_position), std::move(nonce), std::move(cipher) });
|
||||||
@@ -185,10 +203,22 @@ bool ChatDatabase::upsert(const ChatMessage& message)
|
|||||||
{
|
{
|
||||||
if (!key_ready_ || !ensureOpen()) return false;
|
if (!key_ready_ || !ensureOpen()) return false;
|
||||||
|
|
||||||
|
bool skip = false;
|
||||||
|
const unsigned char* wkey = writeKeyFor(message.conversation_id, skip);
|
||||||
|
if (skip) return false; // sensitive conversation, vault locked — don't persist the echo under the seed key
|
||||||
|
|
||||||
std::vector<unsigned char> nonce;
|
std::vector<unsigned char> nonce;
|
||||||
std::vector<unsigned char> cipher;
|
std::vector<unsigned char> cipher;
|
||||||
std::string plain = serialize(message);
|
std::string plain;
|
||||||
const bool encrypted = encrypt(plain, nonce, cipher);
|
bool encrypted = false;
|
||||||
|
try {
|
||||||
|
plain = serialize(message); // strict dump → a malformed body throws; skip rather than crash
|
||||||
|
encrypted = encrypt(wkey, plain, nonce, cipher);
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
if (!plain.empty()) sodium_memzero(&plain[0], plain.size());
|
||||||
|
DEBUG_LOGF("[chat] upsert skipped a malformed message: %s\n", e.what());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (!plain.empty()) sodium_memzero(&plain[0], plain.size());
|
if (!plain.empty()) sodium_memzero(&plain[0], plain.size());
|
||||||
if (!encrypted) return false;
|
if (!encrypted) return false;
|
||||||
|
|
||||||
@@ -232,7 +262,13 @@ std::vector<ChatMessage> ChatDatabase::load()
|
|||||||
std::vector<unsigned char> nonce(noncePtr, noncePtr + nonceLen);
|
std::vector<unsigned char> nonce(noncePtr, noncePtr + nonceLen);
|
||||||
std::vector<unsigned char> cipher(cipherPtr, cipherPtr + cipherLen);
|
std::vector<unsigned char> cipher(cipherPtr, cipherPtr + cipherLen);
|
||||||
std::string plain;
|
std::string plain;
|
||||||
if (!decrypt(nonce, cipher, plain)) continue; // wrong wallet / tampered — skip
|
// Try the seed key first; a sensitive row only decrypts under the vault key (and is silently
|
||||||
|
// withheld when the vault is locked / on a fresh seed import that has no vault key). A wrong
|
||||||
|
// wallet / tampered row fails both and is skipped.
|
||||||
|
if (!decrypt(key_.data(), nonce, cipher, plain) &&
|
||||||
|
!(sensitive_ready_ && decrypt(sensitive_key_.data(), nonce, cipher, plain))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
ChatMessage message;
|
ChatMessage message;
|
||||||
if (deserialize(plain, message)) out.push_back(std::move(message));
|
if (deserialize(plain, message)) out.push_back(std::move(message));
|
||||||
@@ -416,7 +452,7 @@ std::string ChatDatabase::serialize(const ChatMessage& message) const
|
|||||||
json["ts"] = message.timestamp;
|
json["ts"] = message.timestamp;
|
||||||
json["pos"] = static_cast<std::uint64_t>(message.payload_position);
|
json["pos"] = static_cast<std::uint64_t>(message.payload_position);
|
||||||
json["dl"] = static_cast<int>(message.delivery);
|
json["dl"] = static_cast<int>(message.delivery);
|
||||||
return json.dump();
|
return json.dump(); // strict: an invalid-UTF-8 body throws and is DROPPED by the callers (contract)
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ChatDatabase::deserialize(const std::string& json, ChatMessage& out) const
|
bool ChatDatabase::deserialize(const std::string& json, ChatMessage& out) const
|
||||||
@@ -442,11 +478,34 @@ bool ChatDatabase::deserialize(const std::string& json, ChatMessage& out) const
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ChatDatabase::encrypt(const std::string& plain,
|
void ChatDatabase::setSensitiveKey(const std::array<unsigned char, 32>& key)
|
||||||
|
{
|
||||||
|
sensitive_key_ = key; // copy; the caller still owns & wipes its own copy
|
||||||
|
sensitive_ready_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatDatabase::lockSensitive()
|
||||||
|
{
|
||||||
|
sodium_memzero(sensitive_key_.data(), sensitive_key_.size());
|
||||||
|
sensitive_ready_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const unsigned char* ChatDatabase::writeKeyFor(const std::string& cid, bool& skip) const
|
||||||
|
{
|
||||||
|
skip = false;
|
||||||
|
if (sensitive_pred_ && sensitive_pred_(cid)) {
|
||||||
|
if (!sensitive_ready_) { skip = true; return nullptr; } // don't persist under the seed key
|
||||||
|
return sensitive_key_.data();
|
||||||
|
}
|
||||||
|
return key_.data();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChatDatabase::encrypt(const unsigned char* key,
|
||||||
|
const std::string& plain,
|
||||||
std::vector<unsigned char>& nonce,
|
std::vector<unsigned char>& nonce,
|
||||||
std::vector<unsigned char>& cipher) const
|
std::vector<unsigned char>& cipher) const
|
||||||
{
|
{
|
||||||
if (!key_ready_) return false;
|
if (!key) return false;
|
||||||
nonce.resize(crypto_aead_xchacha20poly1305_ietf_NPUBBYTES);
|
nonce.resize(crypto_aead_xchacha20poly1305_ietf_NPUBBYTES);
|
||||||
randombytes_buf(nonce.data(), nonce.size());
|
randombytes_buf(nonce.data(), nonce.size());
|
||||||
|
|
||||||
@@ -457,18 +516,19 @@ bool ChatDatabase::encrypt(const std::string& plain,
|
|||||||
cipher.data(), &cipherLen,
|
cipher.data(), &cipherLen,
|
||||||
reinterpret_cast<const unsigned char*>(plain.data()), plain.size(),
|
reinterpret_cast<const unsigned char*>(plain.data()), plain.size(),
|
||||||
reinterpret_cast<const unsigned char*>(ad.data()), ad.size(),
|
reinterpret_cast<const unsigned char*>(ad.data()), ad.size(),
|
||||||
nullptr, nonce.data(), key_.data()) != 0) {
|
nullptr, nonce.data(), key) != 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
cipher.resize(static_cast<std::size_t>(cipherLen));
|
cipher.resize(static_cast<std::size_t>(cipherLen));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ChatDatabase::decrypt(const std::vector<unsigned char>& nonce,
|
bool ChatDatabase::decrypt(const unsigned char* key,
|
||||||
|
const std::vector<unsigned char>& nonce,
|
||||||
const std::vector<unsigned char>& cipher,
|
const std::vector<unsigned char>& cipher,
|
||||||
std::string& plain) const
|
std::string& plain) const
|
||||||
{
|
{
|
||||||
if (!key_ready_) return false;
|
if (!key) return false;
|
||||||
if (nonce.size() != crypto_aead_xchacha20poly1305_ietf_NPUBBYTES) return false;
|
if (nonce.size() != crypto_aead_xchacha20poly1305_ietf_NPUBBYTES) return false;
|
||||||
if (cipher.size() < crypto_aead_xchacha20poly1305_ietf_ABYTES) return false;
|
if (cipher.size() < crypto_aead_xchacha20poly1305_ietf_ABYTES) return false;
|
||||||
|
|
||||||
@@ -479,7 +539,7 @@ bool ChatDatabase::decrypt(const std::vector<unsigned char>& nonce,
|
|||||||
out.data(), &outLen, nullptr,
|
out.data(), &outLen, nullptr,
|
||||||
cipher.data(), cipher.size(),
|
cipher.data(), cipher.size(),
|
||||||
reinterpret_cast<const unsigned char*>(ad.data()), ad.size(),
|
reinterpret_cast<const unsigned char*>(ad.data()), ad.size(),
|
||||||
nonce.data(), key_.data()) != 0) {
|
nonce.data(), key) != 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
plain.assign(reinterpret_cast<const char*>(out.data()), static_cast<std::size_t>(outLen));
|
plain.assign(reinterpret_cast<const char*>(out.data()), static_cast<std::size_t>(outLen));
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
|
#include <functional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@@ -40,6 +41,17 @@ public:
|
|||||||
void lock(); // wipe the key material (DB handle stays open); load()/append() then no-op
|
void lock(); // wipe the key material (DB handle stays open); load()/append() then no-op
|
||||||
bool hasKey() const { return key_ready_; }
|
bool hasKey() const { return key_ready_; }
|
||||||
|
|
||||||
|
// --- Sensitive conversations (per-conversation local-key encryption) ---
|
||||||
|
// Provide the local vault key (from ChatSensitiveVault). Rows of conversations the sensitive
|
||||||
|
// predicate marks sensitive are AEAD-encrypted under THIS key instead of the seed-derived key, so
|
||||||
|
// they can't be read without it (even by the seed). The caller still owns and should wipe `key`.
|
||||||
|
void setSensitiveKey(const std::array<unsigned char, 32>& key);
|
||||||
|
void lockSensitive(); // wipe the sensitive key; sensitive rows then withheld
|
||||||
|
bool hasSensitiveKey() const { return sensitive_ready_; }
|
||||||
|
// Predicate: true for a conversation id that should be stored under the sensitive key. Consulted on
|
||||||
|
// WRITE to pick the key; on READ both keys are tried, so a predicate isn't needed to decrypt.
|
||||||
|
void setSensitivePredicate(std::function<bool(const std::string&)> pred) { sensitive_pred_ = std::move(pred); }
|
||||||
|
|
||||||
// Persist one message (INSERT OR IGNORE, deduped by a keyed hash of txid+payload_position).
|
// Persist one message (INSERT OR IGNORE, deduped by a keyed hash of txid+payload_position).
|
||||||
// Returns true if newly inserted; false on duplicate or while locked.
|
// Returns true if newly inserted; false on duplicate or while locked.
|
||||||
bool append(const ChatMessage& message);
|
bool append(const ChatMessage& message);
|
||||||
@@ -80,12 +92,18 @@ private:
|
|||||||
std::string dedupHash(const std::string& txid, std::size_t position) const;
|
std::string dedupHash(const std::string& txid, std::size_t position) const;
|
||||||
std::string serialize(const ChatMessage& message) const;
|
std::string serialize(const ChatMessage& message) const;
|
||||||
bool deserialize(const std::string& json, ChatMessage& out) const;
|
bool deserialize(const std::string& json, ChatMessage& out) const;
|
||||||
bool encrypt(const std::string& plain,
|
bool encrypt(const unsigned char* key,
|
||||||
|
const std::string& plain,
|
||||||
std::vector<unsigned char>& nonce,
|
std::vector<unsigned char>& nonce,
|
||||||
std::vector<unsigned char>& cipher) const;
|
std::vector<unsigned char>& cipher) const;
|
||||||
bool decrypt(const std::vector<unsigned char>& nonce,
|
bool decrypt(const unsigned char* key,
|
||||||
|
const std::vector<unsigned char>& nonce,
|
||||||
const std::vector<unsigned char>& cipher,
|
const std::vector<unsigned char>& cipher,
|
||||||
std::string& plain) const;
|
std::string& plain) const;
|
||||||
|
// The key a row for `cid` is written under. Returns nullptr and sets skip=true when the conversation
|
||||||
|
// is sensitive but the vault is locked — the row must NOT be persisted (under the seed key it would be
|
||||||
|
// readable without the vault, defeating the point); it re-imports from chain once the vault is unlocked.
|
||||||
|
const unsigned char* writeKeyFor(const std::string& cid, bool& skip) const;
|
||||||
void close();
|
void close();
|
||||||
|
|
||||||
sqlite3* db_ = nullptr;
|
sqlite3* db_ = nullptr;
|
||||||
@@ -93,6 +111,9 @@ private:
|
|||||||
std::array<unsigned char, 32> key_{}; // AEAD storage key (seed-derived)
|
std::array<unsigned char, 32> key_{}; // AEAD storage key (seed-derived)
|
||||||
std::string wallet_tag_; // seed-derived row partition (a keyed hash, hex)
|
std::string wallet_tag_; // seed-derived row partition (a keyed hash, hex)
|
||||||
bool key_ready_ = false;
|
bool key_ready_ = false;
|
||||||
|
std::array<unsigned char, 32> sensitive_key_{}; // local vault key for sensitive conversations' rows
|
||||||
|
bool sensitive_ready_ = false;
|
||||||
|
std::function<bool(const std::string&)> sensitive_pred_; // true => store this cid under the sensitive key
|
||||||
std::unordered_set<std::string> tombstones_; // dedup_hash cache of locally-deleted messages
|
std::unordered_set<std::string> tombstones_; // dedup_hash cache of locally-deleted messages
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+171
-16
@@ -5,6 +5,10 @@
|
|||||||
#include "chat_protocol.h" // kHushChat* constants
|
#include "chat_protocol.h" // kHushChat* constants
|
||||||
|
|
||||||
#include <nlohmann/json.hpp>
|
#include <nlohmann/json.hpp>
|
||||||
|
#include <sodium.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <ctime>
|
||||||
|
|
||||||
namespace dragonx::chat {
|
namespace dragonx::chat {
|
||||||
|
|
||||||
@@ -17,31 +21,61 @@ std::string buildHeaderMemo(const std::string& replyZaddr,
|
|||||||
const char* type,
|
const char* type,
|
||||||
const std::string& streamHeaderHex,
|
const std::string& streamHeaderHex,
|
||||||
const std::string& publicKeyHex,
|
const std::string& publicKeyHex,
|
||||||
std::int64_t sentAt)
|
std::int64_t sentAt,
|
||||||
|
const std::string& op = "",
|
||||||
|
int parts = 1,
|
||||||
|
const std::string& messageId = "")
|
||||||
{
|
{
|
||||||
nlohmann::json header;
|
nlohmann::json header;
|
||||||
header["h"] = 1; // header number (>= 1)
|
header["h"] = 1; // header number (>= 1)
|
||||||
header["v"] = kHushChatSupportedVersion; // 0
|
header["v"] = kHushChatSupportedVersion; // 0
|
||||||
header["z"] = replyZaddr; // where the peer should reply (my address)
|
header["z"] = replyZaddr; // where the peer should reply (my address)
|
||||||
header["cid"] = conversationId;
|
header["cid"] = conversationId;
|
||||||
header["t"] = type; // "Memo" or "Cont"
|
header["t"] = type; // "Memo" / "Cont" / "Ctrl"
|
||||||
header["e"] = streamHeaderHex; // 48-hex secretstream header (Memo) / "" (Cont)
|
header["e"] = streamHeaderHex; // 48-hex secretstream header (Memo) / "" (Cont, Ctrl)
|
||||||
header["p"] = publicKeyHex; // my 64-hex crypto_kx public key
|
header["p"] = publicKeyHex; // my 64-hex crypto_kx public key
|
||||||
|
if (!op.empty()) header["op"] = op; // Control only; alphabetically sorts to cid,e,h,op,p,t,v,z
|
||||||
|
if (parts > 1) header["n"] = parts; // multi-part Message only; omitted (==1) keeps 1-part byte-identical
|
||||||
|
if (!messageId.empty()) header["mid"] = messageId; // tier 3 only: ties this tx's slice to its siblings
|
||||||
if (sentAt > 0) header["ts"] = sentAt; // optional sender compose time (Unix s) — receiver shows this
|
if (sentAt > 0) header["ts"] = sentAt; // optional sender compose time (Unix s) — receiver shows this
|
||||||
return header.dump();
|
return header.dump();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The N 2-hex index prefix for a multi-part ciphertext chunk (index 0..kHushChatMaxMessageParts-1).
|
||||||
|
std::string partIndexPrefix(std::size_t i)
|
||||||
|
{
|
||||||
|
static const char* kHex = "0123456789abcdef";
|
||||||
|
std::string p;
|
||||||
|
p += kHex[(i >> 4) & 0xF];
|
||||||
|
p += kHex[i & 0xF];
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
bool present(const std::string& value) { return !value.empty(); }
|
bool present(const std::string& value) { return !value.empty(); }
|
||||||
|
|
||||||
|
// A random 8-byte message id as 16 hex chars — ties a tier-3 message's sibling transactions together.
|
||||||
|
std::string newMessageId()
|
||||||
|
{
|
||||||
|
unsigned char buf[8];
|
||||||
|
randombytes_buf(buf, sizeof buf);
|
||||||
|
static const char* kHex = "0123456789abcdef";
|
||||||
|
std::string s;
|
||||||
|
s.reserve(sizeof(buf) * 2);
|
||||||
|
for (unsigned char b : buf) { s += kHex[b >> 4]; s += kHex[b & 0xF]; }
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
std::array<ChatSendOutput, 2> chatSendOutputs(const OutgoingChatMemos& memos, bool utf8Prefix)
|
std::vector<ChatSendOutput> chatSendOutputs(const OutgoingChatMemos& memos, bool utf8Prefix)
|
||||||
{
|
{
|
||||||
const std::string prefix = utf8Prefix ? "utf8:" : "";
|
const std::string prefix = utf8Prefix ? "utf8:" : "";
|
||||||
return {{
|
std::vector<ChatSendOutput> outs;
|
||||||
{ memos.recipientZaddr, prefix + memos.headerMemo }, // header — the lower memo position
|
outs.reserve(1 + memos.payloadMemos.size());
|
||||||
{ memos.recipientZaddr, prefix + memos.payloadMemo },
|
outs.push_back({ memos.recipientZaddr, prefix + memos.headerMemo }); // header (position doesn't matter;
|
||||||
}};
|
for (const auto& p : memos.payloadMemos) // the receiver reassembles by index)
|
||||||
|
outs.push_back({ memos.recipientZaddr, prefix + p });
|
||||||
|
return outs;
|
||||||
}
|
}
|
||||||
|
|
||||||
ChatComposeStatus buildOutgoingMessage(const ChatKeyPair& mine,
|
ChatComposeStatus buildOutgoingMessage(const ChatKeyPair& mine,
|
||||||
@@ -67,19 +101,111 @@ ChatComposeStatus buildOutgoingMessage(const ChatKeyPair& mine,
|
|||||||
return ChatComposeStatus::EncryptFailed;
|
return ChatComposeStatus::EncryptFailed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Chunk the ciphertext hex across payload memos. A single memo → the classic 1-part message, byte-
|
||||||
|
// identical on the wire (no "n", no index prefix). Longer → N index-prefixed chunks (tier 2), capped
|
||||||
|
// at kHushChatMaxMessageParts; beyond that it is refused as TooLong (tier 3 would lift the ceiling).
|
||||||
|
std::vector<std::string> payloads;
|
||||||
|
int parts = 1;
|
||||||
|
if (ciphertextHex.size() <= kHushChatMemoByteLimit) {
|
||||||
|
payloads.push_back(ciphertextHex);
|
||||||
|
} else {
|
||||||
|
const std::size_t chunkHex = (kHushChatMemoByteLimit - kHushChatPartIndexHexLen) & ~std::size_t(1);
|
||||||
|
const std::size_t n = (ciphertextHex.size() + chunkHex - 1) / chunkHex;
|
||||||
|
if (static_cast<int>(n) > kHushChatMaxMessageParts) return ChatComposeStatus::TooLong;
|
||||||
|
parts = static_cast<int>(n);
|
||||||
|
payloads.reserve(n);
|
||||||
|
for (std::size_t i = 0; i < n; ++i) {
|
||||||
|
payloads.push_back(partIndexPrefix(i) + ciphertextHex.substr(i * chunkHex, chunkHex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
OutgoingChatMemos memos;
|
OutgoingChatMemos memos;
|
||||||
memos.recipientZaddr = peerZaddr;
|
memos.recipientZaddr = peerZaddr;
|
||||||
memos.headerMemo = buildHeaderMemo(myReplyZaddr, conversationId, "Memo", streamHeaderHex, myPublicKeyHex,
|
memos.headerMemo = buildHeaderMemo(myReplyZaddr, conversationId, "Memo", streamHeaderHex, myPublicKeyHex,
|
||||||
static_cast<std::int64_t>(std::time(nullptr)));
|
static_cast<std::int64_t>(std::time(nullptr)), "", parts);
|
||||||
memos.payloadMemo = ciphertextHex;
|
memos.payloadMemos = std::move(payloads);
|
||||||
if (memos.headerMemo.size() > kHushChatMemoByteLimit ||
|
if (memos.headerMemo.size() > kHushChatMemoByteLimit) return ChatComposeStatus::TooLong;
|
||||||
memos.payloadMemo.size() > kHushChatMemoByteLimit) {
|
for (const auto& p : memos.payloadMemos)
|
||||||
return ChatComposeStatus::TooLong;
|
if (p.size() > kHushChatMemoByteLimit) return ChatComposeStatus::TooLong;
|
||||||
}
|
|
||||||
out = std::move(memos);
|
out = std::move(memos);
|
||||||
return ChatComposeStatus::Ok;
|
return ChatComposeStatus::Ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ChatComposeStatus splitOutgoingMessage(const ChatKeyPair& mine,
|
||||||
|
const std::string& myPublicKeyHex,
|
||||||
|
const std::string& myReplyZaddr,
|
||||||
|
const std::string& peerPublicKeyHex,
|
||||||
|
const std::string& peerZaddr,
|
||||||
|
const std::string& conversationId,
|
||||||
|
const std::string& plaintext,
|
||||||
|
std::vector<OutgoingChatMemos>& out)
|
||||||
|
{
|
||||||
|
if (plaintext.empty()) return ChatComposeStatus::EmptyBody;
|
||||||
|
if (!present(myPublicKeyHex) || !present(myReplyZaddr) || !present(peerZaddr) ||
|
||||||
|
!present(conversationId)) {
|
||||||
|
return ChatComposeStatus::MissingField;
|
||||||
|
}
|
||||||
|
if (peerPublicKeyHex.size() != kHushChatPublicKeyHexLength) return ChatComposeStatus::BadPeerKey;
|
||||||
|
|
||||||
|
// Encrypt the whole message ONCE (one secretstream session). Chunking is transport-only, so every
|
||||||
|
// transaction of a tier-3 message carries the same secretstream header "e" and its slice of the one
|
||||||
|
// ciphertext — the crypto is byte-identical to the single-tx path.
|
||||||
|
std::string streamHeaderHex;
|
||||||
|
std::string ciphertextHex;
|
||||||
|
if (encryptOutgoing(mine, peerPublicKeyHex, plaintext, streamHeaderHex, ciphertextHex)
|
||||||
|
!= ChatCryptoStatus::Ok) {
|
||||||
|
return ChatComposeStatus::EncryptFailed;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::int64_t now = static_cast<std::int64_t>(std::time(nullptr));
|
||||||
|
|
||||||
|
// 1 chunk if the ciphertext fits one memo, else ceil(hex / chunkHex) chunks (chunkHex = the 512-byte
|
||||||
|
// memo limit minus the 2-hex global index prefix, rounded to an even hex length).
|
||||||
|
const std::size_t chunkHex = (kHushChatMemoByteLimit - kHushChatPartIndexHexLen) & ~std::size_t(1);
|
||||||
|
const std::size_t nChunks = (ciphertextHex.size() <= kHushChatMemoByteLimit)
|
||||||
|
? 1
|
||||||
|
: (ciphertextHex.size() + chunkHex - 1) / chunkHex;
|
||||||
|
if (static_cast<int>(nChunks) > kHushChatMaxMessageChunks) return ChatComposeStatus::TooLong;
|
||||||
|
|
||||||
|
std::vector<OutgoingChatMemos> txs;
|
||||||
|
|
||||||
|
if (nChunks <= 1) {
|
||||||
|
// Tier 1: one classic payload — no index prefix, no "n", no "mid" (byte-identical on the wire).
|
||||||
|
OutgoingChatMemos m;
|
||||||
|
m.recipientZaddr = peerZaddr;
|
||||||
|
m.headerMemo = buildHeaderMemo(myReplyZaddr, conversationId, "Memo", streamHeaderHex,
|
||||||
|
myPublicKeyHex, now, "", 1, "");
|
||||||
|
m.payloadMemos = { ciphertextHex };
|
||||||
|
txs.push_back(std::move(m));
|
||||||
|
} else {
|
||||||
|
// Tier 2/3: index each chunk with its GLOBAL index and pack up to kHushChatMaxMessageParts chunks
|
||||||
|
// per transaction. One tx (≤ the per-tx cap) is tier 2 (no "mid"); several is tier 3 (shared "mid",
|
||||||
|
// same "n" = total chunk count in every sibling header).
|
||||||
|
const bool multiTx = static_cast<int>(nChunks) > kHushChatMaxMessageParts;
|
||||||
|
const std::string mid = multiTx ? newMessageId() : std::string();
|
||||||
|
const std::size_t perTx = static_cast<std::size_t>(kHushChatMaxMessageParts);
|
||||||
|
for (std::size_t base = 0; base < nChunks; base += perTx) {
|
||||||
|
OutgoingChatMemos m;
|
||||||
|
m.recipientZaddr = peerZaddr;
|
||||||
|
m.headerMemo = buildHeaderMemo(myReplyZaddr, conversationId, "Memo", streamHeaderHex,
|
||||||
|
myPublicKeyHex, now, "", static_cast<int>(nChunks), mid);
|
||||||
|
const std::size_t end = std::min(nChunks, base + perTx);
|
||||||
|
m.payloadMemos.reserve(end - base);
|
||||||
|
for (std::size_t i = base; i < end; ++i)
|
||||||
|
m.payloadMemos.push_back(partIndexPrefix(i) + ciphertextHex.substr(i * chunkHex, chunkHex));
|
||||||
|
txs.push_back(std::move(m));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& m : txs) {
|
||||||
|
if (m.headerMemo.size() > kHushChatMemoByteLimit) return ChatComposeStatus::TooLong;
|
||||||
|
for (const auto& p : m.payloadMemos)
|
||||||
|
if (p.size() > kHushChatMemoByteLimit) return ChatComposeStatus::TooLong;
|
||||||
|
}
|
||||||
|
out = std::move(txs);
|
||||||
|
return ChatComposeStatus::Ok;
|
||||||
|
}
|
||||||
|
|
||||||
ChatComposeStatus buildOutgoingContactRequest(const std::string& myPublicKeyHex,
|
ChatComposeStatus buildOutgoingContactRequest(const std::string& myPublicKeyHex,
|
||||||
const std::string& myReplyZaddr,
|
const std::string& myReplyZaddr,
|
||||||
const std::string& peerZaddr,
|
const std::string& peerZaddr,
|
||||||
@@ -100,9 +226,38 @@ ChatComposeStatus buildOutgoingContactRequest(const std::string& myPublicKeyHex,
|
|||||||
memos.recipientZaddr = peerZaddr;
|
memos.recipientZaddr = peerZaddr;
|
||||||
memos.headerMemo = buildHeaderMemo(myReplyZaddr, conversationId, "Cont", "", myPublicKeyHex,
|
memos.headerMemo = buildHeaderMemo(myReplyZaddr, conversationId, "Cont", "", myPublicKeyHex,
|
||||||
static_cast<std::int64_t>(std::time(nullptr)));
|
static_cast<std::int64_t>(std::time(nullptr)));
|
||||||
memos.payloadMemo = requestText;
|
memos.payloadMemos = { requestText };
|
||||||
if (memos.headerMemo.size() > kHushChatMemoByteLimit ||
|
if (memos.headerMemo.size() > kHushChatMemoByteLimit ||
|
||||||
memos.payloadMemo.size() > kHushChatMemoByteLimit) {
|
memos.payloadMemos[0].size() > kHushChatMemoByteLimit) {
|
||||||
|
return ChatComposeStatus::TooLong;
|
||||||
|
}
|
||||||
|
out = std::move(memos);
|
||||||
|
return ChatComposeStatus::Ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatComposeStatus buildOutgoingControl(const std::string& myPublicKeyHex,
|
||||||
|
const std::string& selfZaddr,
|
||||||
|
const std::string& conversationId,
|
||||||
|
const std::string& op,
|
||||||
|
const std::string& label,
|
||||||
|
OutgoingChatMemos& out)
|
||||||
|
{
|
||||||
|
// A control signal is addressed to SELF: only this wallet (any device holding the seed) harvests
|
||||||
|
// it, and it never reaches the peer. op carries the action; the label is a plaintext hint (peer
|
||||||
|
// address / display name) so a fresh import can show the conversation it refers to.
|
||||||
|
if (label.empty()) return ChatComposeStatus::EmptyBody;
|
||||||
|
if (label.front() == '{') return ChatComposeStatus::BadRequestText; // would parse as a header
|
||||||
|
if (!present(op) || !present(myPublicKeyHex) || !present(selfZaddr) || !present(conversationId)) {
|
||||||
|
return ChatComposeStatus::MissingField;
|
||||||
|
}
|
||||||
|
|
||||||
|
OutgoingChatMemos memos;
|
||||||
|
memos.recipientZaddr = selfZaddr;
|
||||||
|
memos.headerMemo = buildHeaderMemo(selfZaddr, conversationId, "Ctrl", "", myPublicKeyHex,
|
||||||
|
static_cast<std::int64_t>(std::time(nullptr)), op);
|
||||||
|
memos.payloadMemos = { label };
|
||||||
|
if (memos.headerMemo.size() > kHushChatMemoByteLimit ||
|
||||||
|
memos.payloadMemos[0].size() > kHushChatMemoByteLimit) {
|
||||||
return ChatComposeStatus::TooLong;
|
return ChatComposeStatus::TooLong;
|
||||||
}
|
}
|
||||||
out = std::move(memos);
|
out = std::move(memos);
|
||||||
|
|||||||
@@ -12,13 +12,17 @@
|
|||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace dragonx::chat {
|
namespace dragonx::chat {
|
||||||
|
|
||||||
struct OutgoingChatMemos {
|
struct OutgoingChatMemos {
|
||||||
std::string recipientZaddr; // the peer's z-address (recipient of both memo outputs)
|
std::string recipientZaddr; // the peer's z-address (recipient of every memo output)
|
||||||
std::string headerMemo; // JSON header — MUST occupy the lower memo position on the wire
|
std::string headerMemo; // JSON header
|
||||||
std::string payloadMemo; // ciphertext hex (Message) or plaintext (ContactRequest)
|
// One payload (ciphertext hex for a Message, plaintext for a ContactRequest/Control), OR — for a
|
||||||
|
// multi-part Message (header "n" = N > 1) — N index-prefixed ciphertext chunks (a 2-hex part index
|
||||||
|
// then that chunk's ciphertext hex), reassembled by the receiver regardless of on-chain order.
|
||||||
|
std::vector<std::string> payloadMemos;
|
||||||
};
|
};
|
||||||
|
|
||||||
// One of the two 0-value memo outputs a HushChat send produces (amount is always 0).
|
// One of the two 0-value memo outputs a HushChat send produces (amount is always 0).
|
||||||
@@ -31,7 +35,7 @@ struct ChatSendOutput {
|
|||||||
// `utf8Prefix` prepends the daemon's "utf8:" marker required by full-node z_sendmany (which then
|
// `utf8Prefix` prepends the daemon's "utf8:" marker required by full-node z_sendmany (which then
|
||||||
// UTF-8-encodes the bytes on-chain, byte-identical to SDXLite's Memo::from_str); lite backends take
|
// UTF-8-encodes the bytes on-chain, byte-identical to SDXLite's Memo::from_str); lite backends take
|
||||||
// raw UTF-8, so pass false there.
|
// raw UTF-8, so pass false there.
|
||||||
std::array<ChatSendOutput, 2> chatSendOutputs(const OutgoingChatMemos& memos, bool utf8Prefix);
|
std::vector<ChatSendOutput> chatSendOutputs(const OutgoingChatMemos& memos, bool utf8Prefix);
|
||||||
|
|
||||||
enum class ChatComposeStatus {
|
enum class ChatComposeStatus {
|
||||||
Ok,
|
Ok,
|
||||||
@@ -54,6 +58,21 @@ ChatComposeStatus buildOutgoingMessage(const ChatKeyPair& mine,
|
|||||||
const std::string& plaintext,
|
const std::string& plaintext,
|
||||||
OutgoingChatMemos& out);
|
OutgoingChatMemos& out);
|
||||||
|
|
||||||
|
// Build an ENCRYPTED message, splitting it across as many transactions as its length needs (larger-
|
||||||
|
// messages tier 3). Returns one OutgoingChatMemos per transaction to broadcast (in order): a short
|
||||||
|
// message yields a single element identical to buildOutgoingMessage (tier 1/2); a long one yields
|
||||||
|
// several, each a self-contained chat send carrying a slice of the one ciphertext under a shared
|
||||||
|
// message id. The receiver reassembles across the siblings. Refused as TooLong beyond
|
||||||
|
// kHushChatMaxMessageChunks total chunks. The caller broadcasts each element as its own chat tx.
|
||||||
|
ChatComposeStatus splitOutgoingMessage(const ChatKeyPair& mine,
|
||||||
|
const std::string& myPublicKeyHex,
|
||||||
|
const std::string& myReplyZaddr,
|
||||||
|
const std::string& peerPublicKeyHex,
|
||||||
|
const std::string& peerZaddr,
|
||||||
|
const std::string& conversationId,
|
||||||
|
const std::string& plaintext,
|
||||||
|
std::vector<OutgoingChatMemos>& out);
|
||||||
|
|
||||||
// Build a plaintext contact request — no peer public key needed yet; this is how the peer first
|
// Build a plaintext contact request — no peer public key needed yet; this is how the peer first
|
||||||
// learns your public key + reply address. The payload is the (plaintext) request text.
|
// learns your public key + reply address. The payload is the (plaintext) request text.
|
||||||
ChatComposeStatus buildOutgoingContactRequest(const std::string& myPublicKeyHex,
|
ChatComposeStatus buildOutgoingContactRequest(const std::string& myPublicKeyHex,
|
||||||
@@ -63,4 +82,15 @@ ChatComposeStatus buildOutgoingContactRequest(const std::string& myPublicKeyHex,
|
|||||||
const std::string& requestText,
|
const std::string& requestText,
|
||||||
OutgoingChatMemos& out);
|
OutgoingChatMemos& out);
|
||||||
|
|
||||||
|
// Build a self-addressed control signal (delete / restore / mark-sensitive). It is sent to your OWN
|
||||||
|
// z-address, so only this wallet's seed can harvest it and it never reaches the peer. `op` is the
|
||||||
|
// action ("del"/"res"/...); `label` is a plaintext hint (peer address or name) carried as the payload
|
||||||
|
// so a fresh seed import can name the conversation the signal refers to.
|
||||||
|
ChatComposeStatus buildOutgoingControl(const std::string& myPublicKeyHex,
|
||||||
|
const std::string& selfZaddr,
|
||||||
|
const std::string& conversationId,
|
||||||
|
const std::string& op,
|
||||||
|
const std::string& label,
|
||||||
|
OutgoingChatMemos& out);
|
||||||
|
|
||||||
} // namespace dragonx::chat
|
} // namespace dragonx::chat
|
||||||
|
|||||||
@@ -81,6 +81,8 @@ const char* hushChatHeaderTypeName(HushChatHeaderType type)
|
|||||||
return "Memo";
|
return "Memo";
|
||||||
case HushChatHeaderType::ContactRequest:
|
case HushChatHeaderType::ContactRequest:
|
||||||
return "Cont";
|
return "Cont";
|
||||||
|
case HushChatHeaderType::Control:
|
||||||
|
return "Ctrl";
|
||||||
}
|
}
|
||||||
return "Unknown";
|
return "Unknown";
|
||||||
}
|
}
|
||||||
@@ -138,7 +140,15 @@ HushChatHeaderParseResult parseHushChatHeaderMemo(const std::string& memo)
|
|||||||
header.type = HushChatHeaderType::Message;
|
header.type = HushChatHeaderType::Message;
|
||||||
} else if (type == "Cont") {
|
} else if (type == "Cont") {
|
||||||
header.type = HushChatHeaderType::ContactRequest;
|
header.type = HushChatHeaderType::ContactRequest;
|
||||||
|
} else if (type == "Ctrl") {
|
||||||
|
header.type = HushChatHeaderType::Control;
|
||||||
|
// A control signal carries a non-empty op ("del"/"res"/...); the op VALUE isn't restricted here so
|
||||||
|
// a future op reaches newer clients and is ignored by the reducer on older ones (forward-compat).
|
||||||
|
if (!readRequiredString(object, "op", header.op, error)) return fail(error);
|
||||||
|
if (header.op.empty()) return fail("control op is empty");
|
||||||
} else {
|
} else {
|
||||||
|
// Unknown type (older client seeing a newer memo type): fail here so it is grouped as an invalid
|
||||||
|
// header and ignored, never crashing. Keep this the catch-all.
|
||||||
return fail("unknown HushChat header type");
|
return fail("unknown HushChat header type");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,6 +161,22 @@ HushChatHeaderParseResult parseHushChatHeaderMemo(const std::string& memo)
|
|||||||
!isHexString(header.secretstream_header_hex)) {
|
!isHexString(header.secretstream_header_hex)) {
|
||||||
return fail("message header must include a 24 byte secretstream header encoded as hex");
|
return fail("message header must include a 24 byte secretstream header encoded as hex");
|
||||||
}
|
}
|
||||||
|
// Optional part count ("n"); absent/1 = classic single-payload message. Bound it so a garbage or
|
||||||
|
// future-oversized header can't make the grouper try to collect an unbounded number of payloads.
|
||||||
|
// A single-tx (tier 2) message chunks at most kHushChatMaxMessageParts; a multi-tx (tier 3)
|
||||||
|
// message carries the larger TOTAL chunk count here alongside a "mid", capped at the chunk ceiling.
|
||||||
|
if (auto it = object.find("n"); it != object.end() && it->is_number_integer()) {
|
||||||
|
const int n = it->get<int>();
|
||||||
|
if (n < 1 || n > kHushChatMaxMessageChunks) return fail("message part count out of range");
|
||||||
|
header.parts = n;
|
||||||
|
}
|
||||||
|
// Optional tier-3 message id ("mid"): present only when the message is split across several
|
||||||
|
// transactions, tying this tx's chunks to its siblings. Short hex (the sender stamps 8 bytes).
|
||||||
|
if (auto it = object.find("mid"); it != object.end() && it->is_string()) {
|
||||||
|
header.message_id = it->get<std::string>();
|
||||||
|
if (header.message_id.empty() || header.message_id.size() > 64 || !isHexString(header.message_id))
|
||||||
|
return fail("message id must be short hex");
|
||||||
|
}
|
||||||
} else if (!header.secretstream_header_hex.empty()) {
|
} else if (!header.secretstream_header_hex.empty()) {
|
||||||
return fail("contact request header must not include a secretstream header");
|
return fail("contact request header must not include a secretstream header");
|
||||||
}
|
}
|
||||||
@@ -179,6 +205,106 @@ HushChatMemoGroupingResult groupHushChatMemoOutputs(const std::vector<HushChatMe
|
|||||||
});
|
});
|
||||||
|
|
||||||
HushChatMemoGroupingResult result;
|
HushChatMemoGroupingResult result;
|
||||||
|
|
||||||
|
// --- Larger-messages tier 2: one message split across N payload outputs in a SINGLE tx. ---
|
||||||
|
// A lone Message header with part count ("n") > 1 means its ciphertext was chunked across N payload
|
||||||
|
// outputs, each prefixed with a 2-hex part index so order survives the daemon's output shuffle.
|
||||||
|
// Reassemble here and emit one pair. Only a clean one-header + exactly-N-chunks tx takes this path;
|
||||||
|
// anything else falls through to the classic single-pair logic. An older client lacks this branch,
|
||||||
|
// mis-pairs the first chunk, fails to decrypt, and drops it — graceful degradation.
|
||||||
|
{
|
||||||
|
std::optional<HushChatHeader> multiHeader;
|
||||||
|
std::size_t multiHeaderPos = 0;
|
||||||
|
int validHeaders = 0;
|
||||||
|
bool anyOversize = false;
|
||||||
|
std::vector<const HushChatMemoOutput*> payloads;
|
||||||
|
for (const auto& entry : ordered) {
|
||||||
|
const auto& o = entry.output;
|
||||||
|
if (o.memo.size() > kHushChatMemoByteLimit) { anyOversize = true; break; }
|
||||||
|
if (!o.memo.empty() && o.memo.front() == '{') {
|
||||||
|
auto parsed = parseHushChatHeaderMemo(o.memo);
|
||||||
|
if (parsed.ok) {
|
||||||
|
++validHeaders;
|
||||||
|
if (parsed.header.type == HushChatHeaderType::Message && parsed.header.parts > 1) {
|
||||||
|
multiHeader = std::move(parsed.header);
|
||||||
|
multiHeaderPos = o.position;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (!o.memo.empty()) {
|
||||||
|
payloads.push_back(&o);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!anyOversize && validHeaders == 1 && multiHeader) {
|
||||||
|
const int want = multiHeader->parts;
|
||||||
|
auto hexVal = [](char c) -> int {
|
||||||
|
if (c >= '0' && c <= '9') return c - '0';
|
||||||
|
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
|
||||||
|
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
|
||||||
|
return -1;
|
||||||
|
};
|
||||||
|
// --- Tier 3: this transaction carries only a SLICE of a larger message (message_id set) ---
|
||||||
|
// Surface its chunks with their GLOBAL indices as a partial; the chat service accumulates
|
||||||
|
// slices across the sibling txs that share this message_id and reassembles once all `parts`
|
||||||
|
// chunks are present. One tx holds at most kHushChatMaxMessageParts of them.
|
||||||
|
if (!multiHeader->message_id.empty()) {
|
||||||
|
HushChatMemoPair pair;
|
||||||
|
pair.header_position = multiHeaderPos;
|
||||||
|
pair.message_id = multiHeader->message_id;
|
||||||
|
pair.total_parts = want;
|
||||||
|
bool okp = true;
|
||||||
|
std::size_t lowPos = 0; bool haveLow = false;
|
||||||
|
for (const auto* p : payloads) {
|
||||||
|
const std::string& m = p->memo;
|
||||||
|
if (m.size() < kHushChatPartIndexHexLen || (m.size() % 2) != 0 || !isHexString(m)) { okp = false; break; }
|
||||||
|
const int idx = hexVal(m[0]) * 16 + hexVal(m[1]);
|
||||||
|
if (idx < 0 || idx >= want) { okp = false; break; }
|
||||||
|
pair.partial_chunks.emplace_back(idx, m.substr(kHushChatPartIndexHexLen));
|
||||||
|
if (!haveLow || p->position < lowPos) { lowPos = p->position; haveLow = true; }
|
||||||
|
}
|
||||||
|
if (okp && !pair.partial_chunks.empty()) {
|
||||||
|
pair.header = std::move(*multiHeader);
|
||||||
|
pair.payload_position = haveLow ? lowPos : 0;
|
||||||
|
result.pairs.push_back(std::move(pair));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
result.issues.push_back(HushChatMemoGroupingIssueInfo{
|
||||||
|
HushChatMemoGroupingIssue::MissingPayload, multiHeaderPos,
|
||||||
|
"tier-3 message slice did not parse"});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
std::vector<std::string> chunks(static_cast<std::size_t>(want));
|
||||||
|
std::vector<bool> seen(static_cast<std::size_t>(want), false);
|
||||||
|
bool ok = (static_cast<int>(payloads.size()) == want);
|
||||||
|
std::size_t lowPos = 0; bool haveLow = false;
|
||||||
|
for (const auto* p : payloads) {
|
||||||
|
if (!ok) break;
|
||||||
|
const std::string& m = p->memo;
|
||||||
|
if (m.size() < kHushChatPartIndexHexLen || (m.size() % 2) != 0 || !isHexString(m)) { ok = false; break; }
|
||||||
|
const int idx = hexVal(m[0]) * 16 + hexVal(m[1]);
|
||||||
|
if (idx < 0 || idx >= want || seen[static_cast<std::size_t>(idx)]) { ok = false; break; }
|
||||||
|
seen[static_cast<std::size_t>(idx)] = true;
|
||||||
|
chunks[static_cast<std::size_t>(idx)] = m.substr(kHushChatPartIndexHexLen);
|
||||||
|
if (!haveLow || p->position < lowPos) { lowPos = p->position; haveLow = true; }
|
||||||
|
}
|
||||||
|
if (ok) {
|
||||||
|
std::string full;
|
||||||
|
for (const auto& c : chunks) full += c;
|
||||||
|
if (!full.empty() && (full.size() % 2) == 0) {
|
||||||
|
HushChatMemoPair pair;
|
||||||
|
pair.header = std::move(*multiHeader);
|
||||||
|
pair.header_position = multiHeaderPos;
|
||||||
|
pair.payload_position = haveLow ? lowPos : 0;
|
||||||
|
pair.payload_memo = std::move(full); // the reassembled full ciphertext hex
|
||||||
|
result.pairs.push_back(std::move(pair));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.issues.push_back(HushChatMemoGroupingIssueInfo{
|
||||||
|
HushChatMemoGroupingIssue::MissingPayload, multiHeaderPos,
|
||||||
|
"multi-part message did not reassemble"});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
std::optional<HushChatMemoOutput> pending_header_output;
|
std::optional<HushChatMemoOutput> pending_header_output;
|
||||||
std::optional<HushChatHeader> pending_header;
|
std::optional<HushChatHeader> pending_header;
|
||||||
// A payload memo seen BEFORE its header. The full-node daemon shuffles the two 0-value memo
|
// A payload memo seen BEFORE its header. The full-node daemon shuffles the two 0-value memo
|
||||||
@@ -299,6 +425,7 @@ HushChatTransactionExtractionResult extractHushChatTransactionMetadata(
|
|||||||
HushChatTransactionMetadata metadata;
|
HushChatTransactionMetadata metadata;
|
||||||
metadata.txid = transaction.txid;
|
metadata.txid = transaction.txid;
|
||||||
metadata.type = pair.header.type;
|
metadata.type = pair.header.type;
|
||||||
|
metadata.op = pair.header.op; // Control only; empty for Message/ContactRequest
|
||||||
metadata.conversation_id = pair.header.conversation_id;
|
metadata.conversation_id = pair.header.conversation_id;
|
||||||
metadata.reply_zaddr = pair.header.reply_zaddr;
|
metadata.reply_zaddr = pair.header.reply_zaddr;
|
||||||
metadata.header_position = pair.header_position;
|
metadata.header_position = pair.header_position;
|
||||||
@@ -308,6 +435,9 @@ HushChatTransactionExtractionResult extractHushChatTransactionMetadata(
|
|||||||
metadata.secretstream_header_hex = pair.header.secretstream_header_hex;
|
metadata.secretstream_header_hex = pair.header.secretstream_header_hex;
|
||||||
metadata.payload_memo = pair.payload_memo;
|
metadata.payload_memo = pair.payload_memo;
|
||||||
metadata.sent_at = pair.header.sent_at; // carry the sender's compose time (0 if absent)
|
metadata.sent_at = pair.header.sent_at; // carry the sender's compose time (0 if absent)
|
||||||
|
metadata.message_id = pair.message_id; // tier 3: set when this tx is a slice of a bigger message
|
||||||
|
metadata.total_parts = pair.total_parts; // (payload_memo empty; chunks below)
|
||||||
|
metadata.partial_chunks = pair.partial_chunks;
|
||||||
result.metadata.push_back(std::move(metadata));
|
result.metadata.push_back(std::move(metadata));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#ifndef DRAGONX_ENABLE_CHAT
|
#ifndef DRAGONX_ENABLE_CHAT
|
||||||
@@ -12,7 +14,8 @@ namespace dragonx::chat {
|
|||||||
|
|
||||||
enum class HushChatHeaderType {
|
enum class HushChatHeaderType {
|
||||||
Message,
|
Message,
|
||||||
ContactRequest
|
ContactRequest,
|
||||||
|
Control // a self-addressed control signal (delete / restore / mark sensitive); carries an `op`
|
||||||
};
|
};
|
||||||
|
|
||||||
struct HushChatHeader {
|
struct HushChatHeader {
|
||||||
@@ -21,8 +24,18 @@ struct HushChatHeader {
|
|||||||
std::string reply_zaddr;
|
std::string reply_zaddr;
|
||||||
std::string conversation_id;
|
std::string conversation_id;
|
||||||
HushChatHeaderType type = HushChatHeaderType::Message;
|
HushChatHeaderType type = HushChatHeaderType::Message;
|
||||||
|
std::string op; // Control only ("del" / "res" / "sensitive" / "unsensitive" / future ops); empty otherwise
|
||||||
std::string secretstream_header_hex;
|
std::string secretstream_header_hex;
|
||||||
std::string public_key_hex;
|
std::string public_key_hex;
|
||||||
|
// Message only: how many payload outputs this message's ciphertext is split across (header "n").
|
||||||
|
// 1 = the classic single-payload message (field omitted on the wire). >1 means the payloads are
|
||||||
|
// index-prefixed ciphertext chunks reassembled in order. For tier 2 all N chunks ride ONE tx; for
|
||||||
|
// tier 3 (message_id set) "n" is the TOTAL chunk count spread across several sibling transactions.
|
||||||
|
int parts = 1;
|
||||||
|
// Message only, larger-messages tier 3: when a message needs more chunks than one tx can carry, it
|
||||||
|
// is split across several transactions that all share this random id (header "mid"); each tx's
|
||||||
|
// payloads carry their GLOBAL chunk index (0..parts-1). Empty = a single-tx message (tier 1/2).
|
||||||
|
std::string message_id;
|
||||||
// Optional sender-stamped compose time (header "ts", Unix seconds). 0 = absent (older sender) → the
|
// Optional sender-stamped compose time (header "ts", Unix seconds). 0 = absent (older sender) → the
|
||||||
// receiver falls back to the tx/receive time. Lets both sides show the SAME (send) time.
|
// receiver falls back to the tx/receive time. Lets both sides show the SAME (send) time.
|
||||||
std::int64_t sent_at = 0;
|
std::int64_t sent_at = 0;
|
||||||
@@ -44,6 +57,12 @@ struct HushChatMemoPair {
|
|||||||
std::size_t header_position = 0;
|
std::size_t header_position = 0;
|
||||||
std::size_t payload_position = 0;
|
std::size_t payload_position = 0;
|
||||||
std::string payload_memo;
|
std::string payload_memo;
|
||||||
|
// Larger-messages tier 3: a single transaction that carries only PART of a bigger message (the rest
|
||||||
|
// ride sibling txs sharing message_id). payload_memo is then empty; partial_chunks holds this tx's
|
||||||
|
// (global index, ciphertext-hex chunk) pairs and total_parts is the whole message's chunk count.
|
||||||
|
std::string message_id;
|
||||||
|
int total_parts = 0;
|
||||||
|
std::vector<std::pair<int, std::string>> partial_chunks;
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class HushChatMemoGroupingIssue {
|
enum class HushChatMemoGroupingIssue {
|
||||||
@@ -73,6 +92,7 @@ struct HushChatTransactionInput {
|
|||||||
struct HushChatTransactionMetadata {
|
struct HushChatTransactionMetadata {
|
||||||
std::string txid;
|
std::string txid;
|
||||||
HushChatHeaderType type = HushChatHeaderType::Message;
|
HushChatHeaderType type = HushChatHeaderType::Message;
|
||||||
|
std::string op; // Control only: the control op ("del"/"res"/...); empty otherwise
|
||||||
std::string conversation_id;
|
std::string conversation_id;
|
||||||
std::string reply_zaddr;
|
std::string reply_zaddr;
|
||||||
std::size_t header_position = 0;
|
std::size_t header_position = 0;
|
||||||
@@ -84,6 +104,13 @@ struct HushChatTransactionMetadata {
|
|||||||
std::string secretstream_header_hex; // header "e": secretstream header (hex; empty for ContactRequest)
|
std::string secretstream_header_hex; // header "e": secretstream header (hex; empty for ContactRequest)
|
||||||
std::string payload_memo; // ciphertext hex (Message) or plaintext request text (ContactRequest)
|
std::string payload_memo; // ciphertext hex (Message) or plaintext request text (ContactRequest)
|
||||||
std::int64_t sent_at = 0; // header "ts": sender compose time (Unix s); 0 = absent → use tx time
|
std::int64_t sent_at = 0; // header "ts": sender compose time (Unix s); 0 = absent → use tx time
|
||||||
|
// Larger-messages tier 3 (a multi-transaction Message): message_id ties this tx's slice to its
|
||||||
|
// siblings, total_parts is the whole message's chunk count, and partial_chunks are this tx's
|
||||||
|
// (global index, ciphertext-hex) chunks. Empty message_id = an ordinary single-tx message (payload
|
||||||
|
// in payload_memo). The chat service accumulates partials by message_id and reassembles when whole.
|
||||||
|
std::string message_id;
|
||||||
|
int total_parts = 0;
|
||||||
|
std::vector<std::pair<int, std::string>> partial_chunks;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct HushChatTransactionExtractionResult {
|
struct HushChatTransactionExtractionResult {
|
||||||
@@ -95,6 +122,18 @@ struct HushChatTransactionExtractionResult {
|
|||||||
|
|
||||||
constexpr int kHushChatSupportedVersion = 0;
|
constexpr int kHushChatSupportedVersion = 0;
|
||||||
constexpr std::size_t kHushChatMemoByteLimit = 512;
|
constexpr std::size_t kHushChatMemoByteLimit = 512;
|
||||||
|
// Larger-messages tier 2: the most payload outputs a single transaction may carry (one tx). Keeps the
|
||||||
|
// chat tx well under the shielded max_tx_size after Sietch decoy padding + proving cost. A message
|
||||||
|
// needing more chunks than this spills into sibling transactions (tier 3), up to kHushChatMaxMessageChunks.
|
||||||
|
constexpr int kHushChatMaxMessageParts = 16;
|
||||||
|
// Larger-messages tier 3: the most chunks a single message may span IN TOTAL, across all its sibling
|
||||||
|
// transactions (ceil(chunks / kHushChatMaxMessageParts) txs, each a separate 0-value chat send + fee).
|
||||||
|
// Bounds the on-chain cost / broadcast time of one message; beyond this compose returns TooLong. The
|
||||||
|
// 2-hex global part index (below) tops out at 256, so this must stay ≤ 256.
|
||||||
|
constexpr int kHushChatMaxMessageChunks = 64;
|
||||||
|
// Each multi-part payload chunk is prefixed with a 2-hex-char (1-byte) part index so the receiver can
|
||||||
|
// reorder chunks after the daemon shuffles a tx's outputs. 2 hex covers indices 0..255 (>> the cap).
|
||||||
|
constexpr std::size_t kHushChatPartIndexHexLen = 2;
|
||||||
constexpr std::size_t kHushChatPublicKeyHexLength = 64;
|
constexpr std::size_t kHushChatPublicKeyHexLength = 64;
|
||||||
constexpr std::size_t kHushChatSecretstreamHeaderHexLength = 48;
|
constexpr std::size_t kHushChatSecretstreamHeaderHexLength = 48;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
// DragonX Wallet - HushChat sensitive-conversation local key vault (implementation).
|
||||||
|
|
||||||
|
#include "chat_sensitive_vault.h"
|
||||||
|
|
||||||
|
#include <sodium.h>
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iterator>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#if !defined(_WIN32)
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace dragonx::chat {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr unsigned char kVersion = 0x01;
|
||||||
|
constexpr std::size_t kSaltLen = 16; // crypto_pwhash_SALTBYTES
|
||||||
|
constexpr std::size_t kNonceLen = 24; // crypto_secretbox_NONCEBYTES
|
||||||
|
constexpr std::size_t kKeyLen = 32; // the vault key / crypto_secretbox_KEYBYTES
|
||||||
|
constexpr std::size_t kMacLen = 16; // crypto_secretbox_MACBYTES
|
||||||
|
constexpr std::size_t kCipherLen = kKeyLen + kMacLen; // 48
|
||||||
|
constexpr std::size_t kFileLen = 1 + kSaltLen + kNonceLen + kCipherLen; // 89
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
ChatSensitiveVault::ChatSensitiveVault(std::string vaultPath) : path_(std::move(vaultPath)) {}
|
||||||
|
|
||||||
|
void ChatSensitiveVault::setPath(std::string vaultPath) { path_ = std::move(vaultPath); }
|
||||||
|
|
||||||
|
bool ChatSensitiveVault::deriveKek(const std::string& passphrase, const unsigned char* salt,
|
||||||
|
unsigned char* kek) const {
|
||||||
|
// Fixed MODERATE limits (≈0.7s / 256 MiB) resist brute-forcing a weak passphrase; because they are
|
||||||
|
// fixed, the file carries no KDF parameters and unlock derives with exactly the same cost.
|
||||||
|
return crypto_pwhash(kek, kKeyLen, passphrase.c_str(), passphrase.size(), salt,
|
||||||
|
crypto_pwhash_OPSLIMIT_MODERATE, crypto_pwhash_MEMLIMIT_MODERATE,
|
||||||
|
crypto_pwhash_ALG_ARGON2ID13) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChatSensitiveVault::writeVault(const unsigned char* salt, const unsigned char* nonce,
|
||||||
|
const unsigned char* cipher, std::size_t cipherLen) const {
|
||||||
|
if (path_.empty()) return false;
|
||||||
|
const std::string tmp = path_ + ".tmp";
|
||||||
|
{
|
||||||
|
std::ofstream f(tmp, std::ios::binary | std::ios::trunc);
|
||||||
|
if (!f) return false;
|
||||||
|
f.put(static_cast<char>(kVersion));
|
||||||
|
f.write(reinterpret_cast<const char*>(salt), kSaltLen);
|
||||||
|
f.write(reinterpret_cast<const char*>(nonce), kNonceLen);
|
||||||
|
f.write(reinterpret_cast<const char*>(cipher), static_cast<std::streamsize>(cipherLen));
|
||||||
|
if (!f) { f.close(); std::remove(tmp.c_str()); return false; }
|
||||||
|
}
|
||||||
|
#if !defined(_WIN32)
|
||||||
|
::chmod(tmp.c_str(), 0600); // best-effort: the key is passphrase-wrapped, so this is defense-in-depth
|
||||||
|
#endif
|
||||||
|
if (std::rename(tmp.c_str(), path_.c_str()) != 0) { std::remove(tmp.c_str()); return false; }
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChatSensitiveVault::exists() const {
|
||||||
|
if (path_.empty()) return false;
|
||||||
|
std::ifstream f(path_, std::ios::binary);
|
||||||
|
if (!f) return false;
|
||||||
|
char ver = 0;
|
||||||
|
if (!f.get(ver) || static_cast<unsigned char>(ver) != kVersion) return false;
|
||||||
|
f.seekg(0, std::ios::end);
|
||||||
|
return f.good() && static_cast<std::size_t>(f.tellg()) == kFileLen;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChatSensitiveVault::create(const std::string& passphrase, std::array<unsigned char, 32>& keyOut) {
|
||||||
|
keyOut.fill(0);
|
||||||
|
if (path_.empty() || passphrase.empty()) return false;
|
||||||
|
if (exists()) return false; // never clobber an existing vault
|
||||||
|
if (sodium_init() < 0) return false;
|
||||||
|
|
||||||
|
unsigned char salt[kSaltLen], nonce[kNonceLen], kek[kKeyLen];
|
||||||
|
unsigned char cipher[kCipherLen];
|
||||||
|
randombytes_buf(salt, kSaltLen);
|
||||||
|
randombytes_buf(nonce, kNonceLen);
|
||||||
|
randombytes_buf(keyOut.data(), kKeyLen); // the random vault key
|
||||||
|
|
||||||
|
bool ok = deriveKek(passphrase, salt, kek);
|
||||||
|
if (ok) {
|
||||||
|
crypto_secretbox_easy(cipher, keyOut.data(), kKeyLen, nonce, kek);
|
||||||
|
ok = writeVault(salt, nonce, cipher, kCipherLen);
|
||||||
|
}
|
||||||
|
sodium_memzero(kek, sizeof kek);
|
||||||
|
sodium_memzero(cipher, sizeof cipher);
|
||||||
|
if (!ok) keyOut.fill(0);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChatSensitiveVault::unlock(const std::string& passphrase, std::array<unsigned char, 32>& keyOut) const {
|
||||||
|
keyOut.fill(0);
|
||||||
|
if (path_.empty() || passphrase.empty()) return false;
|
||||||
|
if (sodium_init() < 0) return false;
|
||||||
|
|
||||||
|
std::ifstream f(path_, std::ios::binary);
|
||||||
|
if (!f) return false;
|
||||||
|
std::vector<unsigned char> buf((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
|
||||||
|
if (buf.size() != kFileLen || buf[0] != kVersion) return false;
|
||||||
|
|
||||||
|
const unsigned char* salt = buf.data() + 1;
|
||||||
|
const unsigned char* nonce = salt + kSaltLen;
|
||||||
|
const unsigned char* cipher = nonce + kNonceLen;
|
||||||
|
|
||||||
|
unsigned char kek[kKeyLen];
|
||||||
|
if (!deriveKek(passphrase, salt, kek)) { sodium_memzero(kek, sizeof kek); return false; }
|
||||||
|
int rc = crypto_secretbox_open_easy(keyOut.data(), cipher, kCipherLen, nonce, kek);
|
||||||
|
sodium_memzero(kek, sizeof kek);
|
||||||
|
if (rc != 0) { keyOut.fill(0); return false; } // wrong passphrase or corrupt file
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChatSensitiveVault::changePassphrase(const std::string& oldPassphrase, const std::string& newPassphrase) {
|
||||||
|
if (newPassphrase.empty()) return false;
|
||||||
|
std::array<unsigned char, 32> key{};
|
||||||
|
if (!unlock(oldPassphrase, key)) return false; // wrong old passphrase
|
||||||
|
|
||||||
|
unsigned char salt[kSaltLen], nonce[kNonceLen], kek[kKeyLen], cipher[kCipherLen];
|
||||||
|
randombytes_buf(salt, kSaltLen);
|
||||||
|
randombytes_buf(nonce, kNonceLen);
|
||||||
|
bool ok = deriveKek(newPassphrase, salt, kek);
|
||||||
|
if (ok) {
|
||||||
|
crypto_secretbox_easy(cipher, key.data(), kKeyLen, nonce, kek);
|
||||||
|
ok = writeVault(salt, nonce, cipher, kCipherLen);
|
||||||
|
}
|
||||||
|
sodium_memzero(kek, sizeof kek);
|
||||||
|
sodium_memzero(cipher, sizeof cipher);
|
||||||
|
sodium_memzero(key.data(), key.size());
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChatSensitiveVault::remove() {
|
||||||
|
if (path_.empty()) return true;
|
||||||
|
std::error_code ec;
|
||||||
|
(void)ec;
|
||||||
|
if (std::remove(path_.c_str()) != 0) {
|
||||||
|
// Treat "already absent" as success; anything else is a real failure only if the file is still there.
|
||||||
|
std::ifstream f(path_, std::ios::binary);
|
||||||
|
return !f.good();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace dragonx::chat
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// DragonX Wallet - HushChat "sensitive conversation" local key vault.
|
||||||
|
//
|
||||||
|
// A per-wallet random 32-byte key, wrapped under a USER PASSPHRASE (Argon2id-derived KEK +
|
||||||
|
// XSalsa20-Poly1305), stored in a small local file. It encrypts the at-rest rows of conversations the
|
||||||
|
// user marks "sensitive" in ChatDatabase, INDEPENDENTLY of the seed-derived chat key — so those rows
|
||||||
|
// cannot be read by someone who has the wallet seed but not this passphrase, and a wallet freshly
|
||||||
|
// restored from the seed alone cannot read them at all. Losing the passphrase (or the file) makes the
|
||||||
|
// sensitive messages unrecoverable BY DESIGN. Not thread-safe; drive from the main thread.
|
||||||
|
//
|
||||||
|
// File format (version 1), 89 bytes:
|
||||||
|
// [1] version (0x01)
|
||||||
|
// [16] Argon2id salt
|
||||||
|
// [24] XSalsa20-Poly1305 nonce
|
||||||
|
// [48] ciphertext = 32-byte vault key + 16-byte Poly1305 tag
|
||||||
|
// Argon2id uses MODERATE ops/mem limits (fixed), so the format needs to carry no KDF parameters.
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace dragonx::chat {
|
||||||
|
|
||||||
|
class ChatSensitiveVault {
|
||||||
|
public:
|
||||||
|
ChatSensitiveVault() = default;
|
||||||
|
explicit ChatSensitiveVault(std::string vaultPath);
|
||||||
|
|
||||||
|
void setPath(std::string vaultPath);
|
||||||
|
const std::string& path() const { return path_; }
|
||||||
|
|
||||||
|
// True if a well-formed vault file exists (version byte + exact size). No passphrase / Argon2.
|
||||||
|
bool exists() const;
|
||||||
|
|
||||||
|
// Create a NEW vault: generate a random 32-byte key, wrap it under `passphrase`, write the file, and
|
||||||
|
// return the key in keyOut. Refuses (returns false) if a vault already exists, the passphrase is
|
||||||
|
// empty, or on IO/crypto error. keyOut is zeroed on any failure.
|
||||||
|
bool create(const std::string& passphrase, std::array<unsigned char, 32>& keyOut);
|
||||||
|
|
||||||
|
// Unlock: derive the KEK from `passphrase`, decrypt the stored key into keyOut. Returns false on a
|
||||||
|
// wrong passphrase, a missing/corrupt file, or crypto failure; keyOut is zeroed unless it succeeds.
|
||||||
|
bool unlock(const std::string& passphrase, std::array<unsigned char, 32>& keyOut) const;
|
||||||
|
|
||||||
|
// Re-wrap the existing key under a new passphrase (the key is unchanged). False if old is wrong.
|
||||||
|
bool changePassphrase(const std::string& oldPassphrase, const std::string& newPassphrase);
|
||||||
|
|
||||||
|
// Delete the vault file. Returns true if removed or already absent.
|
||||||
|
bool remove();
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool deriveKek(const std::string& passphrase, const unsigned char* salt, unsigned char* kek) const;
|
||||||
|
bool writeVault(const unsigned char* salt, const unsigned char* nonce,
|
||||||
|
const unsigned char* cipher, std::size_t cipherLen) const;
|
||||||
|
|
||||||
|
std::string path_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace dragonx::chat
|
||||||
+177
-12
@@ -39,15 +39,14 @@ int ChatService::ingest(const std::vector<HushChatTransactionMetadata>& metadata
|
|||||||
|
|
||||||
int added = 0;
|
int added = 0;
|
||||||
std::vector<ChatMessage> fresh; // persisted once after the loop (one transaction)
|
std::vector<ChatMessage> fresh; // persisted once after the loop (one transaction)
|
||||||
for (const auto& meta : metadata) {
|
|
||||||
|
// Build, decrypt (Message) and store one piece of message metadata — a single-transaction message,
|
||||||
|
// or a tier-3 message reassembled from its slices below. Shared so both apply the same guards exactly.
|
||||||
|
auto processMessage = [&](const HushChatTransactionMetadata& meta) {
|
||||||
// Already stored: append would refuse it. Every scan re-sends the whole history, so this skips the
|
// Already stored: append would refuse it. Every scan re-sends the whole history, so this skips the
|
||||||
// decrypt for all but the new messages.
|
// decrypt for all but the new messages. For a reassembled tier-3 message this keys off its stable
|
||||||
if (store_.contains(meta.txid, meta.payload_position)) continue;
|
// canonical txid, deduping it across scans.
|
||||||
// A memo whose sender is our OWN identity is something we sent (only we hold our key). The local
|
if (store_.contains(meta.txid, meta.payload_position)) return;
|
||||||
// echo already records it as outgoing — ingesting it as incoming would duplicate it as a phantom
|
|
||||||
// "from peer" message. (This also collapses same-seed self-chat, where the "peer" wallet shares
|
|
||||||
// our identity, so every message would otherwise loop back.)
|
|
||||||
if (!myPubKey.empty() && meta.sender_public_key_hex == myPubKey) continue;
|
|
||||||
|
|
||||||
ChatMessage message;
|
ChatMessage message;
|
||||||
message.direction = ChatDirection::Incoming;
|
message.direction = ChatDirection::Incoming;
|
||||||
@@ -76,8 +75,13 @@ int ChatService::ingest(const std::vector<HushChatTransactionMetadata>& metadata
|
|||||||
// is dropped outright (old + future messages) until unblocked; a tombstoned (txid,position) was
|
// is dropped outright (old + future messages) until unblocked; a tombstoned (txid,position) was
|
||||||
// deleted with "revive on new message", so only that exact message is skipped — a new message
|
// deleted with "revive on new message", so only that exact message is skipped — a new message
|
||||||
// in the same conversation has a different txid and flows through normally.
|
// in the same conversation has a different txid and flows through normally.
|
||||||
if (blocked_pred_ && blocked_pred_(message.conversation_id)) continue;
|
if (blocked_pred_ && blocked_pred_(message.conversation_id)) return;
|
||||||
if (db_ && db_->isTombstoned(message)) continue;
|
if (conversationHiddenOnChain(message.conversation_id)) return; // hidden by an on-chain delete
|
||||||
|
// Withhold a sensitive conversation's messages while its local vault key is absent (locked vault or
|
||||||
|
// a fresh seed import). Fail-closed: if we can't store it under the vault key we don't surface it.
|
||||||
|
// They re-import and persist under the vault key once it is unlocked and a scan runs.
|
||||||
|
if (isConversationSensitive(message.conversation_id) && !(db_ && db_->hasSensitiveKey())) return;
|
||||||
|
if (db_ && db_->isTombstoned(message)) return;
|
||||||
|
|
||||||
if (meta.type == HushChatHeaderType::ContactRequest) {
|
if (meta.type == HushChatHeaderType::ContactRequest) {
|
||||||
message.kind = ChatMessageKind::ContactRequest;
|
message.kind = ChatMessageKind::ContactRequest;
|
||||||
@@ -88,7 +92,7 @@ int ChatService::ingest(const std::vector<HushChatTransactionMetadata>& metadata
|
|||||||
// different header with this payload gets its own try.
|
// different header with this payload gets its own try.
|
||||||
const std::string failKey = ChatStore::messageKey(meta.txid, meta.payload_position) + "|" +
|
const std::string failKey = ChatStore::messageKey(meta.txid, meta.payload_position) + "|" +
|
||||||
meta.sender_public_key_hex + "|" + meta.secretstream_header_hex;
|
meta.sender_public_key_hex + "|" + meta.secretstream_header_hex;
|
||||||
if (undecryptable_.count(failKey)) continue;
|
if (undecryptable_.count(failKey)) return;
|
||||||
++decrypt_attempts_;
|
++decrypt_attempts_;
|
||||||
std::string plaintext;
|
std::string plaintext;
|
||||||
const ChatCryptoStatus status = decryptIncoming(
|
const ChatCryptoStatus status = decryptIncoming(
|
||||||
@@ -99,7 +103,7 @@ int ChatService::ingest(const std::vector<HushChatTransactionMetadata>& metadata
|
|||||||
if (undecryptable_.size() >= 4096) undecryptable_.clear();
|
if (undecryptable_.size() >= 4096) undecryptable_.clear();
|
||||||
undecryptable_.insert(failKey);
|
undecryptable_.insert(failKey);
|
||||||
}
|
}
|
||||||
continue;
|
return;
|
||||||
}
|
}
|
||||||
message.body = std::move(plaintext);
|
message.body = std::move(plaintext);
|
||||||
}
|
}
|
||||||
@@ -114,8 +118,92 @@ int ChatService::ingest(const std::vector<HushChatTransactionMetadata>& metadata
|
|||||||
// relying on a seen-watermark delta (which block-time vs wall-clock skew can swallow).
|
// relying on a seen-watermark delta (which block-time vs wall-clock skew can swallow).
|
||||||
if (newIncomingCids) newIncomingCids->push_back(message.conversation_id);
|
if (newIncomingCids) newIncomingCids->push_back(message.conversation_id);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const auto& meta : metadata) {
|
||||||
|
// A self-addressed control signal (delete / restore / ...). Reduce it and move on: it is never a
|
||||||
|
// message, and it must be processed even though WE are its sender — unlike ordinary self-sent memos,
|
||||||
|
// which the guard below skips. The label (peer hint) rides the payload.
|
||||||
|
if (meta.type == HushChatHeaderType::Control) {
|
||||||
|
const auto ctlTimeIt = txTimestamps.find(meta.txid);
|
||||||
|
const std::int64_t ctlRef = ctlTimeIt != txTimestamps.end() ? ctlTimeIt->second : fallbackTimestamp;
|
||||||
|
applyControl(meta.conversation_id, meta.op, meta.payload_memo,
|
||||||
|
meta.sent_at > 0 ? meta.sent_at : ctlRef, meta.txid);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
// A memo whose sender is our OWN identity is something we sent (only we hold our key). The local
|
||||||
|
// echo already records it as outgoing — ingesting it as incoming would duplicate it as a phantom
|
||||||
|
// "from peer" message (this also collapses same-seed self-chat). Skips our own tier-3 slices too.
|
||||||
|
if (!myPubKey.empty() && meta.sender_public_key_hex == myPubKey) continue;
|
||||||
|
|
||||||
|
// Larger-messages tier 3: a slice of a multi-transaction message. Accumulate it by message id;
|
||||||
|
// the whole message is reassembled below once every chunk has arrived (across txs and scans).
|
||||||
|
if (!meta.message_id.empty()) {
|
||||||
|
if (meta.total_parts <= 0 || meta.total_parts > kHushChatMaxMessageChunks) continue; // sanity
|
||||||
|
// Bound the buffer against a flood of never-completing partials (re-fed from chain, so a clear
|
||||||
|
// only delays, never loses, a genuine message).
|
||||||
|
if (tier3_pending_.size() > 256 && !tier3_pending_.count(meta.message_id)) tier3_pending_.clear();
|
||||||
|
Tier3Pending& g = tier3_pending_[meta.message_id];
|
||||||
|
if (g.chunks.empty()) { // first slice seen: capture the shared header fields
|
||||||
|
g.total_parts = meta.total_parts;
|
||||||
|
g.secretstream_header_hex = meta.secretstream_header_hex;
|
||||||
|
g.sender_public_key_hex = meta.sender_public_key_hex;
|
||||||
|
g.conversation_id = meta.conversation_id;
|
||||||
|
g.reply_zaddr = meta.reply_zaddr;
|
||||||
|
g.sent_at = meta.sent_at;
|
||||||
|
g.canonical_txid = meta.txid;
|
||||||
|
}
|
||||||
|
if (meta.txid < g.canonical_txid) g.canonical_txid = meta.txid; // deterministic stable key
|
||||||
|
for (const auto& c : meta.partial_chunks)
|
||||||
|
if (c.first >= 0 && c.first < g.total_parts) g.chunks[c.first] = c.second;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
processMessage(meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finalize every tier-3 message whose chunks have all arrived: reassemble the one ciphertext and run
|
||||||
|
// it through the shared message path (decrypt + the block/hide/sensitive guards). Retire the group
|
||||||
|
// afterwards unless it was withheld only because the sensitive vault is still locked.
|
||||||
|
{
|
||||||
|
std::vector<std::string> retire;
|
||||||
|
for (auto& entry : tier3_pending_) {
|
||||||
|
Tier3Pending& g = entry.second;
|
||||||
|
if (g.total_parts <= 0 || static_cast<int>(g.chunks.size()) != g.total_parts) continue;
|
||||||
|
bool complete = true;
|
||||||
|
for (int i = 0; i < g.total_parts; ++i) if (!g.chunks.count(i)) { complete = false; break; }
|
||||||
|
if (!complete) continue;
|
||||||
|
if (store_.contains(g.canonical_txid, 0)) { retire.push_back(entry.first); continue; } // already have it
|
||||||
|
|
||||||
|
HushChatTransactionMetadata synth;
|
||||||
|
synth.type = HushChatHeaderType::Message;
|
||||||
|
synth.txid = g.canonical_txid;
|
||||||
|
synth.payload_position = 0;
|
||||||
|
synth.conversation_id = g.conversation_id;
|
||||||
|
synth.reply_zaddr = g.reply_zaddr;
|
||||||
|
synth.sender_public_key_hex = g.sender_public_key_hex;
|
||||||
|
synth.secretstream_header_hex = g.secretstream_header_hex;
|
||||||
|
synth.sent_at = g.sent_at;
|
||||||
|
std::string full;
|
||||||
|
for (int i = 0; i < g.total_parts; ++i) full += g.chunks[i];
|
||||||
|
synth.payload_memo = std::move(full);
|
||||||
|
processMessage(synth);
|
||||||
|
|
||||||
|
const bool withheldSensitive =
|
||||||
|
isConversationSensitive(g.conversation_id) && !(db_ && db_->hasSensitiveKey());
|
||||||
|
if (!withheldSensitive) retire.push_back(entry.first);
|
||||||
|
}
|
||||||
|
for (const auto& mid : retire) tier3_pending_.erase(mid);
|
||||||
|
}
|
||||||
|
|
||||||
if (db_ && !fresh.empty()) db_->appendBatch(fresh);
|
if (db_ && !fresh.empty()) db_->appendBatch(fresh);
|
||||||
|
// Enforce on-chain deletions against the store view: erase any conversation whose latest control op is
|
||||||
|
// "del". This catches rows appended earlier in this very batch (before their delete signal was seen in
|
||||||
|
// the metadata order) and rows rehydrated from the DB at startup before any control was reduced. The DB
|
||||||
|
// rows are left intact so a later "res" — or a rescan — can restore the thread.
|
||||||
|
for (const auto& entry : control_) {
|
||||||
|
if (entry.second.op == "del") store_.eraseConversation(entry.first);
|
||||||
|
}
|
||||||
return added;
|
return added;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,6 +234,72 @@ bool ChatService::deleteConversation(const std::string& conversationId, bool blo
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ChatService::applyControl(const std::string& cid, const std::string& op, const std::string& label,
|
||||||
|
std::int64_t timestamp, const std::string& txid) {
|
||||||
|
// Route the op to its axis; ignore unknown future ops so they never pollute either reduction.
|
||||||
|
std::unordered_map<std::string, ControlState>* target = nullptr;
|
||||||
|
if (op == "del" || op == "res") target = &control_;
|
||||||
|
else if (op == "sensitive" || op == "unsensitive") target = &sensitive_control_;
|
||||||
|
else return;
|
||||||
|
|
||||||
|
auto it = target->find(cid);
|
||||||
|
if (it != target->end()) {
|
||||||
|
const ControlState& cur = it->second;
|
||||||
|
// A later signal wins; equal timestamps break by txid so the reduction is deterministic and a
|
||||||
|
// replay of an older op can never flip the resolved state.
|
||||||
|
if (timestamp < cur.timestamp || (timestamp == cur.timestamp && txid <= cur.txid)) return;
|
||||||
|
it->second = ControlState{op, label, timestamp, txid};
|
||||||
|
} else {
|
||||||
|
target->emplace(cid, ControlState{op, label, timestamp, txid});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChatService::conversationHiddenOnChain(const std::string& conversationId) const {
|
||||||
|
auto it = control_.find(conversationId);
|
||||||
|
return it != control_.end() && it->second.op == "del";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::pair<std::string, std::string>> ChatService::onChainDeletedConversations() const {
|
||||||
|
std::vector<std::pair<std::string, std::string>> out;
|
||||||
|
for (const auto& entry : control_) {
|
||||||
|
if (entry.second.op == "del") out.emplace_back(entry.first, entry.second.label);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
bool ChatService::conversationSensitiveOnChain(const std::string& conversationId) const {
|
||||||
|
auto it = sensitive_control_.find(conversationId);
|
||||||
|
return it != sensitive_control_.end() && it->second.op == "sensitive";
|
||||||
|
}
|
||||||
|
bool ChatService::isConversationSensitive(const std::string& conversationId) const {
|
||||||
|
if (conversationSensitiveOnChain(conversationId)) return true;
|
||||||
|
return local_sensitive_pred_ && local_sensitive_pred_(conversationId);
|
||||||
|
}
|
||||||
|
std::vector<ChatService::Tier3Progress> ChatService::pendingLargeMessages() const {
|
||||||
|
std::vector<Tier3Progress> out;
|
||||||
|
for (const auto& entry : tier3_pending_) {
|
||||||
|
const Tier3Pending& g = entry.second;
|
||||||
|
const int received = static_cast<int>(g.chunks.size());
|
||||||
|
// Report only the genuinely in-progress ones. A complete group waiting on the sensitive vault key
|
||||||
|
// (received == total but withheld) is done receiving, so it isn't shown as "incoming".
|
||||||
|
if (g.total_parts > 0 && received < g.total_parts)
|
||||||
|
out.push_back(Tier3Progress{ g.conversation_id, received, g.total_parts });
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
void ChatService::setPersistence(ChatDatabase* db) {
|
||||||
|
db_ = db;
|
||||||
|
// The DB picks the per-row key by this predicate: a sensitive conversation's rows are stored under the
|
||||||
|
// local vault key. Combines the on-chain reduction with the local (instant) mark.
|
||||||
|
if (db_) db_->setSensitivePredicate([this](const std::string& cid) { return isConversationSensitive(cid); });
|
||||||
|
}
|
||||||
|
std::vector<std::pair<std::string, std::string>> ChatService::onChainSensitiveConversations() const {
|
||||||
|
std::vector<std::pair<std::string, std::string>> out;
|
||||||
|
for (const auto& entry : sensitive_control_) {
|
||||||
|
if (entry.second.op == "sensitive") out.emplace_back(entry.first, entry.second.label);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
std::string ChatService::identityPublicKeyHex() const {
|
std::string ChatService::identityPublicKeyHex() const {
|
||||||
if (!has_identity_) return {};
|
if (!has_identity_) return {};
|
||||||
return chatIdentityPublicKeyHex(identity_);
|
return chatIdentityPublicKeyHex(identity_);
|
||||||
@@ -162,6 +316,17 @@ ChatComposeStatus ChatService::composeMessage(const std::string& myReplyZaddr,
|
|||||||
peerPublicKeyHex, peerZaddr, conversationId, plaintext, out);
|
peerPublicKeyHex, peerZaddr, conversationId, plaintext, out);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ChatComposeStatus ChatService::composeMessageParts(const std::string& myReplyZaddr,
|
||||||
|
const std::string& peerPublicKeyHex,
|
||||||
|
const std::string& peerZaddr,
|
||||||
|
const std::string& conversationId,
|
||||||
|
const std::string& plaintext,
|
||||||
|
std::vector<OutgoingChatMemos>& out) const {
|
||||||
|
if (!has_identity_) return ChatComposeStatus::MissingField;
|
||||||
|
return splitOutgoingMessage(identity_, chatIdentityPublicKeyHex(identity_), myReplyZaddr,
|
||||||
|
peerPublicKeyHex, peerZaddr, conversationId, plaintext, out);
|
||||||
|
}
|
||||||
|
|
||||||
ChatComposeStatus ChatService::composeContactRequest(const std::string& myReplyZaddr,
|
ChatComposeStatus ChatService::composeContactRequest(const std::string& myReplyZaddr,
|
||||||
const std::string& peerZaddr,
|
const std::string& peerZaddr,
|
||||||
const std::string& conversationId,
|
const std::string& conversationId,
|
||||||
|
|||||||
+81
-1
@@ -15,6 +15,7 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace dragonx::chat {
|
namespace dragonx::chat {
|
||||||
@@ -52,7 +53,8 @@ public:
|
|||||||
|
|
||||||
// Attach a persistent backing store (Phase 2). Not owned. Pass nullptr to detach. New messages
|
// Attach a persistent backing store (Phase 2). Not owned. Pass nullptr to detach. New messages
|
||||||
// from ingest() are written through; loadFromDatabase() rehydrates the in-memory store from it.
|
// from ingest() are written through; loadFromDatabase() rehydrates the in-memory store from it.
|
||||||
void setPersistence(ChatDatabase* db) { db_ = db; }
|
// Also wires the DB's per-conversation sensitivity predicate to isConversationSensitive().
|
||||||
|
void setPersistence(ChatDatabase* db);
|
||||||
// Load previously-persisted messages (already decrypted at ingest, re-encrypted at rest under
|
// Load previously-persisted messages (already decrypted at ingest, re-encrypted at rest under
|
||||||
// the seed-derived key) into the in-memory store. No-op without an unlocked database.
|
// the seed-derived key) into the in-memory store. No-op without an unlocked database.
|
||||||
void loadFromDatabase();
|
void loadFromDatabase();
|
||||||
@@ -70,6 +72,44 @@ public:
|
|||||||
// persisted rows couldn't be removed, so the caller can avoid a store/DB divergence.
|
// persisted rows couldn't be removed, so the caller can avoid a store/DB divergence.
|
||||||
bool deleteConversation(const std::string& conversationId, bool block);
|
bool deleteConversation(const std::string& conversationId, bool block);
|
||||||
|
|
||||||
|
// On-chain control signals (self-addressed Control memos) reduced per conversation: the latest op,
|
||||||
|
// by sender compose time with a txid tiebreak, wins. A conversation whose latest op is "del" is hidden
|
||||||
|
// on-chain — ingest() drops its messages and erases it from the store view, on every device and on a
|
||||||
|
// fresh seed import — until a later "res" restores it. The state is re-derived from chain each session
|
||||||
|
// (the control memos re-harvest on every scan), so it needs no local persistence.
|
||||||
|
bool conversationHiddenOnChain(const std::string& conversationId) const;
|
||||||
|
// Conversations whose latest on-chain control op is "del" (for the restore picker), each paired with
|
||||||
|
// the label carried in its control memo (the peer address / display name at delete time).
|
||||||
|
std::vector<std::pair<std::string, std::string>> onChainDeletedConversations() const;
|
||||||
|
|
||||||
|
// Sensitive axis (independent of hide/restore): a conversation whose latest on-chain control op is
|
||||||
|
// "sensitive" is marked sensitive — its local rows are stored under the local vault key and withheld
|
||||||
|
// on a device without that key (including a fresh seed import), until "unsensitive". Re-derived from
|
||||||
|
// chain each session, like the hide axis.
|
||||||
|
bool conversationSensitiveOnChain(const std::string& conversationId) const;
|
||||||
|
// Conversations whose latest on-chain control op is "sensitive", each paired with its control label.
|
||||||
|
std::vector<std::pair<std::string, std::string>> onChainSensitiveConversations() const;
|
||||||
|
|
||||||
|
// A local "sensitive" predicate (typically Settings::isChatSensitive) giving INSTANT effect the moment
|
||||||
|
// the user marks a conversation, before the on-chain signal is harvested back. OR-combined with the
|
||||||
|
// on-chain reduction by isConversationSensitive().
|
||||||
|
void setLocalSensitivePredicate(std::function<bool(const std::string&)> pred) {
|
||||||
|
local_sensitive_pred_ = std::move(pred);
|
||||||
|
}
|
||||||
|
// True if this conversation is sensitive (locally marked OR on-chain). Its rows are stored under the
|
||||||
|
// local vault key; while that key is absent its messages are withheld from the store on ingest/load.
|
||||||
|
bool isConversationSensitive(const std::string& conversationId) const;
|
||||||
|
|
||||||
|
// Larger-messages tier 3: a message still being received across transactions (some chunks arrived,
|
||||||
|
// not yet all). Lets the UI show an "incoming large message — N of M parts" hint until it completes
|
||||||
|
// and surfaces as an ordinary message. One entry per in-progress message; empty when none.
|
||||||
|
struct Tier3Progress {
|
||||||
|
std::string conversation_id;
|
||||||
|
int received = 0; // chunks gathered so far
|
||||||
|
int total = 0; // chunks the whole message needs
|
||||||
|
};
|
||||||
|
std::vector<Tier3Progress> pendingLargeMessages() const;
|
||||||
|
|
||||||
// --- Outgoing (compose) ---
|
// --- Outgoing (compose) ---
|
||||||
// My chat public key (hex), or "" without an identity — goes in an outgoing header's "p".
|
// My chat public key (hex), or "" without an identity — goes in an outgoing header's "p".
|
||||||
std::string identityPublicKeyHex() const;
|
std::string identityPublicKeyHex() const;
|
||||||
@@ -80,6 +120,15 @@ public:
|
|||||||
const std::string& conversationId,
|
const std::string& conversationId,
|
||||||
const std::string& plaintext,
|
const std::string& plaintext,
|
||||||
OutgoingChatMemos& out) const;
|
OutgoingChatMemos& out) const;
|
||||||
|
// Construct the outgoing memos for an ENCRYPTED message, splitting it across as many transactions as
|
||||||
|
// its length needs (larger-messages tier 3). Returns one OutgoingChatMemos per transaction to send in
|
||||||
|
// order; a short message yields a single element (identical to composeMessage).
|
||||||
|
ChatComposeStatus composeMessageParts(const std::string& myReplyZaddr,
|
||||||
|
const std::string& peerPublicKeyHex,
|
||||||
|
const std::string& peerZaddr,
|
||||||
|
const std::string& conversationId,
|
||||||
|
const std::string& plaintext,
|
||||||
|
std::vector<OutgoingChatMemos>& out) const;
|
||||||
// Construct the outgoing memos for a plaintext contact request (no peer key needed yet).
|
// Construct the outgoing memos for a plaintext contact request (no peer key needed yet).
|
||||||
ChatComposeStatus composeContactRequest(const std::string& myReplyZaddr,
|
ChatComposeStatus composeContactRequest(const std::string& myReplyZaddr,
|
||||||
const std::string& peerZaddr,
|
const std::string& peerZaddr,
|
||||||
@@ -112,6 +161,37 @@ private:
|
|||||||
ChatStore store_;
|
ChatStore store_;
|
||||||
ChatDatabase* db_ = nullptr; // optional; not owned
|
ChatDatabase* db_ = nullptr; // optional; not owned
|
||||||
std::function<bool(const std::string&)> blocked_pred_; // true => cid is blocked (drop its messages)
|
std::function<bool(const std::string&)> blocked_pred_; // true => cid is blocked (drop its messages)
|
||||||
|
std::function<bool(const std::string&)> local_sensitive_pred_; // true => cid locally marked sensitive
|
||||||
|
|
||||||
|
// Reduced on-chain control state, cid -> latest signal. Not persisted (re-derived from chain).
|
||||||
|
struct ControlState {
|
||||||
|
std::string op; // "del" / "res" / future ops
|
||||||
|
std::string label; // the control memo's payload hint (peer address / display name)
|
||||||
|
std::int64_t timestamp = 0;
|
||||||
|
std::string txid;
|
||||||
|
};
|
||||||
|
std::unordered_map<std::string, ControlState> control_; // hide axis: del / res
|
||||||
|
std::unordered_map<std::string, ControlState> sensitive_control_; // sensitive axis: sensitive / unsensitive
|
||||||
|
|
||||||
|
// Larger-messages tier 3: a big message split across several transactions arrives one slice at a
|
||||||
|
// time (siblings share a message id). We accumulate the slices here until every chunk is present,
|
||||||
|
// then reassemble + decrypt + emit the whole message. Not persisted — a rescan re-sends every slice
|
||||||
|
// so the buffer re-derives from chain (like the control reducer); a completed group is retired.
|
||||||
|
struct Tier3Pending {
|
||||||
|
int total_parts = 0;
|
||||||
|
std::string secretstream_header_hex; // "e", shared by every slice
|
||||||
|
std::string sender_public_key_hex; // "p", shared
|
||||||
|
std::string conversation_id;
|
||||||
|
std::string reply_zaddr;
|
||||||
|
std::int64_t sent_at = 0;
|
||||||
|
std::string canonical_txid; // smallest sibling txid — the reassembled message's stable key
|
||||||
|
std::unordered_map<int, std::string> chunks; // global index -> ciphertext-hex chunk
|
||||||
|
};
|
||||||
|
std::unordered_map<std::string, Tier3Pending> tier3_pending_; // message id -> accumulating slices
|
||||||
|
// Monotonic reduce: route the op to its axis (hide vs sensitive), ignore unknown ops (forward-compat),
|
||||||
|
// and within an axis a later (timestamp, txid) wins so a replay of an older op never flips the state.
|
||||||
|
void applyControl(const std::string& cid, const std::string& op, const std::string& label,
|
||||||
|
std::int64_t timestamp, const std::string& txid);
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace dragonx::chat
|
} // namespace dragonx::chat
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include "chat_store.h"
|
#include "chat_store.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <sodium.h>
|
||||||
|
|
||||||
namespace dragonx::chat {
|
namespace dragonx::chat {
|
||||||
|
|
||||||
@@ -112,6 +113,10 @@ std::vector<ChatMessage> ChatStore::eraseConversation(const std::string& convers
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ChatStore::clear() {
|
void ChatStore::clear() {
|
||||||
|
// Zero decrypted plaintext (message bodies + peer address) before dropping them — clear() runs on
|
||||||
|
// wallet lock / switch / teardown, and the freed std::strings would otherwise linger in the heap.
|
||||||
|
auto wipe = [](std::string& s) { if (!s.empty()) sodium_memzero(&s[0], s.size()); s.clear(); };
|
||||||
|
for (auto& m : messages_) { wipe(m.body); wipe(m.fail_reason); wipe(m.peer_zaddr); }
|
||||||
messages_.clear();
|
messages_.clear();
|
||||||
seen_.clear();
|
seen_.clear();
|
||||||
txid_index_.clear(); // drop the History-badge txid copies too (revision_ below invalidates the memo)
|
txid_index_.clear(); // drop the History-badge txid copies too (revision_ below invalidates the memo)
|
||||||
|
|||||||
@@ -153,6 +153,7 @@ bool Settings::load(const std::string& path)
|
|||||||
loadScalar(j, "theme", theme_);
|
loadScalar(j, "theme", theme_);
|
||||||
loadScalar(j, "save_ztxs", save_ztxs_);
|
loadScalar(j, "save_ztxs", save_ztxs_);
|
||||||
loadScalar(j, "auto_shield", auto_shield_);
|
loadScalar(j, "auto_shield", auto_shield_);
|
||||||
|
loadScalar(j, "auto_consolidate", auto_consolidate_);
|
||||||
loadScalar(j, "use_tor", use_tor_);
|
loadScalar(j, "use_tor", use_tor_);
|
||||||
loadScalar(j, "allow_custom_fees", allow_custom_fees_);
|
loadScalar(j, "allow_custom_fees", allow_custom_fees_);
|
||||||
loadScalar(j, "default_fee", default_fee_);
|
loadScalar(j, "default_fee", default_fee_);
|
||||||
@@ -180,6 +181,14 @@ bool Settings::load(const std::string& path)
|
|||||||
(c.contains("name") && c["name"].is_string())
|
(c.contains("name") && c["name"].is_string())
|
||||||
? c["name"].get<std::string>() : std::string()});
|
? c["name"].get<std::string>() : std::string()});
|
||||||
}
|
}
|
||||||
|
if (j.contains("sensitive_chat_convs") && j["sensitive_chat_convs"].is_array()) {
|
||||||
|
sensitive_chat_convs_.clear();
|
||||||
|
for (const auto& c : j["sensitive_chat_convs"])
|
||||||
|
if (c.is_object() && c.contains("cid") && c["cid"].is_string())
|
||||||
|
sensitive_chat_convs_.push_back({c["cid"].get<std::string>(),
|
||||||
|
(c.contains("name") && c["name"].is_string())
|
||||||
|
? c["name"].get<std::string>() : std::string()});
|
||||||
|
}
|
||||||
// Chat-tab customization (re-clamped through the setters so hand-edited JSON stays in range).
|
// Chat-tab customization (re-clamped through the setters so hand-edited JSON stays in range).
|
||||||
loadScalar(j, "chat_emoji_color", chat_emoji_color_);
|
loadScalar(j, "chat_emoji_color", chat_emoji_color_);
|
||||||
loadScalar(j, "chat_poll_rate_sec", chat_poll_rate_sec_); setChatPollRateSec(chat_poll_rate_sec_);
|
loadScalar(j, "chat_poll_rate_sec", chat_poll_rate_sec_); setChatPollRateSec(chat_poll_rate_sec_);
|
||||||
@@ -271,6 +280,10 @@ bool Settings::load(const std::string& path)
|
|||||||
}
|
}
|
||||||
loadScalar(j, "wizard_completed", wizard_completed_);
|
loadScalar(j, "wizard_completed", wizard_completed_);
|
||||||
loadScalar(j, "seed_backup_reminded", seed_backup_reminded_);
|
loadScalar(j, "seed_backup_reminded", seed_backup_reminded_);
|
||||||
|
loadScalar(j, "chat_intro_dismissed", chat_intro_dismissed_);
|
||||||
|
loadScalar(j, "lock_failed_attempts", lock_failed_attempts_);
|
||||||
|
loadScalar(j, "lockout_until_epoch", lockout_until_epoch_);
|
||||||
|
loadScalar(j, "lock_state_wallet", lock_state_wallet_);
|
||||||
loadScalar(j, "large_wallet_warned", large_wallet_warned_);
|
loadScalar(j, "large_wallet_warned", large_wallet_warned_);
|
||||||
if (j.contains("empty_wallet_warning_acked") && j["empty_wallet_warning_acked"].is_array()) {
|
if (j.contains("empty_wallet_warning_acked") && j["empty_wallet_warning_acked"].is_array()) {
|
||||||
empty_wallet_warning_acked_.clear();
|
empty_wallet_warning_acked_.clear();
|
||||||
@@ -518,6 +531,7 @@ bool Settings::save(const std::string& path)
|
|||||||
j["theme"] = theme_;
|
j["theme"] = theme_;
|
||||||
j["save_ztxs"] = save_ztxs_;
|
j["save_ztxs"] = save_ztxs_;
|
||||||
j["auto_shield"] = auto_shield_;
|
j["auto_shield"] = auto_shield_;
|
||||||
|
j["auto_consolidate"] = auto_consolidate_;
|
||||||
j["use_tor"] = use_tor_;
|
j["use_tor"] = use_tor_;
|
||||||
j["allow_custom_fees"] = allow_custom_fees_;
|
j["allow_custom_fees"] = allow_custom_fees_;
|
||||||
j["default_fee"] = default_fee_;
|
j["default_fee"] = default_fee_;
|
||||||
@@ -540,6 +554,13 @@ bool Settings::save(const std::string& path)
|
|||||||
o["name"] = b.name;
|
o["name"] = b.name;
|
||||||
j["blocked_chat_convs"].push_back(o);
|
j["blocked_chat_convs"].push_back(o);
|
||||||
}
|
}
|
||||||
|
j["sensitive_chat_convs"] = json::array();
|
||||||
|
for (const auto& s : sensitive_chat_convs_) {
|
||||||
|
json o;
|
||||||
|
o["cid"] = s.cid;
|
||||||
|
o["name"] = s.name;
|
||||||
|
j["sensitive_chat_convs"].push_back(o);
|
||||||
|
}
|
||||||
j["chat_emoji_color"] = chat_emoji_color_;
|
j["chat_emoji_color"] = chat_emoji_color_;
|
||||||
j["chat_poll_rate_sec"] = chat_poll_rate_sec_;
|
j["chat_poll_rate_sec"] = chat_poll_rate_sec_;
|
||||||
j["chat_bubble_style"] = chat_bubble_style_;
|
j["chat_bubble_style"] = chat_bubble_style_;
|
||||||
@@ -596,6 +617,10 @@ bool Settings::save(const std::string& path)
|
|||||||
}
|
}
|
||||||
j["wizard_completed"] = wizard_completed_;
|
j["wizard_completed"] = wizard_completed_;
|
||||||
j["seed_backup_reminded"] = seed_backup_reminded_;
|
j["seed_backup_reminded"] = seed_backup_reminded_;
|
||||||
|
j["chat_intro_dismissed"] = chat_intro_dismissed_;
|
||||||
|
j["lock_failed_attempts"] = lock_failed_attempts_;
|
||||||
|
j["lockout_until_epoch"] = lockout_until_epoch_;
|
||||||
|
j["lock_state_wallet"] = lock_state_wallet_;
|
||||||
j["large_wallet_warned"] = large_wallet_warned_;
|
j["large_wallet_warned"] = large_wallet_warned_;
|
||||||
j["empty_wallet_warning_acked"] = json::array();
|
j["empty_wallet_warning_acked"] = json::array();
|
||||||
for (const auto& w : empty_wallet_warning_acked_)
|
for (const auto& w : empty_wallet_warning_acked_)
|
||||||
|
|||||||
@@ -182,6 +182,26 @@ public:
|
|||||||
}
|
}
|
||||||
const std::vector<BlockedChatConv>& blockedChatConversations() const { return blocked_chat_convs_; }
|
const std::vector<BlockedChatConv>& blockedChatConversations() const { return blocked_chat_convs_; }
|
||||||
|
|
||||||
|
// Sensitive chat conversations (by cid): their local rows are stored under the local chat vault key
|
||||||
|
// (not the seed key) and are withheld without it. Kept locally for INSTANT effect the moment the user
|
||||||
|
// marks one; the mark is also signaled on-chain so other devices / a fresh seed import gate it too.
|
||||||
|
// The last-known peer name labels the sensitive-conversations list.
|
||||||
|
struct SensitiveChatConv { std::string cid; std::string name; };
|
||||||
|
bool isChatSensitive(const std::string& cid) const {
|
||||||
|
for (const auto& s : sensitive_chat_convs_) if (s.cid == cid) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
void setChatSensitive(const std::string& cid, const std::string& name, bool sensitive) {
|
||||||
|
const bool already = isChatSensitive(cid);
|
||||||
|
if (sensitive && !already) sensitive_chat_convs_.push_back({cid, name});
|
||||||
|
else if (!sensitive && already)
|
||||||
|
sensitive_chat_convs_.erase(
|
||||||
|
std::remove_if(sensitive_chat_convs_.begin(), sensitive_chat_convs_.end(),
|
||||||
|
[&](const SensitiveChatConv& s) { return s.cid == cid; }),
|
||||||
|
sensitive_chat_convs_.end());
|
||||||
|
}
|
||||||
|
const std::vector<SensitiveChatConv>& sensitiveChatConversations() const { return sensitive_chat_convs_; }
|
||||||
|
|
||||||
// ── Chat-tab customization (chat settings modal + Settings → Chat & Contacts) ──────
|
// ── Chat-tab customization (chat settings modal + Settings → Chat & Contacts) ──────
|
||||||
bool getChatEmojiColor() const { return chat_emoji_color_; }
|
bool getChatEmojiColor() const { return chat_emoji_color_; }
|
||||||
void setChatEmojiColor(bool v) { chat_emoji_color_ = v; }
|
void setChatEmojiColor(bool v) { chat_emoji_color_ = v; }
|
||||||
@@ -209,6 +229,12 @@ public:
|
|||||||
|
|
||||||
bool getAutoShield() const { return auto_shield_; }
|
bool getAutoShield() const { return auto_shield_; }
|
||||||
void setAutoShield(bool shield) { auto_shield_ = shield; }
|
void setAutoShield(bool shield) { auto_shield_ = shield; }
|
||||||
|
// Automatic background note consolidation (full-node): when on, the wallet quietly merges small notes
|
||||||
|
// in tiny batches while idle (see App::pumpAutoConsolidation). Each address's notes are merged back into
|
||||||
|
// THAT SAME address (z_mergetoaddress [addr] -> addr), so funds never move between addresses. On by
|
||||||
|
// default — a missing key in an older config therefore enables it on upgrade, which is intended.
|
||||||
|
bool getAutoConsolidate() const { return auto_consolidate_; }
|
||||||
|
void setAutoConsolidate(bool v) { auto_consolidate_ = v; }
|
||||||
|
|
||||||
bool getUseTor() const { return use_tor_; }
|
bool getUseTor() const { return use_tor_; }
|
||||||
void setUseTor(bool tor) { use_tor_ = tor; }
|
void setUseTor(bool tor) { use_tor_ = tor; }
|
||||||
@@ -392,6 +418,17 @@ public:
|
|||||||
// Whether the one-time "back up your seed phrase" reminder has already been shown.
|
// Whether the one-time "back up your seed phrase" reminder has already been shown.
|
||||||
bool getSeedBackupReminded() const { return seed_backup_reminded_; }
|
bool getSeedBackupReminded() const { return seed_backup_reminded_; }
|
||||||
void setSeedBackupReminded(bool v) { seed_backup_reminded_ = v; }
|
void setSeedBackupReminded(bool v) { seed_backup_reminded_ = v; }
|
||||||
|
bool getChatIntroDismissed() const { return chat_intro_dismissed_; }
|
||||||
|
void setChatIntroDismissed(bool v) { chat_intro_dismissed_ = v; durable_next_save_ = true; }
|
||||||
|
|
||||||
|
// Unlock lockout state, persisted across restarts so the failed-attempt curve can't be reset by
|
||||||
|
// killing the app. Keyed by the wallet file it applies to; the PIN/passphrase is never stored.
|
||||||
|
int getLockFailedAttempts() const { return lock_failed_attempts_; }
|
||||||
|
void setLockFailedAttempts(int v) { lock_failed_attempts_ = v; durable_next_save_ = true; }
|
||||||
|
long long getLockoutUntilEpoch() const { return lockout_until_epoch_; }
|
||||||
|
void setLockoutUntilEpoch(long long v) { lockout_until_epoch_ = v; durable_next_save_ = true; }
|
||||||
|
std::string getLockStateWallet() const { return lock_state_wallet_; }
|
||||||
|
void setLockStateWallet(const std::string& v) { lock_state_wallet_ = v; durable_next_save_ = true; }
|
||||||
|
|
||||||
// One-time nudge when wallet.dat grows past the bloat threshold (re-armed if it shrinks back).
|
// One-time nudge when wallet.dat grows past the bloat threshold (re-armed if it shrinks back).
|
||||||
bool getLargeWalletWarned() const { return large_wallet_warned_; }
|
bool getLargeWalletWarned() const { return large_wallet_warned_; }
|
||||||
@@ -683,6 +720,7 @@ private:
|
|||||||
std::vector<std::string> muted_chat_cids_; // muted chat conversations by cid (Q10)
|
std::vector<std::string> muted_chat_cids_; // muted chat conversations by cid (Q10)
|
||||||
std::vector<std::string> hidden_chat_cids_; // hidden chat conversations by cid
|
std::vector<std::string> hidden_chat_cids_; // hidden chat conversations by cid
|
||||||
std::vector<BlockedChatConv> blocked_chat_convs_; // blocked chat conversations (cid + last-known name)
|
std::vector<BlockedChatConv> blocked_chat_convs_; // blocked chat conversations (cid + last-known name)
|
||||||
|
std::vector<SensitiveChatConv> sensitive_chat_convs_; // sensitive chat conversations (cid + last-known name)
|
||||||
// Chat-tab customization (chat settings modal + Settings → Chat & Contacts).
|
// Chat-tab customization (chat settings modal + Settings → Chat & Contacts).
|
||||||
bool chat_emoji_color_ = true; // true = color (needs FreeType; falls back to mono if absent), false = monochrome
|
bool chat_emoji_color_ = true; // true = color (needs FreeType; falls back to mono if absent), false = monochrome
|
||||||
float chat_poll_rate_sec_ = 2.5f; // 0-conf chat fast-scan cadence (full node)
|
float chat_poll_rate_sec_ = 2.5f; // 0-conf chat fast-scan cadence (full node)
|
||||||
@@ -695,6 +733,7 @@ private:
|
|||||||
int time_format_ = 0; // global clock: 0 = 24h, 1 = 12h
|
int time_format_ = 0; // global clock: 0 = 24h, 1 = 12h
|
||||||
bool save_ztxs_ = true;
|
bool save_ztxs_ = true;
|
||||||
bool auto_shield_ = true;
|
bool auto_shield_ = true;
|
||||||
|
bool auto_consolidate_ = true;
|
||||||
bool use_tor_ = false;
|
bool use_tor_ = false;
|
||||||
bool allow_custom_fees_ = false;
|
bool allow_custom_fees_ = false;
|
||||||
double default_fee_ = 0.0001;
|
double default_fee_ = 0.0001;
|
||||||
@@ -736,6 +775,10 @@ private:
|
|||||||
std::map<std::string, AddressMeta> address_meta_;
|
std::map<std::string, AddressMeta> address_meta_;
|
||||||
bool wizard_completed_ = false;
|
bool wizard_completed_ = false;
|
||||||
bool seed_backup_reminded_ = false;
|
bool seed_backup_reminded_ = false;
|
||||||
|
bool chat_intro_dismissed_ = false; // one-time chat "how it works" intro card
|
||||||
|
int lock_failed_attempts_ = 0; // persisted unlock-failure count (lockout curve)
|
||||||
|
long long lockout_until_epoch_ = 0; // unix time the current lockout expires (0 = none)
|
||||||
|
std::string lock_state_wallet_; // wallet file the above two apply to
|
||||||
bool large_wallet_warned_ = false;
|
bool large_wallet_warned_ = false;
|
||||||
std::set<std::string> empty_wallet_warning_acked_; // wallet files whose empty-wallet warning was dismissed
|
std::set<std::string> empty_wallet_warning_acked_; // wallet files whose empty-wallet warning was dismissed
|
||||||
bool encryption_pending_ = false;
|
bool encryption_pending_ = false;
|
||||||
|
|||||||
@@ -703,8 +703,8 @@ bool EmbeddedDaemon::start(const std::string& binary_path, int lockWaitPolls)
|
|||||||
// ... not executable or wrong architecture"). Surface THAT via setState — which also
|
// ... not executable or wrong architecture"). Surface THAT via setState — which also
|
||||||
// stores the Error message into last_error_ — instead of clobbering it with a generic
|
// stores the Error message into last_error_ — instead of clobbering it with a generic
|
||||||
// string that would then be all getLastError()/the UI ever sees.
|
// string that would then be all getLastError()/the UI ever sees.
|
||||||
std::string detail = last_error_.empty() ? std::string("Failed to start dragonxd process")
|
std::string detail = getLastError();
|
||||||
: last_error_;
|
if (detail.empty()) detail = "Failed to start dragonxd process";
|
||||||
DEBUG_LOGF("[ERROR] %s\n", detail.c_str());
|
DEBUG_LOGF("[ERROR] %s\n", detail.c_str());
|
||||||
setState(State::Error, detail);
|
setState(State::Error, detail);
|
||||||
return false;
|
return false;
|
||||||
@@ -822,7 +822,7 @@ bool EmbeddedDaemon::startProcess(const std::string& binary_path, const std::vec
|
|||||||
char errBuf[256] = {0};
|
char errBuf[256] = {0};
|
||||||
FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
|
FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
|
||||||
NULL, err, 0, errBuf, sizeof(errBuf), NULL);
|
NULL, err, 0, errBuf, sizeof(errBuf), NULL);
|
||||||
last_error_ = "CreateProcess failed with error " + std::to_string(err) + ": " + errBuf;
|
setLastError("CreateProcess failed with error " + std::to_string(err) + ": " + errBuf);
|
||||||
DEBUG_LOGF("[ERROR] CreateProcess failed: error %lu - %s\nCommand: %s\n", err, errBuf, cmd.c_str());
|
DEBUG_LOGF("[ERROR] CreateProcess failed: error %lu - %s\nCommand: %s\n", err, errBuf, cmd.c_str());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1232,7 +1232,7 @@ bool EmbeddedDaemon::startProcess(const std::string& binary_path, const std::vec
|
|||||||
// Create pipe for stdout/stderr
|
// Create pipe for stdout/stderr
|
||||||
int pipefd[2];
|
int pipefd[2];
|
||||||
if (pipe(pipefd) == -1) {
|
if (pipe(pipefd) == -1) {
|
||||||
last_error_ = "Failed to create pipe: " + std::string(strerror(errno));
|
setLastError("Failed to create pipe: " + std::string(strerror(errno)));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1245,7 +1245,7 @@ bool EmbeddedDaemon::startProcess(const std::string& binary_path, const std::vec
|
|||||||
// macOS, which has no pipe2().
|
// macOS, which has no pipe2().
|
||||||
int execpipe[2];
|
int execpipe[2];
|
||||||
if (pipe(execpipe) == -1) {
|
if (pipe(execpipe) == -1) {
|
||||||
last_error_ = "Failed to create exec-status pipe: " + std::string(strerror(errno));
|
setLastError("Failed to create exec-status pipe: " + std::string(strerror(errno)));
|
||||||
close(pipefd[0]);
|
close(pipefd[0]);
|
||||||
close(pipefd[1]);
|
close(pipefd[1]);
|
||||||
return false;
|
return false;
|
||||||
@@ -1255,7 +1255,7 @@ bool EmbeddedDaemon::startProcess(const std::string& binary_path, const std::vec
|
|||||||
|
|
||||||
pid_t pid = fork();
|
pid_t pid = fork();
|
||||||
if (pid == -1) {
|
if (pid == -1) {
|
||||||
last_error_ = "Fork failed: " + std::string(strerror(errno));
|
setLastError("Fork failed: " + std::string(strerror(errno)));
|
||||||
close(pipefd[0]);
|
close(pipefd[0]);
|
||||||
close(pipefd[1]);
|
close(pipefd[1]);
|
||||||
close(execpipe[0]);
|
close(execpipe[0]);
|
||||||
@@ -1370,8 +1370,8 @@ bool EmbeddedDaemon::startProcess(const std::string& binary_path, const std::vec
|
|||||||
close(pipefd[0]);
|
close(pipefd[0]);
|
||||||
int status;
|
int status;
|
||||||
waitpid(pid, &status, 0);
|
waitpid(pid, &status, 0);
|
||||||
last_error_ = "dragonxd could not be executed: " + std::string(strerror(child_errno)) +
|
setLastError("dragonxd could not be executed: " + std::string(strerror(child_errno)) +
|
||||||
" — not executable or wrong architecture";
|
" — not executable or wrong architecture");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -504,6 +504,8 @@ private:
|
|||||||
std::atomic<bool> external_daemon_detected_{false};
|
std::atomic<bool> external_daemon_detected_{false};
|
||||||
std::string last_error_;
|
std::string last_error_;
|
||||||
mutable std::mutex error_mutex_; // protects last_error_ (written by main + monitor threads)
|
mutable std::mutex error_mutex_; // protects last_error_ (written by main + monitor threads)
|
||||||
|
// Locked writer for last_error_ (startProcess() runs on the caller thread while the monitor may call setState()).
|
||||||
|
void setLastError(std::string message) { std::lock_guard<std::mutex> lk(error_mutex_); last_error_ = std::move(message); }
|
||||||
mutable std::mutex output_mutex_; // protects process_output_
|
mutable std::mutex output_mutex_; // protects process_output_
|
||||||
std::string process_output_;
|
std::string process_output_;
|
||||||
size_t output_total_ = 0; // bytes ever appended (the absolute end of process_output_)
|
size_t output_total_ = 0; // bytes ever appended (the absolute end of process_output_)
|
||||||
@@ -511,14 +513,14 @@ private:
|
|||||||
|
|
||||||
// Process handle
|
// Process handle
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
HANDLE process_handle_ = nullptr;
|
std::atomic<HANDLE> process_handle_{nullptr}; // written by start/stop/monitor threads, read by accessors
|
||||||
HANDLE stdout_read_ = nullptr; // pipe handle (Linux-style pipe, unused when tailing debug.log)
|
HANDLE stdout_read_ = nullptr; // pipe handle (Linux-style pipe, unused when tailing debug.log)
|
||||||
std::string launch_cmd_; // full command line used to launch daemon (for diagnostics)
|
std::string launch_cmd_; // full command line used to launch daemon (for diagnostics)
|
||||||
std::string launch_binary_; // binary path used (for diagnostics)
|
std::string launch_binary_; // binary path used (for diagnostics)
|
||||||
std::string launch_workdir_; // working directory used (for diagnostics)
|
std::string launch_workdir_; // working directory used (for diagnostics)
|
||||||
#else
|
#else
|
||||||
pid_t process_pid_ = 0;
|
std::atomic<pid_t> process_pid_{0}; // written by start/stop paths and the monitor (reap), read by accessors
|
||||||
int stdout_fd_ = -1; // the child's stdout+stderr (crash diagnostics; the log itself is debug.log)
|
std::atomic<int> stdout_fd_{-1}; // the child's stdout+stderr (crash diagnostics; the log itself is debug.log)
|
||||||
void tailDebugLog();
|
void tailDebugLog();
|
||||||
|
|
||||||
// Waits up to timeout_ms for `pid` to exit, draining its output; true once reaped. Only valid
|
// Waits up to timeout_ms for `pid` to exit, draining its output; true once reaped. Only valid
|
||||||
|
|||||||
@@ -42,4 +42,24 @@ inline MigrationResume decideSeedMigrationResume(bool pending,
|
|||||||
// wallet.dat on an unconfirmed sweep. (fund-safety: audit)
|
// wallet.dat on an unconfirmed sweep. (fund-safety: audit)
|
||||||
inline bool sweepGateMayAdoptEmptyWallet(bool sweepSubmitted) { return !sweepSubmitted; }
|
inline bool sweepGateMayAdoptEmptyWallet(bool sweepSubmitted) { return !sweepSubmitted; }
|
||||||
|
|
||||||
|
// Reorg-safe depth before the adopt swap is enabled. Matches the History "confirmed" threshold
|
||||||
|
// (>= 10, data/transaction_history_store_policy.h): a 1-block reorg must not be able to un-mine the
|
||||||
|
// sweep after wallet.dat has been moved aside, which would strand the funds in the .bak.
|
||||||
|
inline constexpr int kSweepAdoptMinConfs = 10;
|
||||||
|
inline bool sweepConfsAreReorgSafe(int confs) { return confs >= kSweepAdoptMinConfs; }
|
||||||
|
|
||||||
|
// Final pre-stop re-verification, run on fresh RPC reads immediately before the daemon is stopped.
|
||||||
|
// needSweepTx : a sweep txid exists, so its confirmations must be reorg-safe (false = genuinely
|
||||||
|
// empty wallet, no sweep ever submitted).
|
||||||
|
// confs : fresh gettransaction confirmations (negative = conflicted).
|
||||||
|
// balanceMinconf0 : fresh z_gettotalbalance with minconf 0 (INCLUDES unconfirmed), < 0 = unknown.
|
||||||
|
// syncing : the node is still catching up, so balance reads are not trustworthy.
|
||||||
|
inline bool adoptReverifyPasses(bool needSweepTx, int confs, double balanceMinconf0,
|
||||||
|
bool syncing, double dust)
|
||||||
|
{
|
||||||
|
if (syncing) return false;
|
||||||
|
if (needSweepTx && !sweepConfsAreReorgSafe(confs)) return false;
|
||||||
|
return balanceMinconf0 >= 0.0 && balanceMinconf0 <= dust;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace dragonx
|
} // namespace dragonx
|
||||||
|
|||||||
+34
-2
@@ -75,6 +75,13 @@ struct TransactionInfo {
|
|||||||
std::string getTypeDisplay() const;
|
std::string getTypeDisplay() const;
|
||||||
bool isConfirmed() const { return confirmations >= 1; }
|
bool isConfirmed() const { return confirmations >= 1; }
|
||||||
bool isMature() const { return confirmations >= 100; }
|
bool isMature() const { return confirmations >= 100; }
|
||||||
|
// gettransaction reports confirmations < 0 for a tx conflicted with the chain (double-spent / reorged out).
|
||||||
|
bool isConflicted() const { return confirmations < 0; }
|
||||||
|
// Unconfirmed for longer than maxAgeSec since the wallet first saw it: most likely dropped from the
|
||||||
|
// mempool or expired. A display heuristic only (timestamp 0 = unknown -> never flagged).
|
||||||
|
bool isStalePending(int64_t now, int64_t maxAgeSec = 3600) const {
|
||||||
|
return confirmations == 0 && timestamp > 0 && now - timestamp > maxAgeSec;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Exact field-wise equality: decides whether a refreshed list really differs. `amount` is compared
|
// Exact field-wise equality: decides whether a refreshed list really differs. `amount` is compared
|
||||||
@@ -86,6 +93,15 @@ inline bool sameTransactionInfo(const TransactionInfo& a, const TransactionInfo&
|
|||||||
a.txid == b.txid && a.type == b.type && a.address == b.address &&
|
a.txid == b.txid && a.type == b.type && a.address == b.address &&
|
||||||
a.from_address == b.from_address && a.memo == b.memo;
|
a.from_address == b.from_address && a.memo == b.memo;
|
||||||
}
|
}
|
||||||
|
// Same row except the confirmation COUNT: the pending (confirmations==0) state must still match because it drives the
|
||||||
|
// History tab's pending-first sort and shield-leg merge. A delta for which every row passes this changes nothing the
|
||||||
|
// tab's merged/sorted display list is built from, so it can patch confirmations in place instead of rebuilding.
|
||||||
|
inline bool sameTransactionLayout(const TransactionInfo& a, const TransactionInfo& b) {
|
||||||
|
return a.timestamp == b.timestamp && (a.confirmations == 0) == (b.confirmations == 0) &&
|
||||||
|
std::memcmp(&a.amount, &b.amount, sizeof(double)) == 0 &&
|
||||||
|
a.txid == b.txid && a.type == b.type && a.address == b.address &&
|
||||||
|
a.from_address == b.from_address && a.memo == b.memo;
|
||||||
|
}
|
||||||
inline bool sameTransactionList(const std::vector<TransactionInfo>& a, const std::vector<TransactionInfo>& b) {
|
inline bool sameTransactionList(const std::vector<TransactionInfo>& a, const std::vector<TransactionInfo>& b) {
|
||||||
if (a.size() != b.size()) return false;
|
if (a.size() != b.size()) return false;
|
||||||
for (std::size_t i = 0; i < a.size(); ++i)
|
for (std::size_t i = 0; i < a.size(); ++i)
|
||||||
@@ -189,6 +205,11 @@ struct SyncInfo {
|
|||||||
bool tip_known = false;
|
bool tip_known = false;
|
||||||
|
|
||||||
bool isSynced() const { return tip_known && !syncing && blocks > 0 && blocks >= headers - 2; }
|
bool isSynced() const { return tip_known && !syncing && blocks > 0 && blocks >= headers - 2; }
|
||||||
|
|
||||||
|
// Send precondition: the wallet is caught up to a KNOWN peer-derived tip. A fresh (re)connect or a
|
||||||
|
// peerless node has tip_known == false, so its local height can't be trusted as the network tip.
|
||||||
|
// The lite backend never populates tip_known, so lite is exempt (it has its own sync gate).
|
||||||
|
bool sendTipReady(bool liteBuild) const { return liteBuild || (tip_known && !syncing && blocks > 0); }
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -254,6 +275,8 @@ struct PoolMiningState {
|
|||||||
int64_t uptime_sec = 0;
|
int64_t uptime_sec = 0;
|
||||||
double pool_diff = 0;
|
double pool_diff = 0;
|
||||||
bool connected = false;
|
bool connected = false;
|
||||||
|
std::time_t running_since = 0; // wall-clock the miner was first seen running this session (0 = not running)
|
||||||
|
std::string last_error; // newest xmrig log line that looks like a connection error ("" = none)
|
||||||
|
|
||||||
// Memory/thread usage (bytes for memory)
|
// Memory/thread usage (bytes for memory)
|
||||||
int64_t memory_used = 0;
|
int64_t memory_used = 0;
|
||||||
@@ -368,12 +391,21 @@ struct WalletState {
|
|||||||
// touchTransactions() afterwards. UI caches (History tab, ...) key on this, NOT on last_tx_update (a
|
// touchTransactions() afterwards. UI caches (History tab, ...) key on this, NOT on last_tx_update (a
|
||||||
// wall-clock refresh stamp that lite never sets) nor App::tx_dirty_generation_ (a refresh-needed flag).
|
// wall-clock refresh stamp that lite never sets) nor App::tx_dirty_generation_ (a refresh-needed flag).
|
||||||
std::uint64_t tx_revision = 1;
|
std::uint64_t tx_revision = 1;
|
||||||
void touchTransactions() { ++tx_revision; }
|
// Bumps with tx_revision EXCEPT for a pure confirmation-count change (see sameTransactionLayout): the History tab's
|
||||||
|
// O(N) merge+sort cache keys on this and just patches confirmations when only tx_revision moved. Every writer that
|
||||||
|
// is not setTransactionsIfChanged goes through touchTransactions()/replaceTransactions() and bumps both (safe default).
|
||||||
|
std::uint64_t tx_structure_revision = 1;
|
||||||
|
void touchTransactions() { ++tx_revision; ++tx_structure_revision; }
|
||||||
void replaceTransactions(std::vector<TransactionInfo>&& next) { transactions = std::move(next); touchTransactions(); }
|
void replaceTransactions(std::vector<TransactionInfo>&& next) { transactions = std::move(next); touchTransactions(); }
|
||||||
// Replace only when the content differs; returns true (and bumps) if it did. Keeps the old list when identical.
|
// Replace only when the content differs; returns true (and bumps) if it did. Keeps the old list when identical.
|
||||||
bool setTransactionsIfChanged(std::vector<TransactionInfo>&& next) {
|
bool setTransactionsIfChanged(std::vector<TransactionInfo>&& next) {
|
||||||
if (sameTransactionList(transactions, next)) return false;
|
if (sameTransactionList(transactions, next)) return false;
|
||||||
replaceTransactions(std::move(next));
|
bool layoutSame = transactions.size() == next.size();
|
||||||
|
for (std::size_t i = 0; layoutSame && i < next.size(); ++i)
|
||||||
|
layoutSame = sameTransactionLayout(transactions[i], next[i]);
|
||||||
|
transactions = std::move(next);
|
||||||
|
++tx_revision;
|
||||||
|
if (!layoutSame) ++tx_structure_revision;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
int64_t last_peer_update = 0;
|
int64_t last_peer_update = 0;
|
||||||
|
|||||||
+7
-16
@@ -460,11 +460,10 @@ bool Connection::createDefaultConfig(const std::string& path)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::ofstream file(path);
|
// Build the content in memory, then write it 0600 from the start (writeFileAtomically with
|
||||||
if (!file.is_open()) {
|
// restrictPermissions) — the old ofstream+chmod left a brief world-readable window during which
|
||||||
DEBUG_LOGF("Failed to create config file: %s\n", path.c_str());
|
// another local user could read the freshly-generated rpcuser/rpcpassword.
|
||||||
return false;
|
std::ostringstream file;
|
||||||
}
|
|
||||||
|
|
||||||
// Get the data directory for exportdir
|
// Get the data directory for exportdir
|
||||||
std::string dataDir = getDefaultDataDir();
|
std::string dataDir = getDefaultDataDir();
|
||||||
@@ -486,17 +485,9 @@ bool Connection::createDefaultConfig(const std::string& path)
|
|||||||
file << "addnode=node1.dragonx.is\n";
|
file << "addnode=node1.dragonx.is\n";
|
||||||
file << "addnode=node5.dragonx.is\n";
|
file << "addnode=node5.dragonx.is\n";
|
||||||
|
|
||||||
file.close();
|
if (!util::Platform::writeFileAtomically(path, file.str(), /*restrictPermissions=*/true)) {
|
||||||
|
DEBUG_LOGF("Failed to create config file: %s\n", path.c_str());
|
||||||
// The file holds the freshly-generated rpcuser/rpcpassword in plaintext. ofstream creates it
|
return false;
|
||||||
// with the process umask (typically world-readable 0644), so restrict it to owner read/write
|
|
||||||
// before another local user can read the credentials.
|
|
||||||
{
|
|
||||||
namespace fs = std::filesystem;
|
|
||||||
std::error_code ec;
|
|
||||||
fs::permissions(path, fs::perms::owner_read | fs::perms::owner_write,
|
|
||||||
fs::perm_options::replace, ec);
|
|
||||||
if (ec) DEBUG_LOGF("Could not restrict config permissions on %s: %s\n", path.c_str(), ec.message().c_str());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DEBUG_LOGF("Created default config file: %s\n", path.c_str());
|
DEBUG_LOGF("Created default config file: %s\n", path.c_str());
|
||||||
|
|||||||
+16
-1
@@ -216,7 +216,8 @@ bool RPCClient::open(const std::string& host, const std::string& port,
|
|||||||
curl_easy_setopt(impl_->curl, CURLOPT_HTTPHEADER, impl_->headers);
|
curl_easy_setopt(impl_->curl, CURLOPT_HTTPHEADER, impl_->headers);
|
||||||
curl_easy_setopt(impl_->curl, CURLOPT_WRITEFUNCTION, WriteCallback);
|
curl_easy_setopt(impl_->curl, CURLOPT_WRITEFUNCTION, WriteCallback);
|
||||||
// Progress callback so requestAbort() can unblock an in-flight curl_easy_perform.
|
// Progress callback so requestAbort() can unblock an in-flight curl_easy_perform.
|
||||||
clearAbort(); // a fresh connection must not start in the aborted state
|
// NOTE: abort is deliberately NOT cleared here: a requestAbort() issued while this connect was queued
|
||||||
|
// must still cut it short. Reconnect callers (App::tryConnect / onConnected) call clearAbort() first.
|
||||||
curl_easy_setopt(impl_->curl, CURLOPT_NOPROGRESS, 0L);
|
curl_easy_setopt(impl_->curl, CURLOPT_NOPROGRESS, 0L);
|
||||||
curl_easy_setopt(impl_->curl, CURLOPT_XFERINFOFUNCTION, xferInfoCallback);
|
curl_easy_setopt(impl_->curl, CURLOPT_XFERINFOFUNCTION, xferInfoCallback);
|
||||||
curl_easy_setopt(impl_->curl, CURLOPT_XFERINFODATA, this);
|
curl_easy_setopt(impl_->curl, CURLOPT_XFERINFODATA, this);
|
||||||
@@ -370,6 +371,20 @@ std::string RPCClient::performCall(const std::string& method, const json& params
|
|||||||
|
|
||||||
json payload = makePayload(method, params);
|
json payload = makePayload(method, params);
|
||||||
std::string body = payload.dump();
|
std::string body = payload.dump();
|
||||||
|
// Request-side secret scrub (responses are already scrubbed): the body + payload tree can carry
|
||||||
|
// walletpassphrase / encryptwallet / *_importkey args. Wipe them when this scope ends (all return
|
||||||
|
// and throw paths) and detach CURLOPT_POSTFIELDS so the reused handle holds no dangling pointer.
|
||||||
|
struct RequestScrubGuard {
|
||||||
|
CURL* curl; std::string& body; json& payload;
|
||||||
|
~RequestScrubGuard() {
|
||||||
|
if (!body.empty()) sodium_memzero(&body[0], body.size());
|
||||||
|
scrubJsonSecrets(payload);
|
||||||
|
if (curl) {
|
||||||
|
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, nullptr);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, 0L);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} requestScrubGuard{impl_->curl, body, payload};
|
||||||
std::string response_data;
|
std::string response_data;
|
||||||
|
|
||||||
// Set POST data
|
// Set POST data
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
#include <curl/curl.h>
|
#include <curl/curl.h>
|
||||||
|
|
||||||
|
#include <cctype>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
@@ -152,6 +153,54 @@ inline RpcFailureKind rpcFailureKindOf(const std::exception& e)
|
|||||||
return RpcFailureKind::Other;
|
return RpcFailureKind::Other;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Did a failed z_sendmany (or any submit call) possibly reach the node and take effect? True only for a
|
||||||
|
// transport failure AFTER the request left us with no HTTP status back (reply timeout, connection cut
|
||||||
|
// mid-reply, our own abort): the daemon may already hold the operation, so the caller must not invite an
|
||||||
|
// immediate resend. Definitive answers/rejections (AppError, QueueFull, Auth, Warmup, Deferred,
|
||||||
|
// NotListening, ConnectTimeout, any HTTP status) mean nothing was queued and stay plain failures.
|
||||||
|
inline bool sendReplyMayBeLost(const std::exception& e)
|
||||||
|
{
|
||||||
|
const auto* t = dynamic_cast<const RpcTransportError*>(&e);
|
||||||
|
return t && t->requestSent && t->httpStatus == 0 && t->curlCode != CURLE_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lite broadcast errors are plain backend text (tonic/gRPC). Ambiguous = a transport-style failure while
|
||||||
|
// talking to the lightwalletd server (the server may have accepted the tx before the reply was lost).
|
||||||
|
// Connect-phase failures and an explicit server rejection ("error_code") are definitive. Heuristic by
|
||||||
|
// design: a false positive only shows a more cautious message, never changes the send.
|
||||||
|
inline bool liteSendErrorMayBeInFlight(const std::string& err)
|
||||||
|
{
|
||||||
|
std::string lc = err;
|
||||||
|
for (char& c : lc) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||||
|
auto has = [&](const char* n) { return lc.find(n) != std::string::npos; };
|
||||||
|
if (has("error_code") || has("error trying to connect") || has("connection refused") ||
|
||||||
|
has("dns error") || has("insufficient") || has("locked"))
|
||||||
|
return false;
|
||||||
|
return has("timed out") || has("timeout") || has("deadline") || has("transport error") ||
|
||||||
|
has("connection reset") || has("broken pipe") || has("h2 protocol error") ||
|
||||||
|
has("unexpected eof") || has("status: unknown") || has("status: cancelled");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tag carried in the failure text of a send callback (bool, string) so the Send tab can show the distinct
|
||||||
|
// "may have been broadcast - check History before resending" outcome without changing the callback type.
|
||||||
|
// The prefix is readable English so callers that just print the text still say something accurate.
|
||||||
|
inline const char* sendMaybeInFlightPrefix()
|
||||||
|
{
|
||||||
|
return "Send status unknown - it may already have been broadcast: ";
|
||||||
|
}
|
||||||
|
inline bool isSendMaybeInFlight(const std::string& s)
|
||||||
|
{
|
||||||
|
return s.rfind(sendMaybeInFlightPrefix(), 0) == 0;
|
||||||
|
}
|
||||||
|
inline std::string markSendMaybeInFlight(const std::string& detail)
|
||||||
|
{
|
||||||
|
return isSendMaybeInFlight(detail) ? detail : std::string(sendMaybeInFlightPrefix()) + detail;
|
||||||
|
}
|
||||||
|
inline std::string stripSendMaybeInFlight(const std::string& s)
|
||||||
|
{
|
||||||
|
return isSendMaybeInFlight(s) ? s.substr(std::string(sendMaybeInFlightPrefix()).size()) : s;
|
||||||
|
}
|
||||||
|
|
||||||
// What a failed walletpassphrase call means. Only WrongPassphrase (-14) may count toward the lockout;
|
// What a failed walletpassphrase call means. Only WrongPassphrase (-14) may count toward the lockout;
|
||||||
// a busy/warming-up node, a wallet that is not encrypted, or anything unknown is never a bad guess.
|
// a busy/warming-up node, a wallet that is not encrypted, or anything unknown is never a bad guess.
|
||||||
enum class UnlockFailure { WrongPassphrase, NodeBusy, NotEncrypted, Other };
|
enum class UnlockFailure { WrongPassphrase, NodeBusy, NotEncrypted, Other };
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
#include "history_scan_policy.h"
|
#include "history_scan_policy.h"
|
||||||
#include "rpc/rpc_busy_gate.h"
|
#include "rpc/rpc_busy_gate.h"
|
||||||
#include "../util/logger.h"
|
#include "../util/logger.h"
|
||||||
|
#include "../util/i18n.h" // util::tr for the price-fetch-failed default
|
||||||
|
#include "../util/amount_format.h" // util::toSats: integer-satoshi accumulation
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
@@ -33,7 +35,20 @@ std::optional<T> readOptional(const json& source, const char* key)
|
|||||||
std::optional<double> readBalanceString(const json& source, const char* key)
|
std::optional<double> readBalanceString(const json& source, const char* key)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
if (source.contains(key)) return std::stod(source[key].get<std::string>());
|
if (source.contains(key)) {
|
||||||
|
const std::string text = source[key].get<std::string>();
|
||||||
|
// Strict: plain decimal only (digits, one '.', optional leading '-'). std::stod would accept
|
||||||
|
// '1.5abc', whitespace, '+', hex, exponents and nan/inf, silently.
|
||||||
|
if (text.empty()) return std::nullopt;
|
||||||
|
for (std::size_t i = 0; i < text.size(); ++i) {
|
||||||
|
const char c = text[i];
|
||||||
|
if (!((c >= '0' && c <= '9') || c == '.' || (c == '-' && i == 0))) return std::nullopt;
|
||||||
|
}
|
||||||
|
char* end = nullptr;
|
||||||
|
const double value = std::strtod(text.c_str(), &end);
|
||||||
|
if (end != text.c_str() + text.size() || !std::isfinite(value)) return std::nullopt;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
} catch (...) {}
|
} catch (...) {}
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
@@ -51,15 +66,17 @@ void applyBalancesFromUnspent(std::vector<AddressInfo>& addresses, const json& u
|
|||||||
// Partition each note/utxo by its per-entry "confirmations": `total` (minconf=0 — DISPLAY, includes
|
// Partition each note/utxo by its per-entry "confirmations": `total` (minconf=0 — DISPLAY, includes
|
||||||
// the user's own pending 0-conf change) vs `spendable` (confirmations>=1 — what z_sendmany, run at
|
// the user's own pending 0-conf change) vs `spendable` (confirmations>=1 — what z_sendmany, run at
|
||||||
// minconf=1, can actually spend). This lets a single z_listunspent(0)/listunspent(0) feed both.
|
// minconf=1, can actually spend). This lets a single z_listunspent(0)/listunspent(0) feed both.
|
||||||
std::map<std::string, double> total;
|
// Accumulated in integer satoshis (not doubles) so many notes can't drift by an ulp per addition.
|
||||||
std::map<std::string, double> spendable;
|
std::map<std::string, std::int64_t> total;
|
||||||
|
std::map<std::string, std::int64_t> spendable;
|
||||||
for (const auto& output : unspent) {
|
for (const auto& output : unspent) {
|
||||||
auto address = readOptional<std::string>(output, "address");
|
auto address = readOptional<std::string>(output, "address");
|
||||||
auto amount = readOptional<double>(output, "amount");
|
auto amount = readOptional<double>(output, "amount");
|
||||||
if (!address || !amount) continue;
|
if (!address || !amount || !std::isfinite(*amount)) continue;
|
||||||
total[*address] += *amount;
|
const std::int64_t sats = util::toSats(*amount);
|
||||||
|
total[*address] += sats;
|
||||||
auto conf = readOptional<int>(output, "confirmations");
|
auto conf = readOptional<int>(output, "confirmations");
|
||||||
if (conf && *conf >= 1) spendable[*address] += *amount;
|
if (conf && *conf >= 1) spendable[*address] += sats;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The address lists are rebuilt fresh (default 0) each refresh, so hard-set both — an address with no
|
// The address lists are rebuilt fresh (default 0) each refresh, so hard-set both — an address with no
|
||||||
@@ -67,8 +84,8 @@ void applyBalancesFromUnspent(std::vector<AddressInfo>& addresses, const json& u
|
|||||||
for (auto& info : addresses) {
|
for (auto& info : addresses) {
|
||||||
auto t = total.find(info.address);
|
auto t = total.find(info.address);
|
||||||
auto s = spendable.find(info.address);
|
auto s = spendable.find(info.address);
|
||||||
info.balance = (t != total.end()) ? t->second : 0.0;
|
info.balance = (t != total.end()) ? static_cast<double>(t->second) / 1e8 : 0.0;
|
||||||
info.spendableBalance = (s != spendable.end()) ? s->second : 0.0;
|
info.spendableBalance = (s != spendable.end()) ? static_cast<double>(s->second) / 1e8 : 0.0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1039,8 +1056,13 @@ NetworkRefreshService::TransactionViewCacheEntry NetworkRefreshService::parseVie
|
|||||||
|
|
||||||
void NetworkRefreshService::appendViewTransactionOutputs(std::vector<TransactionInfo>& transactions,
|
void NetworkRefreshService::appendViewTransactionOutputs(std::vector<TransactionInfo>& transactions,
|
||||||
const std::string& txid,
|
const std::string& txid,
|
||||||
const TransactionViewCacheEntry& entry)
|
const TransactionViewCacheEntry& entry,
|
||||||
|
int currentHeight)
|
||||||
{
|
{
|
||||||
|
// A mined entry's confirmations grow by one per block since it was sampled; never go backwards.
|
||||||
|
int entryConfirmations = entry.confirmations;
|
||||||
|
if (entry.confirmations >= 1 && entry.observed_height >= 0 && currentHeight > entry.observed_height)
|
||||||
|
entryConfirmations += currentHeight - entry.observed_height;
|
||||||
for (const auto& out : entry.outgoing_outputs) {
|
for (const auto& out : entry.outgoing_outputs) {
|
||||||
bool alreadyTracked = false;
|
bool alreadyTracked = false;
|
||||||
for (const auto& existing : transactions) {
|
for (const auto& existing : transactions) {
|
||||||
@@ -1072,7 +1094,7 @@ void NetworkRefreshService::appendViewTransactionOutputs(std::vector<Transaction
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!foundExistingMetadata) {
|
if (!foundExistingMetadata) {
|
||||||
info.confirmations = entry.confirmations;
|
info.confirmations = entryConfirmations;
|
||||||
info.timestamp = entry.timestamp;
|
info.timestamp = entry.timestamp;
|
||||||
}
|
}
|
||||||
transactions.push_back(std::move(info));
|
transactions.push_back(std::move(info));
|
||||||
@@ -1459,7 +1481,7 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectTr
|
|||||||
auto cached = snapshot.viewTxCache.find(txid);
|
auto cached = snapshot.viewTxCache.find(txid);
|
||||||
if (cached != snapshot.viewTxCache.end()) {
|
if (cached != snapshot.viewTxCache.end()) {
|
||||||
if (!trackedSend || !cached->second.outgoing_outputs.empty()) {
|
if (!trackedSend || !cached->second.outgoing_outputs.empty()) {
|
||||||
appendViewTransactionOutputs(result.transactions, txid, cached->second);
|
appendViewTransactionOutputs(result.transactions, txid, cached->second, currentBlockHeight);
|
||||||
// NB: do NOT extract chat metadata from our OWN outgoing memos here — ingest marks
|
// NB: do NOT extract chat metadata from our OWN outgoing memos here — ingest marks
|
||||||
// everything Incoming, so that duplicates every sent message as a phantom "from peer"
|
// everything Incoming, so that duplicates every sent message as a phantom "from peer"
|
||||||
// entry. Genuine incoming comes from the z_listreceivedbyaddress path; our sends are
|
// entry. Genuine incoming comes from the z_listreceivedbyaddress path; our sends are
|
||||||
@@ -1492,6 +1514,7 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectTr
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto entry = parseViewTransactionCacheEntry(viewTransaction);
|
auto entry = parseViewTransactionCacheEntry(viewTransaction);
|
||||||
|
entry.observed_height = currentBlockHeight; // confirmations below are sampled at this tip
|
||||||
const bool storedRow = rowBacklog.count(txid) > 0;
|
const bool storedRow = rowBacklog.count(txid) > 0;
|
||||||
if (storedRow && entry.timestamp == 0) {
|
if (storedRow && entry.timestamp == 0) {
|
||||||
const auto settled = settledStoredMeta.find(txid);
|
const auto settled = settledStoredMeta.find(txid);
|
||||||
@@ -1995,7 +2018,7 @@ void NetworkRefreshService::applyPriceRefreshFailure(WalletState& state,
|
|||||||
{
|
{
|
||||||
state.market.price_loading = false;
|
state.market.price_loading = false;
|
||||||
state.market.price_error = errorMessage.empty()
|
state.market.price_error = errorMessage.empty()
|
||||||
? std::string("Price fetch failed")
|
? std::string(util::tr("market_price_fetch_failed"))
|
||||||
: errorMessage;
|
: errorMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -224,6 +224,7 @@ public:
|
|||||||
std::string from_address;
|
std::string from_address;
|
||||||
std::int64_t timestamp = 0;
|
std::int64_t timestamp = 0;
|
||||||
int confirmations = 0;
|
int confirmations = 0;
|
||||||
|
int observed_height = -1; // chain tip when `confirmations` was read (-1 unknown): lets a cached entry advance
|
||||||
struct Output {
|
struct Output {
|
||||||
std::string address;
|
std::string address;
|
||||||
double value = 0.0;
|
double value = 0.0;
|
||||||
@@ -427,9 +428,12 @@ public:
|
|||||||
const std::set<std::string>& miningAddresses = {},
|
const std::set<std::string>& miningAddresses = {},
|
||||||
std::unordered_map<std::string, std::vector<chat::HushChatMemoOutput>>* chatMemoOutputs = nullptr);
|
std::unordered_map<std::string, std::vector<chat::HushChatMemoOutput>>* chatMemoOutputs = nullptr);
|
||||||
static TransactionViewCacheEntry parseViewTransactionCacheEntry(const nlohmann::json& viewTransaction);
|
static TransactionViewCacheEntry parseViewTransactionCacheEntry(const nlohmann::json& viewTransaction);
|
||||||
|
// currentHeight (>=0): the present chain tip; a cached entry that was already mined then reports
|
||||||
|
// confirmations advanced by the blocks added since it was observed (display only).
|
||||||
static void appendViewTransactionOutputs(std::vector<TransactionInfo>& transactions,
|
static void appendViewTransactionOutputs(std::vector<TransactionInfo>& transactions,
|
||||||
const std::string& txid,
|
const std::string& txid,
|
||||||
const TransactionViewCacheEntry& entry);
|
const TransactionViewCacheEntry& entry,
|
||||||
|
int currentHeight = -1);
|
||||||
// Build "send" rows from a z_listsentbyaddress entry's sends.transparentSends[] — the transparent
|
// Build "send" rows from a z_listsentbyaddress entry's sends.transparentSends[] — the transparent
|
||||||
// (z->t deshield) outgoing outputs that z_viewtransaction does not surface. Deduped by
|
// (z->t deshield) outgoing outputs that z_viewtransaction does not surface. Deduped by
|
||||||
// txid+address+amount against rows already present (e.g. listtransactions-derived t->t sends).
|
// txid+address+amount against rows already present (e.g. listtransactions-derived t->t sends).
|
||||||
|
|||||||
@@ -53,6 +53,14 @@ bool worthConsolidating(const NoteGroup& group, double fee)
|
|||||||
return group.notes >= 2 && group.amount > 2.0 * fee;
|
return group.notes >= 2 && group.amount > 2.0 * fee;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<NoteGroup> autoConsolidateCandidates(const std::vector<NoteGroup>& groups, int minNotes, double fee)
|
||||||
|
{
|
||||||
|
std::vector<NoteGroup> out;
|
||||||
|
for (const auto& g : groups)
|
||||||
|
if (g.notes > minNotes && worthConsolidating(g, fee)) out.push_back(g);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
int estimateBatches(int notes, int limit)
|
int estimateBatches(int notes, int limit)
|
||||||
{
|
{
|
||||||
if (notes < 2) return 0;
|
if (notes < 2) return 0;
|
||||||
@@ -184,6 +192,22 @@ MergeFailure classifyMergeFailure(rpc::RpcFailureKind kind, const std::string& m
|
|||||||
return MergeFailure::Fatal;
|
return MergeFailure::Fatal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool isDeferrableMergeOpError(const std::string& message)
|
||||||
|
{
|
||||||
|
// A batch rejected from the mempool because it clashed with an earlier, still-unconfirmed batch (its
|
||||||
|
// Sapling nullifier/anchor, or an input, already in flight). Re-trying after those confirm succeeds.
|
||||||
|
// Substring match — the daemon prefixes a reject code (e.g. "18: bad-txns-shielded-requirements-not-met").
|
||||||
|
static const char* const kNeedles[] = {
|
||||||
|
"shielded-requirements-not-met", // Sapling nullifier/anchor clash (REJECT_DUPLICATE)
|
||||||
|
"joinsplit-requirements-not-met", // the Sprout analogue
|
||||||
|
"txn-mempool-conflict", // spends an output another mempool tx already spends
|
||||||
|
"bad-txns-inputs-spent", // an input already spent by an unconfirmed sibling
|
||||||
|
};
|
||||||
|
for (const char* n : kNeedles)
|
||||||
|
if (message.find(n) != std::string::npos) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
rpc::RpcFailureKind mergeCallFailureKind(const std::exception& e)
|
rpc::RpcFailureKind mergeCallFailureKind(const std::exception& e)
|
||||||
{
|
{
|
||||||
const rpc::RpcFailureKind kind = rpc::rpcFailureKindOf(e);
|
const rpc::RpcFailureKind kind = rpc::rpcFailureKindOf(e);
|
||||||
@@ -290,7 +314,7 @@ void ConsolidationRun::endAddress(RunAddress& a, AddrOutcome o, const std::strin
|
|||||||
if (a.run.opids.empty() && ++deadStreak_ >= kRunMaxFailStreak) halt(msg);
|
if (a.run.opids.empty() && ++deadStreak_ >= kRunMaxFailStreak) halt(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ConsolidationRun::applyOpState(RunBatch& b, OpState st, const OpReport* rep)
|
void ConsolidationRun::applyOpState(RunBatch& b, OpState st, const OpReport* rep, double now)
|
||||||
{
|
{
|
||||||
b.state = st;
|
b.state = st;
|
||||||
if (b.addrIdx < 0 || b.addrIdx >= static_cast<int>(addrs_.size())) return;
|
if (b.addrIdx < 0 || b.addrIdx >= static_cast<int>(addrs_.size())) return;
|
||||||
@@ -299,10 +323,20 @@ void ConsolidationRun::applyOpState(RunBatch& b, OpState st, const OpReport* rep
|
|||||||
case OpState::Success:
|
case OpState::Success:
|
||||||
if (rep) b.txid = rep->txid;
|
if (rep) b.txid = rep->txid;
|
||||||
++a.opsDone;
|
++a.opsDone;
|
||||||
|
a.defers = 0; // progress made: this address's consecutive-defer budget resets
|
||||||
opFailStreak_ = 0;
|
opFailStreak_ = 0;
|
||||||
break;
|
break;
|
||||||
case OpState::Failed:
|
case OpState::Failed:
|
||||||
if (rep) b.error = rep->error;
|
if (rep) b.error = rep->error;
|
||||||
|
// A transient mempool conflict (clashed with an earlier, still-unconfirmed batch): don't fail the
|
||||||
|
// address — hold it and retry once the earlier batches confirm. Bounded, and it doesn't feed the
|
||||||
|
// halt breaker. The batch itself stays terminal (Failed) so it isn't re-polled; the address stays
|
||||||
|
// not-done and gets a fresh batch after the hold (see next()).
|
||||||
|
if (isDeferrableMergeOpError(b.error) && a.defers < kRunMaxDeferRetries) {
|
||||||
|
++a.defers;
|
||||||
|
a.deferUntil = now + kRunMempoolClearSec;
|
||||||
|
break;
|
||||||
|
}
|
||||||
if (a.failure.empty()) a.failure = b.error;
|
if (a.failure.empty()) a.failure = b.error;
|
||||||
a.done = true;
|
a.done = true;
|
||||||
worsen(a, AddrOutcome::Failed);
|
worsen(a, AddrOutcome::Failed);
|
||||||
@@ -367,6 +401,7 @@ RunStep ConsolidationRun::next(const RunEnv& env, double now)
|
|||||||
if (env.locked || now < unlockHoldUntil_) return wait(RunWait::Locked);
|
if (env.locked || now < unlockHoldUntil_) return wait(RunWait::Locked);
|
||||||
if (env.userSendBusy) return wait(RunWait::UserSend);
|
if (env.userSendBusy) return wait(RunWait::UserSend);
|
||||||
if (now < nextCallAt_) return wait(RunWait::Batch);
|
if (now < nextCallAt_) return wait(RunWait::Batch);
|
||||||
|
if (now < addrs_[cur_].deferUntil) return wait(RunWait::Batch); // held after a mempool conflict; retry once it clears
|
||||||
if (!batches_.empty() && (batches_.back().state == OpState::Unknown || batches_.back().state == OpState::Queued))
|
if (!batches_.empty() && (batches_.back().state == OpState::Unknown || batches_.back().state == OpState::Queued))
|
||||||
return wait(RunWait::Batch); // at most 2 unfinished ops, at most 2 x limit notes locked
|
return wait(RunWait::Batch); // at most 2 unfinished ops, at most 2 x limit notes locked
|
||||||
if (env.scanBusy) return wait(RunWait::Scan); // another GUI wallet scan is in the daemon
|
if (env.scanBusy) return wait(RunWait::Scan); // another GUI wallet scan is in the daemon
|
||||||
@@ -476,7 +511,7 @@ void ConsolidationRun::onPollResult(std::uint64_t gen, bool ok, const std::vecto
|
|||||||
}
|
}
|
||||||
b.misses = 0;
|
b.misses = 0;
|
||||||
const OpState st = opStateFromStatus(it->second->status);
|
const OpState st = opStateFromStatus(it->second->status);
|
||||||
if (st != OpState::Unknown) applyOpState(b, st, it->second);
|
if (st != OpState::Unknown) applyOpState(b, st, it->second, now);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,22 @@ bool worthConsolidating(const NoteGroup& group, double fee);
|
|||||||
// note isn't a call (the daemon refuses to merge one note into its own address).
|
// note isn't a call (the daemon refuses to merge one note into its own address).
|
||||||
int estimateBatches(int notes, int limit);
|
int estimateBatches(int notes, int limit);
|
||||||
|
|
||||||
|
// ── Automatic (background) consolidation ──
|
||||||
|
// Gentle defaults for App::pumpAutoConsolidation: a small batch and only a couple of batches per address
|
||||||
|
// per cycle, spaced well apart, so each cycle is a few small merges that barely touch the node. The manual
|
||||||
|
// dialog's 50-input / 20-batch run stays for when the user wants speed.
|
||||||
|
inline constexpr int kAutoConsolidateLimit = 15; // inputs per merge (vs 50 in a manual run)
|
||||||
|
inline constexpr int kAutoConsolidateMaxBatches = 3; // batches per address per cycle
|
||||||
|
inline constexpr int kAutoConsolidateMinNotes = 25; // only act on addresses above this; dormant below
|
||||||
|
inline constexpr double kAutoConsolidateCooldownSec = 600; // between cycles that did work (~10 min)
|
||||||
|
inline constexpr double kAutoConsolidateIdleRescanSec = 3600; // between scans while nothing needs merging (~1 h)
|
||||||
|
inline constexpr double kAutoConsolidateRetrySec = 1800; // back off longer after a failed/halted cycle
|
||||||
|
|
||||||
|
// Addresses worth consolidating automatically: worthConsolidating(group, fee) AND more than `minNotes`
|
||||||
|
// notes, so a near-tidy address is left alone (and the driver can go dormant). Pure; preserves input order
|
||||||
|
// (groupSpendableNotes already returns most-notes-first).
|
||||||
|
std::vector<NoteGroup> autoConsolidateCandidates(const std::vector<NoteGroup>& groups, int minNotes, double fee);
|
||||||
|
|
||||||
// What one z_mergetoaddress call reported.
|
// What one z_mergetoaddress call reported.
|
||||||
struct MergeCallResult {
|
struct MergeCallResult {
|
||||||
std::string opid;
|
std::string opid;
|
||||||
@@ -97,6 +113,10 @@ enum class MergeFailure : std::uint8_t {
|
|||||||
Fatal, // anything else: this address is done
|
Fatal, // anything else: this address is done
|
||||||
};
|
};
|
||||||
MergeFailure classifyMergeFailure(rpc::RpcFailureKind kind, const std::string& message);
|
MergeFailure classifyMergeFailure(rpc::RpcFailureKind kind, const std::string& message);
|
||||||
|
// True when an async op FAILURE message (not an RPC-call failure) is a transient mempool conflict — a batch
|
||||||
|
// that clashed with an earlier, still-unconfirmed batch (bad-txns-shielded-requirements-not-met and kin).
|
||||||
|
// The run holds the address and retries once the mempool clears instead of failing it. Pure; substring match.
|
||||||
|
bool isDeferrableMergeOpError(const std::string& message);
|
||||||
// The failure kind of a z_mergetoaddress call that threw. Sent, then cut off before any HTTP status
|
// The failure kind of a z_mergetoaddress call that threw. Sent, then cut off before any HTTP status
|
||||||
// (connection reset, an empty or partial reply): the daemon may have queued the merge, exactly as after a
|
// (connection reset, an empty or partial reply): the daemon may have queued the merge, exactly as after a
|
||||||
// reply timeout, so it reads as one. Any other failure keeps its kind.
|
// reply timeout, so it reads as one. Any other failure keeps its kind.
|
||||||
@@ -122,6 +142,10 @@ inline constexpr int kRunMaxTransientRetries = 5;
|
|||||||
inline constexpr int kRunMaxUnlockRefusals = 2;
|
inline constexpr int kRunMaxUnlockRefusals = 2;
|
||||||
inline constexpr int kRunMaxFailStreak = 3;
|
inline constexpr int kRunMaxFailStreak = 3;
|
||||||
inline constexpr int kRunMaxPollMisses = 2;
|
inline constexpr int kRunMaxPollMisses = 2;
|
||||||
|
// An op that failed with a transient mempool conflict (it clashed with an earlier, still-unconfirmed batch)
|
||||||
|
// holds its address for this long so the earlier batches can confirm, then retries — up to this many times.
|
||||||
|
inline constexpr double kRunMempoolClearSec = 120.0;
|
||||||
|
inline constexpr int kRunMaxDeferRetries = 5;
|
||||||
|
|
||||||
struct RunConfig {
|
struct RunConfig {
|
||||||
double fee = 0.0001;
|
double fee = 0.0001;
|
||||||
@@ -153,6 +177,8 @@ struct RunAddress {
|
|||||||
int opsDone = 0;
|
int opsDone = 0;
|
||||||
int transient = 0;
|
int transient = 0;
|
||||||
int unlockRefusals = 0;
|
int unlockRefusals = 0;
|
||||||
|
double deferUntil = 0.0; // held until this time after a mempool-conflict op failure, then retried
|
||||||
|
int defers = 0; // consecutive mempool-conflict defers (reset on a success; capped at kRunMaxDeferRetries)
|
||||||
std::string failure;
|
std::string failure;
|
||||||
};
|
};
|
||||||
struct RunBatch {
|
struct RunBatch {
|
||||||
@@ -210,7 +236,7 @@ private:
|
|||||||
enum class Phase : std::uint8_t { Idle, Running, Finished };
|
enum class Phase : std::uint8_t { Idle, Running, Finished };
|
||||||
static void worsen(RunAddress& a, AddrOutcome o) { if (o > a.outcome) a.outcome = o; }
|
static void worsen(RunAddress& a, AddrOutcome o) { if (o > a.outcome) a.outcome = o; }
|
||||||
void endAddress(RunAddress& a, AddrOutcome o, const std::string& msg);
|
void endAddress(RunAddress& a, AddrOutcome o, const std::string& msg);
|
||||||
void applyOpState(RunBatch& b, OpState st, const OpReport* rep);
|
void applyOpState(RunBatch& b, OpState st, const OpReport* rep, double now = 0.0);
|
||||||
void halt(const std::string& msg) { halted_ = true; if (haltError_.empty()) haltError_ = msg; }
|
void halt(const std::string& msg) { halted_ = true; if (haltError_.empty()) haltError_ = msg; }
|
||||||
|
|
||||||
Phase phase_ = Phase::Idle;
|
Phase phase_ = Phase::Idle;
|
||||||
|
|||||||
@@ -171,6 +171,9 @@ ScanTicket WalletScanGovernor::tryAdmit(ScanStream stream, bool visible, const S
|
|||||||
if (st.waitingSince < 0.0 || now - st.lastAskAt > cfg_.waitLapseSec) st.waitingSince = now; // a new wait
|
if (st.waitingSince < 0.0 || now - st.lastAskAt > cfg_.waitLapseSec) st.waitingSince = now; // a new wait
|
||||||
st.lastAskAt = now;
|
st.lastAskAt = now;
|
||||||
st.askVisible = visible;
|
st.askVisible = visible;
|
||||||
|
// Paused (e.g. a user-started consolidation owns the cs_main budget): keep the stream waiting, admit
|
||||||
|
// nothing. It resumes immediately once unpaused; explicit needs still go through beginExplicit.
|
||||||
|
if (suppressed_[static_cast<std::size_t>(stream)]) return {};
|
||||||
pruneLeases(now);
|
pruneLeases(now);
|
||||||
if (!live_.empty()) return {}; // one wallet scan in the daemon at a time
|
if (!live_.empty()) return {}; // one wallet scan in the daemon at a time
|
||||||
if (bestWaiter(now) != stream) return {};
|
if (bestWaiter(now) != stream) return {};
|
||||||
|
|||||||
@@ -159,6 +159,21 @@ public:
|
|||||||
void invalidate(ScanStream stream); // forget its basis: the next routine scan is needed
|
void invalidate(ScanStream stream); // forget its basis: the next routine scan is needed
|
||||||
void reset(); // a new connection: forget everything; old tickets complete as no-ops
|
void reset(); // a new connection: forget everything; old tickets complete as no-ops
|
||||||
|
|
||||||
|
// Pause/resume a stream's ROUTINE scans (tryAdmit returns empty while paused; the stream keeps waiting
|
||||||
|
// and resumes the moment it is unpaused). Explicit needs (beginExplicit) are NEVER paused.
|
||||||
|
void suppressStream(ScanStream stream, bool paused) { suppressed_[static_cast<std::size_t>(stream)] = paused; }
|
||||||
|
bool streamSuppressed(ScanStream stream) const { return suppressed_[static_cast<std::size_t>(stream)]; }
|
||||||
|
// Convenience: pause/resume the heavy wallet-display scans (balance, addresses, history, recent-tx) as a
|
||||||
|
// group — used to hand a user-started consolidation more of the cs_main budget. Chat scans and Adhoc
|
||||||
|
// (the consolidation's own merge calls) are left running.
|
||||||
|
void suppressRoutineWalletScans(bool paused)
|
||||||
|
{
|
||||||
|
suppressStream(ScanStream::Balance, paused);
|
||||||
|
suppressStream(ScanStream::Addresses, paused);
|
||||||
|
suppressStream(ScanStream::History, paused);
|
||||||
|
suppressStream(ScanStream::RecentTx, paused);
|
||||||
|
}
|
||||||
|
|
||||||
bool inFlight(ScanStream stream) const;
|
bool inFlight(ScanStream stream) const;
|
||||||
bool anyInFlight() const { return !live_.empty(); }
|
bool anyInFlight() const { return !live_.empty(); }
|
||||||
double quietUntil() const { return quietUntil_; }
|
double quietUntil() const { return quietUntil_; }
|
||||||
@@ -208,6 +223,7 @@ private:
|
|||||||
|
|
||||||
Config cfg_;
|
Config cfg_;
|
||||||
std::array<Stream, static_cast<std::size_t>(ScanStream::Count)> streams_{};
|
std::array<Stream, static_cast<std::size_t>(ScanStream::Count)> streams_{};
|
||||||
|
std::array<bool, static_cast<std::size_t>(ScanStream::Count)> suppressed_{}; // routine scans paused per stream
|
||||||
std::vector<Live> live_;
|
std::vector<Live> live_;
|
||||||
double quietUntil_ = 0.0;
|
double quietUntil_ = 0.0;
|
||||||
int tipLag_ = 0;
|
int tipLag_ = 0;
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ bool ExplorerBlockCache::ensureOpen()
|
|||||||
}
|
}
|
||||||
|
|
||||||
db_ = openedDb;
|
db_ = openedDb;
|
||||||
|
++revision_;
|
||||||
// UI-thread-only handle: a short wait is enough; a miss falls back to the in-memory spill instead of
|
// UI-thread-only handle: a short wait is enough; a miss falls back to the in-memory spill instead of
|
||||||
// freezing the frame for 2 s.
|
// freezing the frame for 2 s.
|
||||||
sqlite3_busy_timeout(db_, 250);
|
sqlite3_busy_timeout(db_, 250);
|
||||||
@@ -176,6 +177,20 @@ std::map<int, ExplorerBlockSummary> ExplorerBlockCache::loadRange(int minHeight,
|
|||||||
return blocks;
|
return blocks;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const std::map<int, ExplorerBlockSummary>& ExplorerBlockCache::loadRangeCached(int minHeight, int maxHeight)
|
||||||
|
{
|
||||||
|
if (range_cache_revision_ != revision_) { range_cache_.clear(); range_cache_revision_ = revision_; }
|
||||||
|
if (range_cache_.size() >= 16) range_cache_.clear();
|
||||||
|
const std::pair<int, int> key(minHeight, maxHeight);
|
||||||
|
auto it = range_cache_.find(key);
|
||||||
|
if (it == range_cache_.end()) {
|
||||||
|
auto loaded = loadRange(minHeight, maxHeight); // may open the DB (bumps revision_)
|
||||||
|
if (range_cache_revision_ != revision_) { range_cache_.clear(); range_cache_revision_ = revision_; }
|
||||||
|
it = range_cache_.emplace(key, std::move(loaded)).first;
|
||||||
|
}
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<ExplorerBlockSummary> ExplorerBlockCache::searchBlocks(const std::string& query, int limit)
|
std::vector<ExplorerBlockSummary> ExplorerBlockCache::searchBlocks(const std::string& query, int limit)
|
||||||
{
|
{
|
||||||
std::vector<ExplorerBlockSummary> results;
|
std::vector<ExplorerBlockSummary> results;
|
||||||
@@ -256,6 +271,7 @@ bool ExplorerBlockCache::storeBlockRow(const ExplorerBlockSummary& block)
|
|||||||
bool ExplorerBlockCache::storeBlock(const ExplorerBlockSummary& block)
|
bool ExplorerBlockCache::storeBlock(const ExplorerBlockSummary& block)
|
||||||
{
|
{
|
||||||
if (block.height < 1 || block.hash.empty()) return false;
|
if (block.height < 1 || block.hash.empty()) return false;
|
||||||
|
++revision_;
|
||||||
const bool stored = storeBlockRow(block);
|
const bool stored = storeBlockRow(block);
|
||||||
if (stored) {
|
if (stored) {
|
||||||
spill_.erase(block.height);
|
spill_.erase(block.height);
|
||||||
@@ -297,6 +313,7 @@ int ExplorerBlockCache::cachedBlockCount()
|
|||||||
|
|
||||||
void ExplorerBlockCache::clearBlocks()
|
void ExplorerBlockCache::clearBlocks()
|
||||||
{
|
{
|
||||||
|
++revision_;
|
||||||
spill_.clear();
|
spill_.clear();
|
||||||
if (!ensureOpen()) return;
|
if (!ensureOpen()) return;
|
||||||
exec("DELETE FROM explorer_blocks");
|
exec("DELETE FROM explorer_blocks");
|
||||||
@@ -305,6 +322,7 @@ void ExplorerBlockCache::clearBlocks()
|
|||||||
void ExplorerBlockCache::pruneTo(int maxRows)
|
void ExplorerBlockCache::pruneTo(int maxRows)
|
||||||
{
|
{
|
||||||
if (maxRows < 1 || !db_) return;
|
if (maxRows < 1 || !db_) return;
|
||||||
|
++revision_;
|
||||||
// Keep the maxRows highest blocks. The OFFSET row is the (maxRows+1)-th highest; when the table is smaller the
|
// Keep the maxRows highest blocks. The OFFSET row is the (maxRows+1)-th highest; when the table is smaller the
|
||||||
// subquery is NULL and nothing is deleted. Freed pages are reused, so the file stops growing at its high-water
|
// subquery is NULL and nothing is deleted. Freed pages are reused, so the file stops growing at its high-water
|
||||||
// mark (no VACUUM / auto_vacuum: changing auto_vacuum needs a rebuild of existing databases).
|
// mark (no VACUUM / auto_vacuum: changing auto_vacuum needs a rebuild of existing databases).
|
||||||
@@ -333,8 +351,10 @@ ExplorerBlockCache::SavedTipValidation ExplorerBlockCache::prepareValidation(
|
|||||||
// No DB to validate against: keep the spill honest ourselves (tip regressed, or a different block at the same height).
|
// No DB to validate against: keep the spill honest ourselves (tip regressed, or a different block at the same height).
|
||||||
if (!spill_.empty() && !currentBestHash.empty() &&
|
if (!spill_.empty() && !currentBestHash.empty() &&
|
||||||
(currentHeight < spill_tip_height_ ||
|
(currentHeight < spill_tip_height_ ||
|
||||||
(currentHeight == spill_tip_height_ && currentBestHash != spill_tip_hash_)))
|
(currentHeight == spill_tip_height_ && currentBestHash != spill_tip_hash_))) {
|
||||||
spill_.clear();
|
spill_.clear();
|
||||||
|
++revision_;
|
||||||
|
}
|
||||||
if (!currentBestHash.empty()) { spill_tip_height_ = currentHeight; spill_tip_hash_ = currentBestHash; }
|
if (!currentBestHash.empty()) { spill_tip_height_ = currentHeight; spill_tip_hash_ = currentBestHash; }
|
||||||
return validation;
|
return validation;
|
||||||
}
|
}
|
||||||
@@ -526,6 +546,7 @@ void ExplorerBlockCache::close()
|
|||||||
{
|
{
|
||||||
tip_known_ = false;
|
tip_known_ = false;
|
||||||
if (!db_) return;
|
if (!db_) return;
|
||||||
|
++revision_;
|
||||||
sqlite3_close(db_);
|
sqlite3_close(db_);
|
||||||
db_ = nullptr;
|
db_ = nullptr;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
struct sqlite3;
|
struct sqlite3;
|
||||||
@@ -44,6 +45,11 @@ public:
|
|||||||
const std::string& databasePath() const { return database_path_; }
|
const std::string& databasePath() const { return database_path_; }
|
||||||
|
|
||||||
std::map<int, ExplorerBlockSummary> loadRange(int minHeight, int maxHeight);
|
std::map<int, ExplorerBlockSummary> loadRange(int minHeight, int maxHeight);
|
||||||
|
// loadRange memoized per (min,max) and invalidated by revision() so the per-frame Explorer list only hits SQLite
|
||||||
|
// after the cache actually changed (store / clear / prune / spill reset / open / close). The reference is valid
|
||||||
|
// until the next call; copy it if you call again before using it.
|
||||||
|
const std::map<int, ExplorerBlockSummary>& loadRangeCached(int minHeight, int maxHeight);
|
||||||
|
std::uint64_t revision() const { return revision_; }
|
||||||
// Fuzzy search over cached blocks: matches when the query is a substring of the height (as text)
|
// Fuzzy search over cached blocks: matches when the query is a substring of the height (as text)
|
||||||
// or the block hash (case-insensitive). Returns newest-first, capped at `limit`.
|
// or the block hash (case-insensitive). Returns newest-first, capped at `limit`.
|
||||||
std::vector<ExplorerBlockSummary> searchBlocks(const std::string& query, int limit);
|
std::vector<ExplorerBlockSummary> searchBlocks(const std::string& query, int limit);
|
||||||
@@ -102,6 +108,11 @@ private:
|
|||||||
int open_attempts_ = 0;
|
int open_attempts_ = 0;
|
||||||
int stores_since_prune_ = 0;
|
int stores_since_prune_ = 0;
|
||||||
|
|
||||||
|
// Bumped by every mutation that can change what loadRange returns; keys the loadRangeCached memo.
|
||||||
|
std::uint64_t revision_ = 1;
|
||||||
|
std::uint64_t range_cache_revision_ = 0;
|
||||||
|
std::map<std::pair<int, int>, std::map<int, ExplorerBlockSummary>> range_cache_;
|
||||||
|
|
||||||
// Blocks the DB refused, bounded and UI-thread only. Evicts the entry farthest from the last spilled height
|
// Blocks the DB refused, bounded and UI-thread only. Evicts the entry farthest from the last spilled height
|
||||||
// (the Explorer fetches around the visible window). Cleared with clearBlocks() and when the chain tip regresses.
|
// (the Explorer fetches around the visible window). Cleared with clearBlocks() and when the chain tip regresses.
|
||||||
static constexpr std::size_t kMaxSpillBlocks = 512;
|
static constexpr std::size_t kMaxSpillBlocks = 512;
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
// DragonX Wallet - ImGui Edition
|
||||||
|
// Copyright 2024-2026 The Hush Developers
|
||||||
|
// Released under the GPLv3
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
// Best-effort scrub of ImGui's internal InputText buffers, which retain un-zeroed copies of typed
|
||||||
|
// secrets (wallet passphrase / PIN / seed / private key) after a field is edited — the app's own
|
||||||
|
// char[] buffers are memzeroed, but ImGui also copies the text into its global edit/undo/deactivated
|
||||||
|
// state, which it only frees (not zeroes).
|
||||||
|
//
|
||||||
|
// This reaches into vendored imgui_internal state, so it is PINNED to the vendored ImGui version: a
|
||||||
|
// bump trips the #error below and forces a re-audit of the member names. It is BEST-EFFORT only —
|
||||||
|
// std::-style buffer reallocation, the system clipboard, and OS/IME layers can still hold copies, so
|
||||||
|
// this shrinks the exposure window but does not guarantee absence of secret material in a core dump.
|
||||||
|
|
||||||
|
#include "imgui.h"
|
||||||
|
#include "imgui_internal.h"
|
||||||
|
#include <sodium.h>
|
||||||
|
#include <cstddef>
|
||||||
|
|
||||||
|
#if IMGUI_VERSION_NUM != 19259
|
||||||
|
#error "imgui_secret_scrub.h is pinned to ImGui 1.92.6 (IMGUI_VERSION_NUM 19259). After bumping libs/imgui, re-audit ImGuiInputTextState / ImGuiInputTextDeactivatedState / ImGuiContext member names below, then update this guard."
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace dragonx {
|
||||||
|
namespace ui {
|
||||||
|
|
||||||
|
// Zero ImGui's retained InputText buffers. Safe to call at a lock transition / when a secret dialog
|
||||||
|
// closes (it defocuses the active field). Do NOT call while a secret field is actively being typed.
|
||||||
|
inline void scrubImGuiInputTextState() {
|
||||||
|
ImGuiContext* ctx = ImGui::GetCurrentContext();
|
||||||
|
if (!ctx) return;
|
||||||
|
ImGuiInputTextState& st = ctx->InputTextState;
|
||||||
|
// Defocus first so the next frame can't re-read the buffer we're about to zero (ClearActiveID also
|
||||||
|
// refreshes the deactivated-state copy, which we then zero below).
|
||||||
|
if (st.ID != 0 && ctx->ActiveId == st.ID) ImGui::ClearActiveID();
|
||||||
|
auto zeroVec = [](ImVector<char>& v) {
|
||||||
|
if (v.Data && v.Size > 0) sodium_memzero(v.Data, static_cast<size_t>(v.Size));
|
||||||
|
};
|
||||||
|
zeroVec(st.TextA); // the live edit buffer
|
||||||
|
zeroVec(st.TextToRevertTo); // Escape-to-revert backup
|
||||||
|
zeroVec(st.CallbackTextBackup); // callback reconcile scratch
|
||||||
|
st.TextLen = 0;
|
||||||
|
// NOTE: st.Stb (the stb_textedit undo/redo char buffers) is deliberately NOT zeroed here — its type
|
||||||
|
// ImStbTexteditState is incomplete in imgui_internal.h (fully defined only inside ImGui's .cpp), so we
|
||||||
|
// can't sizeof it from a header. The primary copies above cover the committed text; the undo scratch
|
||||||
|
// is a residual gap in this best-effort scrub.
|
||||||
|
zeroVec(ctx->InputTextDeactivatedState.TextA); // the just-deactivated copy
|
||||||
|
ctx->InputTextDeactivatedState.ID = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ui
|
||||||
|
} // namespace dragonx
|
||||||
@@ -36,27 +36,52 @@ inline void PopTooltipStyle()
|
|||||||
ImGui::PopStyleVar();
|
ImGui::PopStyleVar();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Drop-in replacement for ImGui::SetTooltip (same printf-style signature). (No IM_FMTARGS: GCC
|
// Comfortable maximum tooltip text width (logical px, scaled by DPI). Long tooltip text wraps at this
|
||||||
// rejects the format attribute on an inline definition; call sites already passed SetTooltip's check.)
|
// instead of stretching across the whole screen; short text is unaffected (it's narrower than the cap).
|
||||||
|
inline constexpr float kTooltipWrapWidth = 420.0f;
|
||||||
|
|
||||||
|
// The wrap position (window-local x) to clamp a just-begun tooltip's text to. Called right after
|
||||||
|
// ImGui::BeginTooltip, when the cursor sits at the tooltip's content origin.
|
||||||
|
inline float tooltipWrapPosX()
|
||||||
|
{
|
||||||
|
return ImGui::GetCursorPosX() + kTooltipWrapWidth * Layout::dpiScale();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop-in replacement for ImGui::SetTooltip (same printf-style signature), with the text clamped to a
|
||||||
|
// comfortable width. (No IM_FMTARGS: GCC rejects the format attribute on an inline definition; call sites
|
||||||
|
// already passed SetTooltip's check.)
|
||||||
inline void Tooltip(const char* fmt, ...)
|
inline void Tooltip(const char* fmt, ...)
|
||||||
{
|
{
|
||||||
PushTooltipStyle();
|
PushTooltipStyle();
|
||||||
|
if (ImGui::BeginTooltip()) {
|
||||||
|
ImGui::PushTextWrapPos(tooltipWrapPosX());
|
||||||
va_list args;
|
va_list args;
|
||||||
va_start(args, fmt);
|
va_start(args, fmt);
|
||||||
ImGui::SetTooltipV(fmt, args);
|
ImGui::TextV(fmt, args);
|
||||||
va_end(args);
|
va_end(args);
|
||||||
|
ImGui::PopTextWrapPos();
|
||||||
|
ImGui::EndTooltip();
|
||||||
|
}
|
||||||
PopTooltipStyle();
|
PopTooltipStyle();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Drop-in replacements for ImGui::BeginTooltip / ImGui::EndTooltip (must be paired, as before).
|
// Drop-in replacements for ImGui::BeginTooltip / ImGui::EndTooltip (must be paired, as before). The begin
|
||||||
|
// clamps text to a comfortable width by default; a caller that pushes its own wrap pos nests cleanly (it
|
||||||
|
// pops back to this one). Text-only callers need do nothing; non-text content ignores the wrap pos.
|
||||||
inline bool BeginTooltip()
|
inline bool BeginTooltip()
|
||||||
{
|
{
|
||||||
PushTooltipStyle();
|
PushTooltipStyle();
|
||||||
return ImGui::BeginTooltip();
|
if (ImGui::BeginTooltip()) {
|
||||||
|
ImGui::PushTextWrapPos(tooltipWrapPosX());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
PopTooltipStyle(); // not shown: EndTooltip won't run, so undo the style push here
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
inline void EndTooltip()
|
inline void EndTooltip()
|
||||||
{
|
{
|
||||||
|
ImGui::PopTextWrapPos(); // pair the wrap pos pushed in BeginTooltip
|
||||||
ImGui::EndTooltip();
|
ImGui::EndTooltip();
|
||||||
PopTooltipStyle();
|
PopTooltipStyle();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ enum class NodeBannerAction {
|
|||||||
None, // no button — nothing the user can usefully do from here
|
None, // no button — nothing the user can usefully do from here
|
||||||
Reconnect, // full node: re-run the RPC connect state machine (App::tryConnect)
|
Reconnect, // full node: re-run the RPC connect state machine (App::tryConnect)
|
||||||
RestartNode, // full node: the embedded daemon crashed & auto-restart gave up (App::restartDaemon)
|
RestartNode, // full node: the embedded daemon crashed & auto-restart gave up (App::restartDaemon)
|
||||||
|
OpenConfigFolder,// external node: open the folder holding DRAGONX.conf so the user can fix the login
|
||||||
|
RecoverLegacyFunds, // a degraded legacy wallet with stuck transparent funds: open migrate-to-seed
|
||||||
};
|
};
|
||||||
|
|
||||||
// Why the banner is up. App maps this to a translated headline; `detail` carries the live,
|
// Why the banner is up. App maps this to a translated headline; `detail` carries the live,
|
||||||
@@ -33,6 +35,9 @@ enum class NodeBannerReason {
|
|||||||
LiteOpenFailed, // lite build: the wallet failed to open
|
LiteOpenFailed, // lite build: the wallet failed to open
|
||||||
NoPeers, // connected to the daemon but no peer-derived tip (0 peers) — stalled, stale
|
NoPeers, // connected to the daemon but no peer-derived tip (0 peers) — stalled, stale
|
||||||
NodeBusy, // the node is up but busy (rebuilding witnesses / catching up / not answering)
|
NodeBusy, // the node is up but busy (rebuilding witnesses / catching up / not answering)
|
||||||
|
AuthFailed, // the node rejected the wallet's RPC login (bad rpcuser/rpcpassword in the conf)
|
||||||
|
LegacyFundsStuck, // a DEGRADED legacy wallet (can't derive a z-address) holding transparent funds it
|
||||||
|
// therefore can't shield/spend — offer the migrate-to-seed recovery
|
||||||
};
|
};
|
||||||
|
|
||||||
struct NodeBannerState {
|
struct NodeBannerState {
|
||||||
@@ -71,6 +76,13 @@ struct NodeBannerInputs {
|
|||||||
bool node_busy = false;
|
bool node_busy = false;
|
||||||
std::string node_busy_detail;
|
std::string node_busy_detail;
|
||||||
|
|
||||||
|
bool auth_failed = false; // the node rejected the wallet's RPC login (bad rpcuser/rpcpassword)
|
||||||
|
|
||||||
|
// The wallet opened DEGRADED (no usable HD seed, so z_getnewaddress / shielding fail) AND it holds
|
||||||
|
// transparent funds that are therefore stuck. App sets this ONLY from wallet_degraded_ + a transparent
|
||||||
|
// balance — never from merely-non-mnemonic status, which is a functional wallet and must not be flagged.
|
||||||
|
bool legacy_funds_stuck = false;
|
||||||
|
|
||||||
std::string connection_status; // human-readable status line (already translated)
|
std::string connection_status; // human-readable status line (already translated)
|
||||||
std::string daemon_last_error; // DaemonController::lastError() (may be empty)
|
std::string daemon_last_error; // DaemonController::lastError() (may be empty)
|
||||||
std::string lite_open_error; // lite: last wallet-open failure reason
|
std::string lite_open_error; // lite: last wallet-open failure reason
|
||||||
@@ -130,6 +142,16 @@ inline NodeBannerState evaluateNodeStatusBanner(const NodeBannerInputs& in) {
|
|||||||
? NodeBannerAction::RestartNode
|
? NodeBannerAction::RestartNode
|
||||||
: NodeBannerAction::None;
|
: NodeBannerAction::None;
|
||||||
s.detail.clear();
|
s.detail.clear();
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
// Connected and synced, but a degraded legacy wallet can't shield its transparent funds. Offer the
|
||||||
|
// recovery flow. Checked after NoPeers (can't recover until the node can see the chain anyway).
|
||||||
|
if (in.legacy_funds_stuck) {
|
||||||
|
s.show = true;
|
||||||
|
s.severity = NodeBannerSeverity::Warning;
|
||||||
|
s.reason = NodeBannerReason::LegacyFundsStuck;
|
||||||
|
s.action = NodeBannerAction::RecoverLegacyFunds;
|
||||||
|
s.detail.clear();
|
||||||
}
|
}
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
@@ -146,6 +168,21 @@ inline NodeBannerState evaluateNodeStatusBanner(const NodeBannerInputs& in) {
|
|||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The node rejected the wallet's RPC login — retrying won't help (the creds are wrong). Restarting
|
||||||
|
// the embedded node regenerates its cookie/creds; for an external node the user fixes DRAGONX.conf,
|
||||||
|
// so offer to open its folder. App renders a translated, actionable detail (connection_status here
|
||||||
|
// is the jargon "Auth failed…" line).
|
||||||
|
if (in.auth_failed) {
|
||||||
|
s.show = true;
|
||||||
|
s.severity = NodeBannerSeverity::Error;
|
||||||
|
s.reason = NodeBannerReason::AuthFailed;
|
||||||
|
s.action = (in.using_embedded_daemon && in.has_daemon_controller)
|
||||||
|
? NodeBannerAction::RestartNode
|
||||||
|
: NodeBannerAction::OpenConfigFolder;
|
||||||
|
s.detail.clear();
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
s.show = true;
|
s.show = true;
|
||||||
s.severity = NodeBannerSeverity::Warning;
|
s.severity = NodeBannerSeverity::Warning;
|
||||||
s.reason = NodeBannerReason::FullNodeOffline;
|
s.reason = NodeBannerReason::FullNodeOffline;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include "schema/ui_schema.h"
|
#include "schema/ui_schema.h"
|
||||||
#include "material/type.h"
|
#include "material/type.h"
|
||||||
#include "material/draw_helpers.h"
|
#include "material/draw_helpers.h"
|
||||||
|
#include "material/tooltip_style.h" // material::Tooltip (width-clamped)
|
||||||
#include "../embedded/IconsMaterialDesign.h"
|
#include "../embedded/IconsMaterialDesign.h"
|
||||||
#include "imgui.h"
|
#include "imgui.h"
|
||||||
|
|
||||||
@@ -100,6 +101,15 @@ void Notifications::render()
|
|||||||
float iconGap = 4.0f * dp;
|
float iconGap = 4.0f * dp;
|
||||||
float msgW = textFont ? textFont->CalcTextSizeA(textFont->LegacySize, FLT_MAX, 0.0f, notif.message.c_str()).x : 100.0f;
|
float msgW = textFont ? textFont->CalcTextSizeA(textFont->LegacySize, FLT_MAX, 0.0f, notif.message.c_str()).x : 100.0f;
|
||||||
float pillWidth = pillPadX + iconW + iconGap + msgW + pillPadX;
|
float pillWidth = pillPadX + iconW + iconGap + msgW + pillPadX;
|
||||||
|
// "+N more" badge when several live toasts are queued (render shows only the newest).
|
||||||
|
const size_t moreCount = hiddenLiveCount();
|
||||||
|
std::string badge = moreCount ? ("+" + std::to_string(moreCount)) : std::string();
|
||||||
|
float badgeW = 0.0f, badgeGap = 0.0f;
|
||||||
|
if (!badge.empty() && textFont) {
|
||||||
|
badgeW = textFont->CalcTextSizeA(textFont->LegacySize, FLT_MAX, 0.0f, badge.c_str()).x + 10.0f * dp;
|
||||||
|
badgeGap = 6.0f * dp;
|
||||||
|
pillWidth += badgeW + badgeGap;
|
||||||
|
}
|
||||||
// Clamp to reasonable bounds
|
// Clamp to reasonable bounds
|
||||||
float maxPillW = viewport->WorkSize.x * 0.5f;
|
float maxPillW = viewport->WorkSize.x * 0.5f;
|
||||||
if (pillWidth > maxPillW) pillWidth = maxPillW;
|
if (pillWidth > maxPillW) pillWidth = maxPillW;
|
||||||
@@ -123,9 +133,16 @@ void Notifications::render()
|
|||||||
glassSpec.borderWidth = 1.0f;
|
glassSpec.borderWidth = 1.0f;
|
||||||
material::DrawGlassPanel(dl, pMin, pMax, glassSpec);
|
material::DrawGlassPanel(dl, pMin, pMax, glassSpec);
|
||||||
|
|
||||||
|
// Severity-tinted fill so the toast differs by more than hue (helps when color alone is hard to read).
|
||||||
|
{
|
||||||
|
ImVec4 tint = accent_color; // already premultiplied by fade `alpha`
|
||||||
|
tint.w *= nde("accent-fill-alpha", 0.22f);
|
||||||
|
dl->AddRectFilled(pMin, pMax, ImGui::ColorConvertFloat4ToU32(tint), pillRounding);
|
||||||
|
}
|
||||||
|
|
||||||
// Colored accent border on top of the glass border for type indication
|
// Colored accent border on top of the glass border for type indication
|
||||||
ImU32 accentCol = ImGui::ColorConvertFloat4ToU32(accent_color);
|
ImU32 accentCol = ImGui::ColorConvertFloat4ToU32(accent_color);
|
||||||
dl->AddRect(pMin, pMax, accentCol, pillRounding, 0, 1.0f);
|
dl->AddRect(pMin, pMax, accentCol, pillRounding, 0, 1.5f * dp);
|
||||||
|
|
||||||
// Progress bar at bottom of pill (accent-colored), clipped to pill rounded
|
// Progress bar at bottom of pill (accent-colored), clipped to pill rounded
|
||||||
// corners. Draw a full-pill-size rounded rect and clip it to just the
|
// corners. Draw a full-pill-size rounded rect and clip it to just the
|
||||||
@@ -156,7 +173,7 @@ void Notifications::render()
|
|||||||
bool clipped = false;
|
bool clipped = false;
|
||||||
if (textFont) {
|
if (textFont) {
|
||||||
ImU32 textCol = ImGui::ColorConvertFloat4ToU32(text_color);
|
ImU32 textCol = ImGui::ColorConvertFloat4ToU32(text_color);
|
||||||
const float availTextW = (pMax.x - pillPadX) - cursorX;
|
const float availTextW = (pMax.x - pillPadX) - cursorX - (badge.empty() ? 0.0f : (badgeW + badgeGap));
|
||||||
std::string shown = notif.message;
|
std::string shown = notif.message;
|
||||||
if (availTextW > 0.0f &&
|
if (availTextW > 0.0f &&
|
||||||
textFont->CalcTextSizeA(textFont->LegacySize, FLT_MAX, 0.0f, shown.c_str()).x > availTextW) {
|
textFont->CalcTextSizeA(textFont->LegacySize, FLT_MAX, 0.0f, shown.c_str()).x > availTextW) {
|
||||||
@@ -176,10 +193,25 @@ void Notifications::render()
|
|||||||
dl->PopClipRect();
|
dl->PopClipRect();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hover: pause the auto-dismiss (handled next frame) and reveal the full message if it was clipped.
|
// "+N more" badge, right-aligned inside the pill.
|
||||||
|
if (!badge.empty() && textFont) {
|
||||||
|
float badgeX = pMax.x - pillPadX - badgeW;
|
||||||
|
ImVec4 bf = accent_color; bf.w *= nde("badge-fill-alpha", 0.35f);
|
||||||
|
dl->AddRectFilled(ImVec2(badgeX, pillY + 3.0f * dp), ImVec2(badgeX + badgeW, pMax.y - 3.0f * dp),
|
||||||
|
ImGui::ColorConvertFloat4ToU32(bf), (pillHeight - 6.0f * dp) * 0.5f);
|
||||||
|
dl->AddText(textFont, textFont->LegacySize, ImVec2(badgeX + 5.0f * dp, contentY),
|
||||||
|
ImGui::ColorConvertFloat4ToU32(text_color), badge.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hover: pause the auto-dismiss (next frame), reveal the full message if clipped, and allow a click to
|
||||||
|
// open the notification history (the status bar consumes the request).
|
||||||
toast_hovered_ = ImGui::IsMouseHoveringRect(pMin, pMax);
|
toast_hovered_ = ImGui::IsMouseHoveringRect(pMin, pMax);
|
||||||
|
if (toast_hovered_) {
|
||||||
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
|
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) requestHistory();
|
||||||
|
}
|
||||||
if (toast_hovered_ && clipped)
|
if (toast_hovered_ && clipped)
|
||||||
ImGui::SetTooltip("%s", notif.message.c_str());
|
material::Tooltip("%s", notif.message.c_str()); // wrapped: a long alert message mustn't span the screen
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace ui
|
} // namespace ui
|
||||||
|
|||||||
@@ -169,8 +169,20 @@ public:
|
|||||||
|
|
||||||
void clear() {
|
void clear() {
|
||||||
notifications_.clear();
|
notifications_.clear();
|
||||||
|
history_requested_ = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Number of currently-live toasts beyond the one the pill shows (for a "+N more" badge).
|
||||||
|
size_t hiddenLiveCount() const {
|
||||||
|
const auto now = std::chrono::steady_clock::now();
|
||||||
|
size_t live = 0;
|
||||||
|
for (const auto& n : notifications_) if (!n.isExpiredAt(now)) ++live;
|
||||||
|
return live > 1 ? live - 1 : 0;
|
||||||
|
}
|
||||||
|
/// render() sets this when the toast is clicked; the status bar consumes it to open the history panel.
|
||||||
|
void requestHistory() { history_requested_ = true; }
|
||||||
|
bool consumeHistoryRequest() { bool r = history_requested_; history_requested_ = false; return r; }
|
||||||
|
|
||||||
void setMaxNotifications(size_t max) {
|
void setMaxNotifications(size_t max) {
|
||||||
max_notifications_ = max;
|
max_notifications_ = max;
|
||||||
}
|
}
|
||||||
@@ -191,6 +203,7 @@ private:
|
|||||||
Notifications& operator=(const Notifications&) = delete;
|
Notifications& operator=(const Notifications&) = delete;
|
||||||
|
|
||||||
std::deque<Notification> notifications_;
|
std::deque<Notification> notifications_;
|
||||||
|
bool history_requested_ = false; // set on toast click, consumed by the status bar to open history
|
||||||
size_t max_notifications_ = 5;
|
size_t max_notifications_ = 5;
|
||||||
bool toast_hovered_ = false; // pointer was over the toast last frame → pause its auto-dismiss
|
bool toast_hovered_ = false; // pointer was over the toast last frame → pause its auto-dismiss
|
||||||
std::chrono::steady_clock::time_point last_drawn_{};
|
std::chrono::steady_clock::time_point last_drawn_{};
|
||||||
|
|||||||
+163
-53
@@ -92,6 +92,7 @@ struct SettingsPageState {
|
|||||||
bool save_ztxs = true;
|
bool save_ztxs = true;
|
||||||
bool allow_custom_fees = false;
|
bool allow_custom_fees = false;
|
||||||
bool auto_shield = false;
|
bool auto_shield = false;
|
||||||
|
bool auto_consolidate = true; // on by default (loaded from settings on open; see getAutoConsolidate)
|
||||||
bool fetch_prices = true;
|
bool fetch_prices = true;
|
||||||
bool use_tor = false;
|
bool use_tor = false;
|
||||||
char rpc_host[128] = DRAGONX_DEFAULT_RPC_HOST;
|
char rpc_host[128] = DRAGONX_DEFAULT_RPC_HOST;
|
||||||
@@ -185,6 +186,7 @@ struct SettingsPageState {
|
|||||||
bool rescan_probe_failed = false; // the probe could not tell (node busy): warn before the -rescan restart
|
bool rescan_probe_failed = false; // the probe could not tell (node busy): warn before the -rescan restart
|
||||||
int rescan_probe_seq = 0; // the probe is async: a late result for an earlier opening is dropped
|
int rescan_probe_seq = 0; // the probe is async: a late result for an earlier opening is dropped
|
||||||
bool confirm_repair_wallet = false;
|
bool confirm_repair_wallet = false;
|
||||||
|
bool confirm_compact_wallet = false;
|
||||||
bool confirm_reinstall_daemon = false;
|
bool confirm_reinstall_daemon = false;
|
||||||
// Cached daemon-binary status for the "daemon binary" panel (loaded once / on Refresh,
|
// Cached daemon-binary status for the "daemon binary" panel (loaded once / on Refresh,
|
||||||
// since reading the installed binary to scan its version is a one-off disk read).
|
// since reading the installed binary to scan its version is a one-off disk read).
|
||||||
@@ -423,6 +425,7 @@ static void loadSettingsPageState(App* app) {
|
|||||||
s_settingsState.save_ztxs = settings->getSaveZtxs();
|
s_settingsState.save_ztxs = settings->getSaveZtxs();
|
||||||
s_settingsState.allow_custom_fees = settings->getAllowCustomFees();
|
s_settingsState.allow_custom_fees = settings->getAllowCustomFees();
|
||||||
s_settingsState.auto_shield = settings->getAutoShield();
|
s_settingsState.auto_shield = settings->getAutoShield();
|
||||||
|
s_settingsState.auto_consolidate = settings->getAutoConsolidate();
|
||||||
s_settingsState.fetch_prices = settings->getFetchPrices();
|
s_settingsState.fetch_prices = settings->getFetchPrices();
|
||||||
s_settingsState.use_tor = settings->getUseTor();
|
s_settingsState.use_tor = settings->getUseTor();
|
||||||
|
|
||||||
@@ -512,6 +515,7 @@ static void saveSettingsPageState(config::Settings* settings) {
|
|||||||
settings->setSaveZtxs(s_settingsState.save_ztxs);
|
settings->setSaveZtxs(s_settingsState.save_ztxs);
|
||||||
settings->setAllowCustomFees(s_settingsState.allow_custom_fees);
|
settings->setAllowCustomFees(s_settingsState.allow_custom_fees);
|
||||||
settings->setAutoShield(s_settingsState.auto_shield);
|
settings->setAutoShield(s_settingsState.auto_shield);
|
||||||
|
settings->setAutoConsolidate(s_settingsState.auto_consolidate);
|
||||||
settings->setFetchPrices(s_settingsState.fetch_prices);
|
settings->setFetchPrices(s_settingsState.fetch_prices);
|
||||||
settings->setUseTor(s_settingsState.use_tor);
|
settings->setUseTor(s_settingsState.use_tor);
|
||||||
settings->setTxExplorerUrl(s_settingsState.tx_explorer);
|
settings->setTxExplorerUrl(s_settingsState.tx_explorer);
|
||||||
@@ -869,7 +873,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
if (skin.bundled) continue;
|
if (skin.bundled) continue;
|
||||||
bool is_selected = (skin.id == skinMgr.activeSkinId());
|
bool is_selected = (skin.id == skinMgr.activeSkinId());
|
||||||
if (!skin.valid) {
|
if (!skin.valid) {
|
||||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.3f, 0.3f, 1.0f));
|
ImGui::PushStyleColor(ImGuiCol_Text, material::ErrorVec4());
|
||||||
ImGui::BeginDisabled(true);
|
ImGui::BeginDisabled(true);
|
||||||
std::string lbl = skin.name + TR("grpa_invalid_suffix");
|
std::string lbl = skin.name + TR("grpa_invalid_suffix");
|
||||||
ImGui::Selectable(lbl.c_str(), false);
|
ImGui::Selectable(lbl.c_str(), false);
|
||||||
@@ -904,7 +908,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
float cellGap = Layout::spacingLg();
|
float cellGap = Layout::spacingLg();
|
||||||
bool twoCol = contentW >= 460.0f * dp; // drop to a single column when too narrow (high font scale)
|
bool twoCol = contentW >= 460.0f * dp; // drop to a single column when too narrow (high font scale)
|
||||||
int cols = twoCol ? 2 : 1;
|
int cols = twoCol ? 2 : 1;
|
||||||
float colW = std::max(160.0f, twoCol ? (contentW - cellGap) * 0.5f : contentW);
|
float colW = std::max(160.0f * dp, twoCol ? (contentW - cellGap) * 0.5f : contentW);
|
||||||
float baseX = ImGui::GetCursorScreenPos().x;
|
float baseX = ImGui::GetCursorScreenPos().x;
|
||||||
|
|
||||||
const char* cellLabels[4] = { TR("theme"), TR("balance_layout"), TR("language"), TR("clock_format") };
|
const char* cellLabels[4] = { TR("theme"), TR("balance_layout"), TR("language"), TR("clock_format") };
|
||||||
@@ -1330,6 +1334,9 @@ void RenderSettingsPage(App* app) {
|
|||||||
if (CB(TrId("stop_external", "stop_ext"), &s_settingsState.stop_external_daemon))
|
if (CB(TrId("stop_external", "stop_ext"), &s_settingsState.stop_external_daemon))
|
||||||
saveSettingsPageState(app->settings());
|
saveSettingsPageState(app->settings());
|
||||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_stop_external"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_stop_external"));
|
||||||
|
if (CB(TrId("auto_consolidate", "auto_consol"), &s_settingsState.auto_consolidate))
|
||||||
|
saveSettingsPageState(app->settings());
|
||||||
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_auto_consolidate"));
|
||||||
// (Stratum pool hosting lives in the Node & Security tab — it's a node feature.)
|
// (Stratum pool hosting lives in the Node & Security tab — it's a node feature.)
|
||||||
}
|
}
|
||||||
if (CB(TrId("verbose_logging", "verbose"), &s_settingsState.verbose_logging)) {
|
if (CB(TrId("verbose_logging", "verbose"), &s_settingsState.verbose_logging)) {
|
||||||
@@ -1346,7 +1353,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
ImGui::SetCursorScreenPos(ImVec2(cx, last + gp));
|
ImGui::SetCursorScreenPos(ImVec2(cx, last + gp));
|
||||||
ImGui::PushTextWrapPos((cx + cw) - ImGui::GetWindowPos().x); // wrap to the card content width
|
ImGui::PushTextWrapPos((cx + cw) - ImGui::GetWindowPos().x); // wrap to the card content width
|
||||||
if (app->daemonAutoShieldActive()) {
|
if (app->daemonAutoShieldActive()) {
|
||||||
ImGui::TextColored(ImVec4(0.40f, 0.78f, 0.40f, 1.0f), "%s", TR("autoshield_by_node"));
|
ImGui::TextColored(material::SuccessVec4(), "%s", TR("autoshield_by_node"));
|
||||||
if (!app->daemonAutoShieldAddress().empty())
|
if (!app->daemonAutoShieldAddress().empty())
|
||||||
ImGui::TextDisabled(" %s", app->daemonAutoShieldAddress().c_str());
|
ImGui::TextDisabled(" %s", app->daemonAutoShieldAddress().c_str());
|
||||||
} else {
|
} else {
|
||||||
@@ -1490,6 +1497,9 @@ void RenderSettingsPage(App* app) {
|
|||||||
last = cy + bh; cy += bh + bgp;
|
last = cy + bh; cy += bh + bgp;
|
||||||
if (cardBtn(cx, cy, cw, "##imp_vk", TR("settings_import_viewkey"), ICON_MD_VISIBILITY)) app->showImportViewingKeyDialog();
|
if (cardBtn(cx, cy, cw, "##imp_vk", TR("settings_import_viewkey"), ICON_MD_VISIBILITY)) app->showImportViewingKeyDialog();
|
||||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_import_viewkey"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_import_viewkey"));
|
||||||
|
last = cy + bh; cy += bh + bgp;
|
||||||
|
if (cardBtn(cx, cy, cw, "##imp_file", TR("settings_import_wallet_file"), ICON_MD_UPLOAD_FILE)) app->showImportWalletFileDialog();
|
||||||
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_import_wallet_file"));
|
||||||
last = cy + bh;
|
last = cy + bh;
|
||||||
if (fullNode) {
|
if (fullNode) {
|
||||||
cy += bh + bgp;
|
cy += bh + bgp;
|
||||||
@@ -1548,9 +1558,13 @@ void RenderSettingsPage(App* app) {
|
|||||||
if (cardBtn(cx, cy, cw, "##exp_key", TR("settings_export_key"), ICON_MD_LOGOUT)) app->showExportKeyDialog();
|
if (cardBtn(cx, cy, cw, "##exp_key", TR("settings_export_key"), ICON_MD_LOGOUT)) app->showExportKeyDialog();
|
||||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_export_key"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_export_key"));
|
||||||
last = cy + bh; cy += bh + bgp;
|
last = cy + bh; cy += bh + bgp;
|
||||||
|
// Export-all only works over the full-node RPC; hide it in lite (which has its own key export
|
||||||
|
// in the lite wallet section) rather than dead-ending on "Not connected to the daemon".
|
||||||
|
if (fullNode) {
|
||||||
if (cardBtn(cx, cy, cw, "##exp_all", TR("settings_export_all"), ICON_MD_ARCHIVE)) ExportAllKeysDialog::show();
|
if (cardBtn(cx, cy, cw, "##exp_all", TR("settings_export_all"), ICON_MD_ARCHIVE)) ExportAllKeysDialog::show();
|
||||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_export_all"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_export_all"));
|
||||||
last = cy + bh; cy += bh + bgp;
|
last = cy + bh; cy += bh + bgp;
|
||||||
|
}
|
||||||
if (cardBtn(cx, cy, cw, "##exp_csv", TR("settings_export_csv"), ICON_MD_DESCRIPTION)) ExportTransactionsDialog::show();
|
if (cardBtn(cx, cy, cw, "##exp_csv", TR("settings_export_csv"), ICON_MD_DESCRIPTION)) ExportTransactionsDialog::show();
|
||||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_export_csv"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_export_csv"));
|
||||||
last = cy + bh;
|
last = cy + bh;
|
||||||
@@ -1645,20 +1659,20 @@ void RenderSettingsPage(App* app) {
|
|||||||
ImGui::SameLine(0, Layout::spacingMd());
|
ImGui::SameLine(0, Layout::spacingMd());
|
||||||
if (isLocked) {
|
if (isLocked) {
|
||||||
ImGui::PushFont(Type().iconSmall());
|
ImGui::PushFont(Type().iconSmall());
|
||||||
ImGui::TextColored(ImVec4(1,0.7f,0.3f,1.0f), ICON_MD_LOCK);
|
ImGui::TextColored(material::WarningVec4(), ICON_MD_LOCK);
|
||||||
ImGui::PopFont();
|
ImGui::PopFont();
|
||||||
ImGui::SameLine(0, Layout::spacingXs());
|
ImGui::SameLine(0, Layout::spacingXs());
|
||||||
ImGui::TextColored(ImVec4(1,0.7f,0.3f,1.0f), "%s", TR("settings_locked"));
|
ImGui::TextColored(material::WarningVec4(), "%s", TR("settings_locked"));
|
||||||
} else {
|
} else {
|
||||||
if (TactileButton(TR("settings_lock_now"), ImVec2(secBtnW, 0), S.resolveFont("button")))
|
if (TactileButton(TR("settings_lock_now"), ImVec2(secBtnW, 0), S.resolveFont("button")))
|
||||||
app->lockWallet();
|
app->lockWallet();
|
||||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lock"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lock"));
|
||||||
ImGui::SameLine(0, Layout::spacingSm());
|
ImGui::SameLine(0, Layout::spacingSm());
|
||||||
ImGui::PushFont(Type().iconSmall());
|
ImGui::PushFont(Type().iconSmall());
|
||||||
ImGui::TextColored(ImVec4(0.3f,1.0f,0.5f,1.0f), ICON_MD_LOCK_OPEN);
|
ImGui::TextColored(material::SuccessVec4(), ICON_MD_LOCK_OPEN);
|
||||||
ImGui::PopFont();
|
ImGui::PopFont();
|
||||||
ImGui::SameLine(0, Layout::spacingXs());
|
ImGui::SameLine(0, Layout::spacingXs());
|
||||||
ImGui::TextColored(ImVec4(0.3f,1.0f,0.5f,1.0f), "%s", TR("settings_unlocked"));
|
ImGui::TextColored(material::SuccessVec4(), "%s", TR("settings_unlocked"));
|
||||||
}
|
}
|
||||||
// Remove Encryption button — trails the row, or wraps below
|
// Remove Encryption button — trails the row, or wraps below
|
||||||
// when the column is too narrow for three buttons + status.
|
// when the column is too narrow for three buttons + status.
|
||||||
@@ -1704,7 +1718,11 @@ void RenderSettingsPage(App* app) {
|
|||||||
|
|
||||||
// PIN unlock controls, trailing the auto-lock combo on the same row.
|
// PIN unlock controls, trailing the auto-lock combo on the same row.
|
||||||
bool isEncryptedPIN = app->state().isEncrypted();
|
bool isEncryptedPIN = app->state().isEncrypted();
|
||||||
if (isEncryptedPIN) {
|
// Lite has no PIN quick-unlock: its unlock paths (Settings, send-time prompt) take the passphrase
|
||||||
|
// directly and never consult a PIN vault, so these controls would be dead. Hide them.
|
||||||
|
if (app->isLiteBuild()) {
|
||||||
|
// nothing to show
|
||||||
|
} else if (isEncryptedPIN) {
|
||||||
bool hasPIN = app->hasPinVault();
|
bool hasPIN = app->hasPinVault();
|
||||||
float pinBtnW = std::min(rowBtnW({TR("settings_set_pin"), TR("settings_change_pin"), TR("settings_remove_pin")}), (secColW - Layout::spacingSm()) * 0.5f);
|
float pinBtnW = std::min(rowBtnW({TR("settings_set_pin"), TR("settings_change_pin"), TR("settings_remove_pin")}), (secColW - Layout::spacingSm()) * 0.5f);
|
||||||
|
|
||||||
@@ -1752,10 +1770,10 @@ void RenderSettingsPage(App* app) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ImGui::PushFont(Type().iconSmall());
|
ImGui::PushFont(Type().iconSmall());
|
||||||
ImGui::TextColored(ImVec4(0.3f,1.0f,0.5f,1.0f), ICON_MD_DIALPAD);
|
ImGui::TextColored(material::SuccessVec4(), ICON_MD_DIALPAD);
|
||||||
ImGui::PopFont();
|
ImGui::PopFont();
|
||||||
ImGui::SameLine(0, Layout::spacingXs());
|
ImGui::SameLine(0, Layout::spacingXs());
|
||||||
ImGui::TextColored(ImVec4(0.3f,1.0f,0.5f,1.0f), "%s", TR("settings_pin_active"));
|
ImGui::TextColored(material::SuccessVec4(), "%s", TR("settings_pin_active"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -2168,9 +2186,10 @@ void RenderSettingsPage(App* app) {
|
|||||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_unlock_pass"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_unlock_pass"));
|
||||||
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||||||
if (TactileButton(TrId("lite_unlock", "LiteUnlock").c_str(), ImVec2(0, 0), S.resolveFont("button"))) {
|
if (TactileButton(TrId("lite_unlock", "LiteUnlock").c_str(), ImVec2(0, 0), S.resolveFont("button"))) {
|
||||||
const bool ok = app->liteWallet()->unlockWallet(s_settingsState.lite_enc_pass);
|
std::string unlockErr;
|
||||||
|
const bool ok = app->unlockLiteWallet(std::string(s_settingsState.lite_enc_pass), &unlockErr);
|
||||||
sodium_memzero(s_settingsState.lite_enc_pass, sizeof(s_settingsState.lite_enc_pass));
|
sodium_memzero(s_settingsState.lite_enc_pass, sizeof(s_settingsState.lite_enc_pass));
|
||||||
s_settingsState.lite_encryption_status = ok ? TR("lite_wallet_unlocked") : TR("lite_unlock_failed");
|
s_settingsState.lite_encryption_status = ok ? std::string(TR("lite_wallet_unlocked")) : unlockErr;
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_unlock"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_unlock"));
|
||||||
} else {
|
} else {
|
||||||
@@ -2327,8 +2346,10 @@ void RenderSettingsPage(App* app) {
|
|||||||
cs.hoverBg = material::WithAlpha(OnSurface(), 30);
|
cs.hoverBg = material::WithAlpha(OnSurface(), 30);
|
||||||
cs.tooltip = TR("copy");
|
cs.tooltip = TR("copy");
|
||||||
const float ih = ImGui::GetFrameHeight();
|
const float ih = ImGui::GetFrameHeight();
|
||||||
if (material::IconButton("##copydir", ICON_MD_CONTENT_COPY, Type().iconSmall(), ImVec2(ih, ih), cs))
|
if (material::IconButton("##copydir", ICON_MD_CONTENT_COPY, Type().iconSmall(), ImVec2(ih, ih), cs)) {
|
||||||
ImGui::SetClipboardText(dirPath.c_str());
|
ImGui::SetClipboardText(dirPath.c_str());
|
||||||
|
Notifications::instance().info(TR("copied_to_clipboard"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Row 2: Wallet size — label left, value RIGHT-aligned.
|
// Row 2: Wallet size — label left, value RIGHT-aligned.
|
||||||
@@ -2363,6 +2384,20 @@ void RenderSettingsPage(App* app) {
|
|||||||
ICON_MD_CALL_MERGE, material::ActionTier::Secondary))
|
ICON_MD_CALL_MERGE, material::ActionTier::Secondary))
|
||||||
ShieldDialog::showConsolidate();
|
ShieldDialog::showConsolidate();
|
||||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_merge"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_merge"));
|
||||||
|
ImGui::SameLine();
|
||||||
|
// Compact reclaims the file space consolidation frees (BDB won't shrink in place).
|
||||||
|
// It rewrites wallet.dat by restarting the node with -salvagewallet, so it only works
|
||||||
|
// with the embedded node — disable it for an external dragonxd and explain on hover.
|
||||||
|
// Enabled whenever this build can run the built-in node. If connected to an external
|
||||||
|
// node, the confirmation offers to switch to the built-in one first (compactViaEmbeddedSwitch).
|
||||||
|
const bool compactPossible = app->supportsEmbeddedDaemon();
|
||||||
|
ImGui::BeginDisabled(!compactPossible);
|
||||||
|
if (material::ActionButton("##walletcompact", TR("wallet_size_compact"),
|
||||||
|
ICON_MD_COMPRESS, material::ActionTier::Secondary))
|
||||||
|
s_settingsState.confirm_compact_wallet = true;
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
|
||||||
|
material::Tooltip("%s", TR(compactPossible ? "tt_wallet_size_compact" : "compact_needs_embedded"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Row 3: folder buttons (their own row so the path gets the full width).
|
// Row 3: folder buttons (their own row so the path gets the full width).
|
||||||
@@ -2591,6 +2626,21 @@ void RenderSettingsPage(App* app) {
|
|||||||
if (ui::DaemonUpdateDialog::consumeInstalled())
|
if (ui::DaemonUpdateDialog::consumeInstalled())
|
||||||
s_settingsState.daemon_info_loaded = false;
|
s_settingsState.daemon_info_loaded = false;
|
||||||
|
|
||||||
|
// Persistent reminder: a newer daemon binary is installed but the running node is still
|
||||||
|
// the old one until a restart (easy to miss on the updater's Done screen).
|
||||||
|
if (app->daemonRestartPending()) {
|
||||||
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||||
|
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + contentW);
|
||||||
|
Type().textColored(TypeStyle::Caption, material::Warning(), TR("daemon_restart_pending"));
|
||||||
|
ImGui::PopTextWrapPos();
|
||||||
|
if (app->isUsingEmbeddedDaemon()) {
|
||||||
|
if (material::ActionButton("##drestartpend", TR("daemon_update_restart_now"),
|
||||||
|
ICON_MD_RESTART_ALT, material::ActionTier::Primary))
|
||||||
|
app->restartDaemon();
|
||||||
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_daemon_restart_pending"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- UPDATES ---
|
// --- UPDATES ---
|
||||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("daemon_updates_label"));
|
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("daemon_updates_label"));
|
||||||
@@ -2727,7 +2777,10 @@ void RenderSettingsPage(App* app) {
|
|||||||
ImGui::TextUnformatted(TR("transaction_url"));
|
ImGui::TextUnformatted(TR("transaction_url"));
|
||||||
ImGui::SameLine(urlRowX + urlLblW);
|
ImGui::SameLine(urlRowX + urlLblW);
|
||||||
ImGui::SetNextItemWidth(urlInputW);
|
ImGui::SetNextItemWidth(urlInputW);
|
||||||
|
// These Explorer-tab controls persist on edit — they were the only ones still relying on the
|
||||||
|
// About-only Save button, which a user on this tab never sees.
|
||||||
ImGui::InputText("##TxExplorer", s_settingsState.tx_explorer, sizeof(s_settingsState.tx_explorer));
|
ImGui::InputText("##TxExplorer", s_settingsState.tx_explorer, sizeof(s_settingsState.tx_explorer));
|
||||||
|
if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings());
|
||||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_tx_url"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_tx_url"));
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||||
@@ -2737,6 +2790,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
ImGui::SameLine(urlRowX + urlLblW);
|
ImGui::SameLine(urlRowX + urlLblW);
|
||||||
ImGui::SetNextItemWidth(urlInputW);
|
ImGui::SetNextItemWidth(urlInputW);
|
||||||
ImGui::InputText("##AddrExplorer", s_settingsState.addr_explorer, sizeof(s_settingsState.addr_explorer));
|
ImGui::InputText("##AddrExplorer", s_settingsState.addr_explorer, sizeof(s_settingsState.addr_explorer));
|
||||||
|
if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings());
|
||||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_addr_url"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_addr_url"));
|
||||||
|
|
||||||
ImGui::PopFont();
|
ImGui::PopFont();
|
||||||
@@ -2754,10 +2808,12 @@ void RenderSettingsPage(App* app) {
|
|||||||
|
|
||||||
// Checkboxes + Block Explorer button (button wraps to its own row when it won't fit).
|
// Checkboxes + Block Explorer button (button wraps to its own row when it won't fit).
|
||||||
const float expRowRight = ImGui::GetCursorScreenPos().x + contentW;
|
const float expRowRight = ImGui::GetCursorScreenPos().x + contentW;
|
||||||
ImGui::Checkbox(TrId("custom_fees", "custom_fees").c_str(), &s_settingsState.allow_custom_fees);
|
if (ImGui::Checkbox(TrId("custom_fees", "custom_fees").c_str(), &s_settingsState.allow_custom_fees))
|
||||||
|
saveSettingsPageState(app->settings());
|
||||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_custom_fees"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_custom_fees"));
|
||||||
ImGui::SameLine(0, Layout::spacingLg());
|
ImGui::SameLine(0, Layout::spacingLg());
|
||||||
ImGui::Checkbox(TrId("fetch_prices", "fetch_prices").c_str(), &s_settingsState.fetch_prices);
|
if (ImGui::Checkbox(TrId("fetch_prices", "fetch_prices").c_str(), &s_settingsState.fetch_prices))
|
||||||
|
saveSettingsPageState(app->settings());
|
||||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_fetch_prices"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_fetch_prices"));
|
||||||
const float expBtnW = ImGui::CalcTextSize(TR("block_explorer")).x
|
const float expBtnW = ImGui::CalcTextSize(TR("block_explorer")).x
|
||||||
+ ImGui::GetStyle().FramePadding.x * 2.0f + Layout::spacingLg();
|
+ ImGui::GetStyle().FramePadding.x * 2.0f + Layout::spacingLg();
|
||||||
@@ -2962,7 +3018,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
s_settingsState.stratum_allowip, sizeof(s_settingsState.stratum_allowip)))
|
s_settingsState.stratum_allowip, sizeof(s_settingsState.stratum_allowip)))
|
||||||
saveSettingsPageState(app->settings());
|
saveSettingsPageState(app->settings());
|
||||||
if (s_settingsState.stratum_allowip[0] != '\0')
|
if (s_settingsState.stratum_allowip[0] != '\0')
|
||||||
ImGui::TextColored(ImVec4(0.95f, 0.75f, 0.25f, 1.0f), " %s",
|
ImGui::TextColored(material::WarningVec4(), " %s",
|
||||||
TR("stratum_expose_warn"));
|
TR("stratum_expose_warn"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2991,14 +3047,23 @@ void RenderSettingsPage(App* app) {
|
|||||||
s_settingsState.debug_gate_verifying = false;
|
s_settingsState.debug_gate_verifying = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", s_settingsState.debug_expanded ? TR("tt_debug_collapse") : TR("tt_debug_expand"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", s_settingsState.debug_expanded ? TR("tt_debug_collapse")
|
||||||
|
: (s_settingsState.debug_gate_passed ? TR("tt_debug_expand") : TR("tt_debug_expand_gated")));
|
||||||
ImGui::PopStyleColor(3);
|
ImGui::PopStyleColor(3);
|
||||||
|
|
||||||
// Draw overline label + arrow on top of the invisible button
|
// Draw overline label + arrow on top of the invisible button
|
||||||
{
|
{
|
||||||
ImFont* ovFont = Type().overline();
|
ImFont* ovFont = Type().overline();
|
||||||
float textY = headerPos.y + (ImGui::GetFrameHeight() - ovFont->LegacySize) * 0.5f;
|
float textY = headerPos.y + (ImGui::GetFrameHeight() - ovFont->LegacySize) * 0.5f;
|
||||||
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(headerPos.x, textY), OnSurfaceMedium(), TR("debug_logging"));
|
float labelX = headerPos.x;
|
||||||
|
// Show a lock beside the label while the session gate hasn't been passed, so it's clear the
|
||||||
|
// section is gated rather than just a bare collapsible header.
|
||||||
|
if (!s_settingsState.debug_gate_passed) {
|
||||||
|
ImFont* lockFont = Type().iconSmall(); if (!lockFont) lockFont = ovFont;
|
||||||
|
dl->AddText(lockFont, lockFont->LegacySize, ImVec2(labelX, textY), OnSurfaceMedium(), ICON_MD_LOCK);
|
||||||
|
labelX += lockFont->CalcTextSizeA(lockFont->LegacySize, FLT_MAX, 0, ICON_MD_LOCK).x + Layout::spacingXs();
|
||||||
|
}
|
||||||
|
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(labelX, textY), OnSurfaceMedium(), TR("debug_logging"));
|
||||||
// Use the icon font for the expand/collapse arrow
|
// Use the icon font for the expand/collapse arrow
|
||||||
ImFont* iconFont = Type().iconSmall();
|
ImFont* iconFont = Type().iconSmall();
|
||||||
if (!iconFont) iconFont = ovFont;
|
if (!iconFont) iconFont = ovFont;
|
||||||
@@ -3006,6 +3071,14 @@ void RenderSettingsPage(App* app) {
|
|||||||
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(headerPos.x + availWidth - arrowW - pad, textY), OnSurfaceMedium(), arrow);
|
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(headerPos.x + availWidth - arrowW - pad, textY), OnSurfaceMedium(), arrow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// One-line explanation under the collapsed header (what it is + that it's gated).
|
||||||
|
if (!s_settingsState.debug_expanded) {
|
||||||
|
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(),
|
||||||
|
s_settingsState.debug_gate_passed ? TR("debug_options_caption_unlocked")
|
||||||
|
: TR("debug_options_caption_gated"));
|
||||||
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||||
|
}
|
||||||
|
|
||||||
if (s_settingsState.debug_expanded) {
|
if (s_settingsState.debug_expanded) {
|
||||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||||
|
|
||||||
@@ -3049,9 +3122,33 @@ void RenderSettingsPage(App* app) {
|
|||||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
ImGui::TextColored(ImVec4(1,1,1,0.5f), "%s", TR("settings_debug_select"));
|
ImGui::PushStyleColor(ImGuiCol_Text, material::OnSurfaceMedium());
|
||||||
ImGui::TextColored(ImVec4(1,1,1,0.35f), "%s", TR("settings_debug_restart_note"));
|
ImGui::TextUnformatted(TR("settings_debug_select"));
|
||||||
|
ImGui::TextWrapped("%s", TR("settings_debug_restart_note")); // was near-invisible 35% white
|
||||||
|
ImGui::PopStyleColor();
|
||||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||||
|
// Restart prompt moved directly under the note so the required action is prominent (appears the
|
||||||
|
// frame after a category toggles, which sets debug_cats_dirty while rendering the grid below).
|
||||||
|
if (s_settingsState.debug_cats_dirty && app->supportsFullNodeLifecycleActions()) {
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, material::Warning());
|
||||||
|
ImFont* iconFont = Type().iconSmall();
|
||||||
|
if (iconFont) {
|
||||||
|
ImGui::PushFont(iconFont);
|
||||||
|
ImGui::AlignTextToFramePadding();
|
||||||
|
ImGui::TextUnformatted(ICON_MD_INFO);
|
||||||
|
ImGui::PopFont();
|
||||||
|
ImGui::SameLine(0, Layout::spacingXs());
|
||||||
|
}
|
||||||
|
ImGui::AlignTextToFramePadding();
|
||||||
|
ImGui::TextUnformatted(TR("settings_debug_changed"));
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (TactileButton(TR("settings_restart_daemon"), ImVec2(0, 0), S.resolveFont("button"))) {
|
||||||
|
s_settingsState.confirm_restart_daemon = true;
|
||||||
|
}
|
||||||
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_restart_daemon"));
|
||||||
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||||
|
}
|
||||||
|
|
||||||
// The 22 dragonxd debug categories
|
// The 22 dragonxd debug categories
|
||||||
static const char* debugCats[] = {
|
static const char* debugCats[] = {
|
||||||
@@ -3109,27 +3206,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||||
|
// (The "restart daemon" prompt was moved up to directly under the restart note above.)
|
||||||
// "Restart daemon" button — only active when categories changed
|
|
||||||
if (s_settingsState.debug_cats_dirty && app->supportsFullNodeLifecycleActions()) {
|
|
||||||
ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255, 218, 0, 255));
|
|
||||||
ImFont* iconFont = Type().iconSmall();
|
|
||||||
if (iconFont) {
|
|
||||||
ImGui::PushFont(iconFont);
|
|
||||||
ImGui::AlignTextToFramePadding();
|
|
||||||
ImGui::TextUnformatted(ICON_MD_INFO);
|
|
||||||
ImGui::PopFont();
|
|
||||||
ImGui::SameLine(0, Layout::spacingXs());
|
|
||||||
}
|
|
||||||
ImGui::AlignTextToFramePadding();
|
|
||||||
ImGui::TextUnformatted(TR("settings_debug_changed"));
|
|
||||||
ImGui::PopStyleColor();
|
|
||||||
ImGui::SameLine();
|
|
||||||
if (TactileButton(TR("settings_restart_daemon"), ImVec2(0, 0), S.resolveFont("button"))) {
|
|
||||||
s_settingsState.confirm_restart_daemon = true;
|
|
||||||
}
|
|
||||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_restart_daemon"));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Debug-options gate: confirmation + warning (+ re-auth if a PIN/passphrase is set) ----
|
// ---- Debug-options gate: confirmation + warning (+ re-auth if a PIN/passphrase is set) ----
|
||||||
@@ -3277,12 +3354,12 @@ void RenderSettingsPage(App* app) {
|
|||||||
// Confirmation dialog for deleting blockchain data
|
// Confirmation dialog for deleting blockchain data
|
||||||
if (s_settingsState.confirm_delete_blockchain) {
|
if (s_settingsState.confirm_delete_blockchain) {
|
||||||
if (BeginOverlayDialog(TR("confirm_delete_blockchain_title"), &s_settingsState.confirm_delete_blockchain, 500.0f, 0.94f)) {
|
if (BeginOverlayDialog(TR("confirm_delete_blockchain_title"), &s_settingsState.confirm_delete_blockchain, 500.0f, 0.94f)) {
|
||||||
material::DialogWarningHeader(TR("warning"), ImVec4(1.0f, 0.3f, 0.3f, 1.0f));
|
material::DialogWarningHeader(TR("warning"), material::ErrorVec4());
|
||||||
|
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::TextWrapped("%s", TR("confirm_delete_blockchain_msg"));
|
ImGui::TextWrapped("%s", TR("confirm_delete_blockchain_msg"));
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", TR("confirm_delete_blockchain_safe"));
|
ImGui::TextColored(material::SuccessVec4(), "%s", TR("confirm_delete_blockchain_safe"));
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
@@ -3324,7 +3401,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (BeginOverlayDialog(TR("confirm_rescan_title"), &s_settingsState.confirm_rescan, 500.0f, 0.94f)) {
|
if (BeginOverlayDialog(TR("confirm_rescan_title"), &s_settingsState.confirm_rescan, 500.0f, 0.94f)) {
|
||||||
material::DialogWarningHeader(TR("warning"), ImVec4(1.0f, 0.8f, 0.2f, 1.0f));
|
material::DialogWarningHeader(TR("warning"), material::WarningVec4());
|
||||||
|
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
|
||||||
@@ -3343,14 +3420,14 @@ void RenderSettingsPage(App* app) {
|
|||||||
} else {
|
} else {
|
||||||
if (s_settingsState.rescan_probe_failed) {
|
if (s_settingsState.rescan_probe_failed) {
|
||||||
// A long line: TextColored does not wrap, so colour a wrapped one.
|
// A long line: TextColored does not wrap, so colour a wrapped one.
|
||||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.8f, 0.2f, 1.0f));
|
ImGui::PushStyleColor(ImGuiCol_Text, material::WarningVec4());
|
||||||
ImGui::TextWrapped("%s", TR("rescan_probe_failed"));
|
ImGui::TextWrapped("%s", TR("rescan_probe_failed"));
|
||||||
ImGui::PopStyleColor();
|
ImGui::PopStyleColor();
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
}
|
}
|
||||||
ImGui::TextWrapped("%s", TR("confirm_rescan_msg"));
|
ImGui::TextWrapped("%s", TR("confirm_rescan_msg"));
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", TR("confirm_rescan_safe"));
|
ImGui::TextColored(material::SuccessVec4(), "%s", TR("confirm_rescan_safe"));
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
@@ -3379,12 +3456,12 @@ void RenderSettingsPage(App* app) {
|
|||||||
// Confirm: repair wallet (-zapwallettxes=2 — wipe & rebuild wallet tx records, then rescan)
|
// Confirm: repair wallet (-zapwallettxes=2 — wipe & rebuild wallet tx records, then rescan)
|
||||||
if (s_settingsState.confirm_repair_wallet) {
|
if (s_settingsState.confirm_repair_wallet) {
|
||||||
if (BeginOverlayDialog(TR("confirm_repair_wallet_title"), &s_settingsState.confirm_repair_wallet, 500.0f, 0.94f)) {
|
if (BeginOverlayDialog(TR("confirm_repair_wallet_title"), &s_settingsState.confirm_repair_wallet, 500.0f, 0.94f)) {
|
||||||
material::DialogWarningHeader(TR("warning"), ImVec4(1.0f, 0.8f, 0.2f, 1.0f));
|
material::DialogWarningHeader(TR("warning"), material::WarningVec4());
|
||||||
|
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::TextWrapped("%s", TR("confirm_repair_wallet_msg"));
|
ImGui::TextWrapped("%s", TR("confirm_repair_wallet_msg"));
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", TR("confirm_repair_wallet_safe"));
|
ImGui::TextColored(material::SuccessVec4(), "%s", TR("confirm_repair_wallet_safe"));
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
@@ -3404,15 +3481,48 @@ void RenderSettingsPage(App* app) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Confirm: compact wallet.dat to reclaim the space consolidation freed (rewrite via -salvagewallet,
|
||||||
|
// which restarts the node and re-scans the chain). A backup of the current wallet is kept.
|
||||||
|
if (s_settingsState.confirm_compact_wallet) {
|
||||||
|
// Direct compact when the wallet already runs the built-in node; otherwise the dialog explains it
|
||||||
|
// will stop the external node and switch to the built-in one first.
|
||||||
|
const bool embeddedNow = app->canCompactWallet();
|
||||||
|
if (BeginOverlayDialog(TR("confirm_compact_wallet_title"), &s_settingsState.confirm_compact_wallet, 500.0f, 0.94f)) {
|
||||||
|
material::DialogWarningHeader(TR("warning"), material::WarningVec4());
|
||||||
|
|
||||||
|
ImGui::Spacing();
|
||||||
|
ImGui::TextWrapped("%s", TR(embeddedNow ? "confirm_compact_wallet_msg" : "confirm_compact_switch_msg"));
|
||||||
|
ImGui::Spacing();
|
||||||
|
ImGui::TextColored(material::SuccessVec4(), "%s", TR("confirm_compact_wallet_safe"));
|
||||||
|
ImGui::Spacing();
|
||||||
|
ImGui::Separator();
|
||||||
|
ImGui::Spacing();
|
||||||
|
|
||||||
|
bool doCancel = false, doConfirm = false;
|
||||||
|
material::DialogConfirmFooter(TrId("cancel", "compact_wallet_cancel").c_str(),
|
||||||
|
TrId("compact_wallet_confirm", "compact_wallet_confirm").c_str(),
|
||||||
|
false, doCancel, doConfirm);
|
||||||
|
if (doCancel) {
|
||||||
|
s_settingsState.confirm_compact_wallet = false;
|
||||||
|
}
|
||||||
|
if (doConfirm) {
|
||||||
|
if (embeddedNow) app->compactWallet();
|
||||||
|
else app->compactViaEmbeddedSwitch();
|
||||||
|
s_settingsState.confirm_compact_wallet = false;
|
||||||
|
}
|
||||||
|
EndOverlayDialog();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Confirm: reinstall the bundled daemon binary (stop → overwrite → restart)
|
// Confirm: reinstall the bundled daemon binary (stop → overwrite → restart)
|
||||||
if (s_settingsState.confirm_reinstall_daemon) {
|
if (s_settingsState.confirm_reinstall_daemon) {
|
||||||
if (BeginOverlayDialog(TR("confirm_reinstall_daemon_title"), &s_settingsState.confirm_reinstall_daemon, 500.0f, 0.94f)) {
|
if (BeginOverlayDialog(TR("confirm_reinstall_daemon_title"), &s_settingsState.confirm_reinstall_daemon, 500.0f, 0.94f)) {
|
||||||
material::DialogWarningHeader(TR("warning"), ImVec4(1.0f, 0.8f, 0.2f, 1.0f));
|
material::DialogWarningHeader(TR("warning"), material::WarningVec4());
|
||||||
|
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::TextWrapped("%s", TR("confirm_reinstall_daemon_msg"));
|
ImGui::TextWrapped("%s", TR("confirm_reinstall_daemon_msg"));
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", TR("confirm_reinstall_daemon_safe"));
|
ImGui::TextColored(material::SuccessVec4(), "%s", TR("confirm_reinstall_daemon_safe"));
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
@@ -3436,7 +3546,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
// Confirm: restart daemon (briefly drops the connection to apply changed options)
|
// Confirm: restart daemon (briefly drops the connection to apply changed options)
|
||||||
if (s_settingsState.confirm_restart_daemon) {
|
if (s_settingsState.confirm_restart_daemon) {
|
||||||
if (BeginOverlayDialog(TR("confirm_restart_daemon_title"), &s_settingsState.confirm_restart_daemon, 500.0f, 0.94f)) {
|
if (BeginOverlayDialog(TR("confirm_restart_daemon_title"), &s_settingsState.confirm_restart_daemon, 500.0f, 0.94f)) {
|
||||||
material::DialogWarningHeader(TR("warning"), ImVec4(1.0f, 0.8f, 0.2f, 1.0f));
|
material::DialogWarningHeader(TR("warning"), material::WarningVec4());
|
||||||
|
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::TextWrapped("%s", TR("confirm_restart_daemon_msg"));
|
ImGui::TextWrapped("%s", TR("confirm_restart_daemon_msg"));
|
||||||
@@ -3463,12 +3573,12 @@ void RenderSettingsPage(App* app) {
|
|||||||
// Confirm: lite wallet re-download blocks (rescan from the lite server — long but safe)
|
// Confirm: lite wallet re-download blocks (rescan from the lite server — long but safe)
|
||||||
if (s_settingsState.confirm_lite_redownload) {
|
if (s_settingsState.confirm_lite_redownload) {
|
||||||
if (BeginOverlayDialog(TR("confirm_lite_redownload_title"), &s_settingsState.confirm_lite_redownload, 500.0f, 0.94f)) {
|
if (BeginOverlayDialog(TR("confirm_lite_redownload_title"), &s_settingsState.confirm_lite_redownload, 500.0f, 0.94f)) {
|
||||||
material::DialogWarningHeader(TR("warning"), ImVec4(1.0f, 0.8f, 0.2f, 1.0f));
|
material::DialogWarningHeader(TR("warning"), material::WarningVec4());
|
||||||
|
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::TextWrapped("%s", TR("confirm_lite_redownload_msg"));
|
ImGui::TextWrapped("%s", TR("confirm_lite_redownload_msg"));
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", TR("confirm_lite_redownload_safe"));
|
ImGui::TextColored(material::SuccessVec4(), "%s", TR("confirm_lite_redownload_safe"));
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ void RenderAboutDialog(App* app, bool* p_open)
|
|||||||
#ifdef DRAGONX_DEBUG
|
#ifdef DRAGONX_DEBUG
|
||||||
ImGui::Text("%s", TR("about_build_type"));
|
ImGui::Text("%s", TR("about_build_type"));
|
||||||
ImGui::SameLine(valX);
|
ImGui::SameLine(valX);
|
||||||
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), "%s", TR("about_debug"));
|
ImGui::TextColored(material::WarningVec4(), "%s", TR("about_debug"));
|
||||||
#else
|
#else
|
||||||
ImGui::Text("%s", TR("about_build_type"));
|
ImGui::Text("%s", TR("about_build_type"));
|
||||||
ImGui::SameLine(valX);
|
ImGui::SameLine(valX);
|
||||||
@@ -92,7 +92,7 @@ void RenderAboutDialog(App* app, bool* p_open)
|
|||||||
|
|
||||||
ImGui::Text("%s", TR("about_daemon"));
|
ImGui::Text("%s", TR("about_daemon"));
|
||||||
ImGui::SameLine(valX);
|
ImGui::SameLine(valX);
|
||||||
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", TR("connected"));
|
ImGui::TextColored(material::SuccessVec4(), "%s", TR("connected"));
|
||||||
|
|
||||||
const auto& state = app->getWalletState();
|
const auto& state = app->getWalletState();
|
||||||
ImGui::Text("%s", TR("about_chain"));
|
ImGui::Text("%s", TR("about_chain"));
|
||||||
|
|||||||
@@ -141,6 +141,14 @@ public:
|
|||||||
double newFromBal = s_info.fromBalance - totalDeduct;
|
double newFromBal = s_info.fromBalance - totalDeduct;
|
||||||
double newToBal = s_info.toBalance + amount;
|
double newToBal = s_info.toBalance + amount;
|
||||||
bool amountValid = amount > 0 && totalDeduct <= s_info.fromBalance;
|
bool amountValid = amount > 0 && totalDeduct <= s_info.fromBalance;
|
||||||
|
// Mirror the Send tab's guards (this dialog previously enabled Confirm on amount alone, so a
|
||||||
|
// disconnected / syncing / locked wallet would fail only at submit). These are own-address
|
||||||
|
// moves, but gating is strictly more restrictive — the transfer logic below is unchanged.
|
||||||
|
const auto& ws = s_app->getWalletState();
|
||||||
|
const bool connected = s_app->isConnected();
|
||||||
|
const bool syncing = ws.sync.syncing;
|
||||||
|
const bool liteLocked = s_app->liteWallet() && ws.isLocked();
|
||||||
|
const bool canTransfer = amountValid && !s_sending && connected && !syncing && !liteLocked;
|
||||||
|
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
@@ -167,6 +175,19 @@ public:
|
|||||||
ImGui::TextWrapped("%s", TR("insufficient_funds"));
|
ImGui::TextWrapped("%s", TR("insufficient_funds"));
|
||||||
ImGui::PopStyleColor();
|
ImGui::PopStyleColor();
|
||||||
}
|
}
|
||||||
|
if (amount <= 0.0) {
|
||||||
|
ImGui::Spacing();
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Error()));
|
||||||
|
ImGui::TextWrapped("%s", TR("send_tooltip_enter_amount"));
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
} else if (amountValid && (!connected || syncing || liteLocked)) {
|
||||||
|
ImGui::Spacing();
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Warning()));
|
||||||
|
ImGui::TextWrapped("%s", !connected ? TR("send_tooltip_not_connected")
|
||||||
|
: syncing ? TR("send_tooltip_syncing")
|
||||||
|
: TR("transfer_tooltip_locked"));
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
}
|
||||||
if (amountValid && newFromBal < 1e-9) {
|
if (amountValid && newFromBal < 1e-9) {
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
// Full-balance send: same warning-icon treatment as the de-shielding header above,
|
// Full-balance send: same warning-icon treatment as the de-shielding header above,
|
||||||
@@ -204,13 +225,13 @@ public:
|
|||||||
bool outConfirm = false;
|
bool outConfirm = false;
|
||||||
bool outClose = false;
|
bool outClose = false;
|
||||||
DialogActionFooter(s_sending ? sendingLabel : confirmLabel,
|
DialogActionFooter(s_sending ? sendingLabel : confirmLabel,
|
||||||
amountValid && !s_sending,
|
canTransfer,
|
||||||
cancelLabel, outConfirm, outClose, btnW);
|
cancelLabel, outConfirm, outClose, btnW);
|
||||||
|
|
||||||
if (outClose) {
|
if (outClose) {
|
||||||
s_open = false;
|
s_open = false;
|
||||||
}
|
}
|
||||||
if (outConfirm) {
|
if (outConfirm && canTransfer) { // re-check live state (the dialog persists across frames)
|
||||||
s_sending = true;
|
s_sending = true;
|
||||||
s_app->sendTransaction(s_info.fromAddr, s_info.toAddr,
|
s_app->sendTransaction(s_info.fromAddr, s_info.toAddr,
|
||||||
amount, s_fee, "",
|
amount, s_fee, "",
|
||||||
@@ -296,12 +317,19 @@ private:
|
|||||||
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("transfer_sent_desc"));
|
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("transfer_sent_desc"));
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
{
|
{
|
||||||
char buf[128];
|
// The result is a TRANSACTION id, not an "Operation ID" — label it correctly and make
|
||||||
snprintf(buf, sizeof(buf), TR("shield_operation_id"),
|
// it copyable.
|
||||||
s_resultMsg.size() > 40
|
std::string disp = s_resultMsg.size() > 40
|
||||||
? (s_resultMsg.substr(0, 20) + "..." + s_resultMsg.substr(s_resultMsg.size() - 10)).c_str()
|
? (s_resultMsg.substr(0, 20) + "..." + s_resultMsg.substr(s_resultMsg.size() - 10))
|
||||||
: s_resultMsg.c_str());
|
: s_resultMsg;
|
||||||
|
char buf[160];
|
||||||
|
snprintf(buf, sizeof(buf), TR("transfer_txid_label"), disp.c_str());
|
||||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), buf);
|
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), buf);
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::SmallButton(TR("grpb_copy"))) {
|
||||||
|
ImGui::SetClipboardText(s_resultMsg.c_str());
|
||||||
|
Notifications::instance().info(TR("copied_to_clipboard"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
|||||||
@@ -97,6 +97,18 @@ bool FormatTotalEarnings(App* app, std::string& outText, ImU32& outCol) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Render compact hero line: logo + balance + USD + mining on one line
|
// Render compact hero line: logo + balance + USD + mining on one line
|
||||||
|
float DrawPendingChip(ImDrawList* dl, ImVec2 pos, float maxRight, ImFont* font, float fontSize) {
|
||||||
|
using namespace material;
|
||||||
|
if (s_dispUnconfirmed <= 1e-9) return 0.0f;
|
||||||
|
char buf[64];
|
||||||
|
snprintf(buf, sizeof(buf), TR("baltab_pending_chip"), s_dispUnconfirmed);
|
||||||
|
const float w = font->CalcTextSizeA(fontSize, 10000.0f, 0.0f, buf).x;
|
||||||
|
if (pos.x + w > maxRight) return 0.0f; // would overflow the card — skip rather than overlap
|
||||||
|
dl->AddText(font, fontSize, pos, WithAlpha(Warning(), 230), buf);
|
||||||
|
if (IsRectHovered(pos, ImVec2(pos.x + w, pos.y + fontSize))) Tooltip("%s", TR("baltab_pending_tip"));
|
||||||
|
return w;
|
||||||
|
}
|
||||||
|
|
||||||
void RenderCompactHero(App* app, ImDrawList* dl, float availW, float hs, float vs, float heroHeightOverride) {
|
void RenderCompactHero(App* app, ImDrawList* dl, float availW, float hs, float vs, float heroHeightOverride) {
|
||||||
using namespace material;
|
using namespace material;
|
||||||
char buf[64];
|
char buf[64];
|
||||||
@@ -136,6 +148,18 @@ void RenderCompactHero(App* app, ImDrawList* dl, float availW, float hs, float v
|
|||||||
ImGui::Dummy(ImVec2(capFont->CalcTextSizeA(tickerFontSize, 10000, 0, DRAGONX_TICKER).x + Layout::spacingLg(), lineH));
|
ImGui::Dummy(ImVec2(capFont->CalcTextSizeA(tickerFontSize, 10000, 0, DRAGONX_TICKER).x + Layout::spacingLg(), lineH));
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
|
|
||||||
|
// Pending (unconfirmed) chip — so these layouts show it too, labeled, not just the Classic card.
|
||||||
|
if (s_dispUnconfirmed > 1e-9) {
|
||||||
|
snprintf(buf, sizeof(buf), TR("baltab_pending_chip"), s_dispUnconfirmed);
|
||||||
|
ImVec2 pp = ImGui::GetCursorScreenPos();
|
||||||
|
dl->AddText(capFont, tickerFontSize, ImVec2(pp.x, tickerY), WithAlpha(Warning(), 230), buf);
|
||||||
|
float pw = capFont->CalcTextSizeA(tickerFontSize, 10000, 0, buf).x;
|
||||||
|
if (material::IsRectHovered(ImVec2(pp.x, tickerY), ImVec2(pp.x + pw, tickerY + tickerFontSize)))
|
||||||
|
material::Tooltip("%s", TR("baltab_pending_tip"));
|
||||||
|
ImGui::Dummy(ImVec2(pw + Layout::spacingLg(), lineH));
|
||||||
|
ImGui::SameLine();
|
||||||
|
}
|
||||||
|
|
||||||
// Total period-earnings (left of the USD) when the global earnings toggle is on.
|
// Total period-earnings (left of the USD) when the global earnings toggle is on.
|
||||||
{
|
{
|
||||||
std::string earnText; ImU32 earnCol;
|
std::string earnText; ImU32 earnCol;
|
||||||
@@ -1033,7 +1057,27 @@ void RenderSharedRecentTx(App* app, float recentH, float availW, float hs, float
|
|||||||
int count = std::min({4, maxRows, (int)txs.size()}); // show only the most recent (state.transactions is newest-first)
|
int count = std::min({4, maxRows, (int)txs.size()}); // show only the most recent (state.transactions is newest-first)
|
||||||
|
|
||||||
if (count == 0) {
|
if (count == 0) {
|
||||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("no_transactions_yet"));
|
const bool roomForIcon = ImGui::GetContentRegionAvail().y >= 100.0f * dp;
|
||||||
|
if (!app->isConnected()) {
|
||||||
|
// Disconnected: not "empty" and not "loading" — keep a plain caption.
|
||||||
|
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(),
|
||||||
|
TR(app->isLiteBuild() ? "lite_no_wallet" : "not_connected"));
|
||||||
|
} else {
|
||||||
|
const std::string txLoadingText = app->transactionRefreshProgressText();
|
||||||
|
// In lite, transactionRefreshProgressText() is empty and last_tx_update is stamped differently,
|
||||||
|
// so treat only an active sync as "loading" there.
|
||||||
|
const bool loadingOrSyncing = !txLoadingText.empty() ||
|
||||||
|
(state.sync.syncing && !state.sync.isSynced()) ||
|
||||||
|
(!app->isLiteBuild() && state.last_tx_update == 0);
|
||||||
|
if (loadingOrSyncing) {
|
||||||
|
char lbuf[192];
|
||||||
|
const char* base = !txLoadingText.empty() ? txLoadingText.c_str() : TR("loading_transactions");
|
||||||
|
snprintf(lbuf, sizeof(lbuf), "%s%s", base, material::LoadingDots());
|
||||||
|
material::DrawEmptyState(roomForIcon ? ICON_MD_HOURGLASS_EMPTY : nullptr, lbuf);
|
||||||
|
} else {
|
||||||
|
material::DrawEmptyState(roomForIcon ? ICON_MD_RECEIPT_LONG : nullptr, TR("no_transactions_yet"));
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||||
ImFont* capFont = Type().caption();
|
ImFont* capFont = Type().caption();
|
||||||
|
|||||||
@@ -33,6 +33,11 @@ void RenderSharedAddressList(App* app, float listH, float availW, float glassRou
|
|||||||
void RenderSharedRecentTx(App* app, float recentH, float availW, float hs, float vs);
|
void RenderSharedRecentTx(App* app, float recentH, float availW, float hs, float vs);
|
||||||
void RenderSyncBar(App* app, ImDrawList* dl, float vs);
|
void RenderSyncBar(App* app, ImDrawList* dl, float vs);
|
||||||
|
|
||||||
|
// Draws a labeled "Pending +X" chip (unconfirmed balance) at `pos`, via the draw list only, when there
|
||||||
|
// is unconfirmed balance AND it fits left of `maxRight`. Returns the width drawn (0 if skipped). Adds a
|
||||||
|
// hover tooltip. Shared by the balance layouts so pending isn't visible in only one of them.
|
||||||
|
float DrawPendingChip(ImDrawList* dl, ImVec2 pos, float maxRight, ImFont* font, float fontSize);
|
||||||
|
|
||||||
// Total period-earnings for the whole wallet, formatted per the earnings settings. Returns false
|
// Total period-earnings for the whole wallet, formatted per the earnings settings. Returns false
|
||||||
// (and leaves out* untouched) when the global earnings toggle is off, so layouts show it only when
|
// (and leaves out* untouched) when the global earnings toggle is off, so layouts show it only when
|
||||||
// the user has opted into the earnings view. Shared by RenderCompactHero and the layout totals.
|
// the user has opted into the earnings view. Shared by RenderCompactHero and the layout totals.
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include "balance_recent_tx.h"
|
#include "balance_recent_tx.h"
|
||||||
#include "../../config/version.h"
|
#include "../../config/version.h"
|
||||||
#include "../../util/text_format.h"
|
#include "../../util/text_format.h"
|
||||||
|
#include "../../util/i18n.h"
|
||||||
|
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
@@ -23,12 +24,22 @@ std::string recentTimeAgo(int64_t timestamp)
|
|||||||
{
|
{
|
||||||
return util::formatTimeAgoShort(timestamp);
|
return util::formatTimeAgoShort(timestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Translated type label (the data layer's getTypeDisplay() returns raw English). Never leak a raw RPC
|
||||||
|
// category: anything unrecognized (including "shield") falls back to a generic "Transaction".
|
||||||
|
std::string recentTypeLabel(const std::string& type)
|
||||||
|
{
|
||||||
|
if (type == "send") return TR("sent_type");
|
||||||
|
if (type == "receive") return TR("received_label");
|
||||||
|
if (type == "mined" || type == "generate" || type == "immature") return TR("mined_type");
|
||||||
|
return TR("tx_type_generic");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RecentTxDisplay buildRecentTxDisplay(const TransactionInfo& tx, int addressMaxLen)
|
RecentTxDisplay buildRecentTxDisplay(const TransactionInfo& tx, int addressMaxLen)
|
||||||
{
|
{
|
||||||
return {
|
return {
|
||||||
tx.getTypeDisplay(),
|
recentTypeLabel(tx.type),
|
||||||
util::truncateMiddle(tx.address, addressMaxLen),
|
util::truncateMiddle(tx.address, addressMaxLen),
|
||||||
formatRecentAmount(tx.type, tx.amount),
|
formatRecentAmount(tx.type, tx.amount),
|
||||||
recentTimeAgo(tx.timestamp)
|
recentTimeAgo(tx.timestamp)
|
||||||
|
|||||||
@@ -525,9 +525,10 @@ static void RenderBalanceClassic(App* app)
|
|||||||
WithAlpha(Success(), 160), buf);
|
WithAlpha(Success(), 160), buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unconfirmed badge (top-right corner)
|
// Pending badge (top-right corner) — labeled so an unconfirmed "+X" doesn't read as a mystery.
|
||||||
|
bool pendingChipHovered = false;
|
||||||
if (state.unconfirmed_balance > 0.0) {
|
if (state.unconfirmed_balance > 0.0) {
|
||||||
snprintf(buf, sizeof(buf), "+%.4f", state.unconfirmed_balance);
|
snprintf(buf, sizeof(buf), TR("baltab_pending_chip"), state.unconfirmed_balance);
|
||||||
ImVec2 ts = capFont->CalcTextSizeA(
|
ImVec2 ts = capFont->CalcTextSizeA(
|
||||||
capFont->LegacySize, 10000, 0, buf);
|
capFont->LegacySize, 10000, 0, buf);
|
||||||
float bp = S.drawElement("tabs.balance.classic", "unconfirmed-badge-padding").sizeOr(4.0f) * dp;
|
float bp = S.drawElement("tabs.balance.classic", "unconfirmed-badge-padding").sizeOr(4.0f) * dp;
|
||||||
@@ -540,6 +541,7 @@ static void RenderBalanceClassic(App* app)
|
|||||||
dl->AddText(capFont, capFont->LegacySize,
|
dl->AddText(capFont, capFont->LegacySize,
|
||||||
ImVec2(bMin.x + bp, bMin.y + bp * 0.5f),
|
ImVec2(bMin.x + bp, bMin.y + bp * 0.5f),
|
||||||
amberCol, buf);
|
amberCol, buf);
|
||||||
|
if (material::IsRectHovered(bMin, bMax)) { pendingChipHovered = true; material::Tooltip("%s", TR("baltab_pending_tip")); }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hover glow + click to Receive
|
// Hover glow + click to Receive
|
||||||
@@ -547,6 +549,7 @@ static void RenderBalanceClassic(App* app)
|
|||||||
dl->AddRect(cMin, cMax, IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance", "hover-glow-alpha").sizeOr(40.0f)),
|
dl->AddRect(cMin, cMax, IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance", "hover-glow-alpha").sizeOr(40.0f)),
|
||||||
cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f) * dp);
|
cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f) * dp);
|
||||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
|
if (!pendingChipHovered) material::Tooltip("%s", TR("shielded_card_tooltip"));
|
||||||
if (ImGui::IsMouseClicked(0))
|
if (ImGui::IsMouseClicked(0))
|
||||||
app->setCurrentPage(NavPage::Receive);
|
app->setCurrentPage(NavPage::Receive);
|
||||||
}
|
}
|
||||||
@@ -747,6 +750,11 @@ static void RenderBalanceDonut(App* app) {
|
|||||||
dl->AddText(capFont, capFont->LegacySize,
|
dl->AddText(capFont, capFont->LegacySize,
|
||||||
ImVec2(ImGui::GetCursorScreenPos().x, tickerY),
|
ImVec2(ImGui::GetCursorScreenPos().x, tickerY),
|
||||||
OnSurfaceMedium(), DRAGONX_TICKER);
|
OnSurfaceMedium(), DRAGONX_TICKER);
|
||||||
|
DrawPendingChip(dl, ImVec2(ImGui::GetCursorScreenPos().x
|
||||||
|
+ capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, DRAGONX_TICKER).x
|
||||||
|
+ Layout::spacingMd(), tickerY),
|
||||||
|
ImGui::GetCursorScreenPos().x + ImGui::GetContentRegionAvail().x,
|
||||||
|
capFont, capFont->LegacySize);
|
||||||
ImGui::NewLine();
|
ImGui::NewLine();
|
||||||
|
|
||||||
// USD value
|
// USD value
|
||||||
@@ -1662,6 +1670,11 @@ static void RenderBalanceShield(App* app) {
|
|||||||
dl->AddText(capFont, capFont->LegacySize,
|
dl->AddText(capFont, capFont->LegacySize,
|
||||||
ImVec2(ImGui::GetCursorScreenPos().x, pos.y + heroSz.y - capFont->LegacySize),
|
ImVec2(ImGui::GetCursorScreenPos().x, pos.y + heroSz.y - capFont->LegacySize),
|
||||||
OnSurfaceMedium(), DRAGONX_TICKER);
|
OnSurfaceMedium(), DRAGONX_TICKER);
|
||||||
|
DrawPendingChip(dl, ImVec2(ImGui::GetCursorScreenPos().x
|
||||||
|
+ capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, DRAGONX_TICKER).x
|
||||||
|
+ Layout::spacingMd(), pos.y + heroSz.y - capFont->LegacySize),
|
||||||
|
ImGui::GetCursorScreenPos().x + ImGui::GetContentRegionAvail().x,
|
||||||
|
capFont, capFont->LegacySize);
|
||||||
ImGui::NewLine();
|
ImGui::NewLine();
|
||||||
|
|
||||||
double usd_value = state.getBalanceUSD();
|
double usd_value = state.getBalanceUSD();
|
||||||
@@ -1851,6 +1864,11 @@ static void RenderBalanceTimeline(App* app) {
|
|||||||
dl->AddText(capFont, capFont->LegacySize,
|
dl->AddText(capFont, capFont->LegacySize,
|
||||||
ImVec2(ImGui::GetCursorScreenPos().x, pos.y + heroSz.y - capFont->LegacySize),
|
ImVec2(ImGui::GetCursorScreenPos().x, pos.y + heroSz.y - capFont->LegacySize),
|
||||||
OnSurfaceMedium(), DRAGONX_TICKER);
|
OnSurfaceMedium(), DRAGONX_TICKER);
|
||||||
|
DrawPendingChip(dl, ImVec2(ImGui::GetCursorScreenPos().x
|
||||||
|
+ capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, DRAGONX_TICKER).x
|
||||||
|
+ Layout::spacingMd(), pos.y + heroSz.y - capFont->LegacySize),
|
||||||
|
ImGui::GetCursorScreenPos().x + ImGui::GetContentRegionAvail().x,
|
||||||
|
capFont, capFont->LegacySize);
|
||||||
ImGui::NewLine();
|
ImGui::NewLine();
|
||||||
|
|
||||||
double usd_value = state.getBalanceUSD();
|
double usd_value = state.getBalanceUSD();
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ public:
|
|||||||
s_state = State::Confirm;
|
s_state = State::Confirm;
|
||||||
s_bootstrap.reset();
|
s_bootstrap.reset();
|
||||||
s_errorMsg.clear();
|
s_errorMsg.clear();
|
||||||
|
s_lastUrl.clear();
|
||||||
s_wasDaemonRunning = false;
|
s_wasDaemonRunning = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,13 +283,17 @@ private:
|
|||||||
|
|
||||||
Type().textColored(TypeStyle::H6, Success(), TR("bootstrap_success"));
|
Type().textColored(TypeStyle::H6, Success(), TR("bootstrap_success"));
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::TextWrapped("%s", TR("bootstrap_success_desc"));
|
// For the embedded daemon, tell the user the node will start (and offer the button) instead of
|
||||||
|
// instructing them to start it by hand. External-daemon users keep the manual-start wording.
|
||||||
|
const bool embedded = s_app->isUsingEmbeddedDaemon();
|
||||||
|
ImGui::TextWrapped("%s", TR(embedded ? "bootstrap_success_desc_start" : "bootstrap_success_desc"));
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
|
||||||
float btnW = 140.0f * dp;
|
float btnW = 150.0f * dp;
|
||||||
if (s_wasDaemonRunning) {
|
if (s_wasDaemonRunning || embedded) {
|
||||||
if (TactileButton(TR("bootstrap_restart_daemon"), ImVec2(btnW, 0))) {
|
if (TactileButton(s_wasDaemonRunning ? TR("bootstrap_restart_daemon") : TR("bootstrap_start_daemon"),
|
||||||
|
ImVec2(btnW, 0))) {
|
||||||
s_app->setBootstrapDownloading(false);
|
s_app->setBootstrapDownloading(false);
|
||||||
s_app->startEmbeddedDaemon();
|
s_app->startEmbeddedDaemon();
|
||||||
s_open = false;
|
s_open = false;
|
||||||
@@ -314,9 +319,15 @@ private:
|
|||||||
|
|
||||||
float btnW = 120.0f * dp;
|
float btnW = 120.0f * dp;
|
||||||
if (TactileButton(TR("retry"), ImVec2(btnW, 0))) {
|
if (TactileButton(TR("retry"), ImVec2(btnW, 0))) {
|
||||||
startDownload("");
|
startDownload(s_lastUrl); // keep the user's source/mirror choice (was startDownload(""))
|
||||||
}
|
}
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
|
{
|
||||||
|
const std::string mirrorUrl = std::string(util::Bootstrap::kMirrorUrl) + "/" + util::Bootstrap::kZipName;
|
||||||
|
if (s_lastUrl != mirrorUrl && TactileButton(TR("bootstrap_try_mirror"), ImVec2(130.0f * dp, 0)))
|
||||||
|
startDownload(mirrorUrl);
|
||||||
|
ImGui::SameLine();
|
||||||
|
}
|
||||||
if (TactileButton(TR("close"), ImVec2(90.0f * dp, 0))) {
|
if (TactileButton(TR("close"), ImVec2(90.0f * dp, 0))) {
|
||||||
s_app->setBootstrapDownloading(false);
|
s_app->setBootstrapDownloading(false);
|
||||||
s_open = false;
|
s_open = false;
|
||||||
@@ -336,6 +347,7 @@ private:
|
|||||||
s_bootstrap->start(dataDir);
|
s_bootstrap->start(dataDir);
|
||||||
else
|
else
|
||||||
s_bootstrap->start(dataDir, url);
|
s_bootstrap->start(dataDir, url);
|
||||||
|
s_lastUrl = url; // remember what we started (set only after the guards passed)
|
||||||
s_state = State::Downloading;
|
s_state = State::Downloading;
|
||||||
s_errorMsg.clear();
|
s_errorMsg.clear();
|
||||||
s_cancelling = false;
|
s_cancelling = false;
|
||||||
@@ -347,6 +359,7 @@ private:
|
|||||||
static inline std::unique_ptr<util::Bootstrap> s_bootstrap;
|
static inline std::unique_ptr<util::Bootstrap> s_bootstrap;
|
||||||
static inline bool s_wasDaemonRunning = false;
|
static inline bool s_wasDaemonRunning = false;
|
||||||
static inline bool s_cancelling = false; // cancel requested; disables the Cancel button until the worker stops
|
static inline bool s_cancelling = false; // cancel requested; disables the Cancel button until the worker stops
|
||||||
|
static inline std::string s_lastUrl; // last source started ("" = default); so Retry keeps the mirror choice
|
||||||
static inline std::string s_errorMsg;
|
static inline std::string s_errorMsg;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+694
-35
@@ -63,15 +63,19 @@ int s_msgsel_head = 0; // drag-current byte offset
|
|||||||
bool s_msgsel_dragging = false;
|
bool s_msgsel_dragging = false;
|
||||||
|
|
||||||
// Composer + new-conversation UI state.
|
// Composer + new-conversation UI state.
|
||||||
char s_compose[512] = "";
|
char s_compose[16400] = ""; // must be >= kChatBodyMaxBytes + 1 (static_assert'd below)
|
||||||
std::string s_compose_cid; // the conversation s_compose is a draft for; draft is wiped when it changes
|
std::string s_compose_cid; // the conversation s_compose is a draft for; draft is wiped when it changes
|
||||||
// Live byte offset of the composer's text caret, kept in sync by composeInputCallback while the composer
|
// Live byte offset of the composer's text caret, kept in sync by composeInputCallback while the composer
|
||||||
// is active (the callback only fires then). The emoji picker uses it to splice a glyph at the cursor
|
// is active (the callback only fires then). The emoji picker uses it to splice a glyph at the cursor
|
||||||
// instead of always appending. -1 = unknown/never-focused => append at the end.
|
// instead of always appending. -1 = unknown/never-focused => append at the end.
|
||||||
int s_composeCursor = -1;
|
int s_composeCursor = -1;
|
||||||
// On-chain chat body cap in bytes = (512 − len("utf8:"))/2 − secretstream ABYTES (see chat_outgoing.cpp).
|
// On-chain chat body cap in bytes. Larger messages split the ciphertext across many memo outputs: tier 2
|
||||||
// The composer hard-caps input to this; the emoji picker respects it too.
|
// packs up to kHushChatMaxMessageParts (16) into ONE transaction (~4 KB), and tier 3 spills further across
|
||||||
constexpr int kChatBodyMaxBytes = (512 - 5) / 2 - 17; // = 236
|
// several transactions up to kHushChatMaxMessageChunks (64) chunks total (protocol max ~16303: 64 × 255
|
||||||
|
// cipher bytes − 17 secretstream tag). Kept a touch under that so the composer never produces a body that
|
||||||
|
// compose would reject as TooLong. The composer hard-caps input here; the emoji picker too.
|
||||||
|
constexpr int kChatBodyMaxBytes = 16000;
|
||||||
|
static_assert(sizeof(s_compose) >= kChatBodyMaxBytes + 1, "compose buffer too small for the body cap");
|
||||||
float s_composerAnimH = 0.0f; // animated composer-box height — grows with newlines, pushing the thread up
|
float s_composerAnimH = 0.0f; // animated composer-box height — grows with newlines, pushing the thread up
|
||||||
float s_composerTargetH = 0.0f; // target height measured in the composer block (wrapped content), consumed next frame
|
float s_composerTargetH = 0.0f; // target height measured in the composer block (wrapped content), consumed next frame
|
||||||
|
|
||||||
@@ -96,12 +100,35 @@ int composeInputCallback(ImGuiInputTextCallbackData* data) {
|
|||||||
bool s_show_new_convo = false;
|
bool s_show_new_convo = false;
|
||||||
char s_new_zaddr[128] = "";
|
char s_new_zaddr[128] = "";
|
||||||
char s_new_msg[256] = "";
|
char s_new_msg[256] = "";
|
||||||
|
char s_pending_new_zaddr[128] = ""; // cross-tab request from Contacts' "Message" action
|
||||||
|
bool s_pending_new_set = false;
|
||||||
char s_search[80] = ""; // conversation-list filter (Q8)
|
char s_search[80] = ""; // conversation-list filter (Q8)
|
||||||
bool s_show_hidden = false; // when on, the list also shows hidden conversations (with an Unhide action)
|
bool s_show_hidden = false; // when on, the list also shows hidden conversations (with an Unhide action)
|
||||||
bool s_show_delete_confirm = false; // "Delete conversation?" confirm overlay (revive vs block)
|
bool s_show_delete_confirm = false; // "Delete conversation?" confirm overlay (revive vs block)
|
||||||
std::string s_delete_cid; // conversation targeted by the delete confirm
|
std::string s_delete_cid; // conversation targeted by the delete confirm
|
||||||
std::string s_delete_name; // its peer name (for the confirm copy / block-list label)
|
std::string s_delete_name; // its peer name (for the confirm copy / block-list label)
|
||||||
bool s_show_blocked = false; // blocked-conversations manager overlay (unblock)
|
bool s_show_blocked = false; // blocked-conversations manager overlay (unblock)
|
||||||
|
bool s_show_restore = false; // restore-conversations overlay (reverse an on-chain delete)
|
||||||
|
char s_restore_addr[128] = ""; // paste-a-chat-address buffer in the restore overlay
|
||||||
|
bool s_show_sensitive = false; // sensitive mark/unmark/setup/unlock overlay
|
||||||
|
std::string s_sensitive_cid; // target conversation (empty => "unlock the vault only", from the opener)
|
||||||
|
std::string s_sensitive_name; // peer display name (for the label)
|
||||||
|
std::string s_sensitive_peeraddr; // peer z-address (carried as the control label, for restore-by-address parity)
|
||||||
|
bool s_sensitive_unmark = false; // true => the dialog is UNmarking a currently-sensitive conversation
|
||||||
|
char s_sensitive_pass[128] = ""; // passphrase (setup / unlock)
|
||||||
|
char s_sensitive_pass2[128] = ""; // confirm passphrase (setup only)
|
||||||
|
bool s_show_export_confirm = false; // "export as plaintext" confirm overlay
|
||||||
|
std::string s_export_cid; // conversation targeted by the export confirm
|
||||||
|
bool s_show_fingerprint = false; // key-fingerprint verification dialog
|
||||||
|
std::string s_fingerprint_cid; // conversation targeted by the fingerprint dialog
|
||||||
|
|
||||||
|
// Export file path for a conversation (the same scheme the inline export used).
|
||||||
|
static std::string chatExportPathFor(const std::string& peerName) {
|
||||||
|
std::string safe;
|
||||||
|
for (char ch : peerName) safe += (std::isalnum(static_cast<unsigned char>(ch)) ? ch : '_');
|
||||||
|
if (safe.empty()) safe = "chat";
|
||||||
|
return util::Platform::getConfigDir() + "/dragonx-chat-" + safe + ".txt";
|
||||||
|
}
|
||||||
bool s_show_emoji_picker = false; // emoji picker overlay — fills the conversation-list pane while open
|
bool s_show_emoji_picker = false; // emoji picker overlay — fills the conversation-list pane while open
|
||||||
char s_emoji_search[48] = ""; // emoji picker keyword filter
|
char s_emoji_search[48] = ""; // emoji picker keyword filter
|
||||||
|
|
||||||
@@ -535,6 +562,7 @@ struct ConvSummary {
|
|||||||
std::int64_t lastTs = 0;
|
std::int64_t lastTs = 0;
|
||||||
int count = 0;
|
int count = 0;
|
||||||
bool hidden = false; // shown only while "Show hidden" is on
|
bool hidden = false; // shown only while "Show hidden" is on
|
||||||
|
bool pendingRequest = false; // unsolicited incoming contact request from a stranger: awaiting Accept/Block
|
||||||
};
|
};
|
||||||
|
|
||||||
// Per-frame memoization of the conversation-list build and the open-thread message list (both otherwise
|
// Per-frame memoization of the conversation-list build and the open-thread message list (both otherwise
|
||||||
@@ -576,6 +604,17 @@ void centeredHint(const char* text) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Centered empty state: big muted icon + title + optional wrapped hint (V4).
|
// Centered empty state: big muted icon + title + optional wrapped hint (V4).
|
||||||
|
// Copy the user's own chat address to the clipboard (shared by the sidebar button and the intro card).
|
||||||
|
void copyMyChatAddress(App* app) {
|
||||||
|
std::string myAddr = app->chatMyAddress();
|
||||||
|
if (myAddr.empty()) {
|
||||||
|
Notifications::instance().warning(TR("chat_no_my_address"));
|
||||||
|
} else {
|
||||||
|
ImGui::SetClipboardText(myAddr.c_str());
|
||||||
|
Notifications::instance().info(TR("address_copied"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void centeredEmptyState(const char* icon, const char* title, const char* hint) {
|
void centeredEmptyState(const char* icon, const char* title, const char* hint) {
|
||||||
const ImVec2 avail = ImGui::GetContentRegionAvail();
|
const ImVec2 avail = ImGui::GetContentRegionAvail();
|
||||||
const ImVec2 origin = ImGui::GetCursorPos();
|
const ImVec2 origin = ImGui::GetCursorPos();
|
||||||
@@ -766,6 +805,17 @@ void RenderChatTab(App* app)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Consume a cross-tab "Message this contact" request (from Contacts): open the New-conversation
|
||||||
|
// dialog prefilled with the z-address. Runs every frame the tab renders, so it fires right after the
|
||||||
|
// Contacts tab switches us here.
|
||||||
|
if (s_pending_new_set) {
|
||||||
|
s_pending_new_set = false;
|
||||||
|
s_show_new_convo = true;
|
||||||
|
std::memcpy(s_new_zaddr, s_pending_new_zaddr, sizeof(s_new_zaddr));
|
||||||
|
s_new_msg[0] = '\0';
|
||||||
|
sodium_memzero(s_pending_new_zaddr, sizeof(s_pending_new_zaddr));
|
||||||
|
}
|
||||||
|
|
||||||
// Resolve the Chat-tab clock once per frame: the chat override wins, else the global format.
|
// Resolve the Chat-tab clock once per frame: the chat override wins, else the global format.
|
||||||
if (auto* cst = app->settings()) {
|
if (auto* cst = app->settings()) {
|
||||||
const int ctf = cst->getChatTimeFormat(); // 0=follow global, 1=24h, 2=12h
|
const int ctf = cst->getChatTimeFormat(); // 0=follow global, 1=24h, 2=12h
|
||||||
@@ -795,9 +845,11 @@ void RenderChatTab(App* app)
|
|||||||
c.cid = cid;
|
c.cid = cid;
|
||||||
c.hidden = hidden;
|
c.hidden = hidden;
|
||||||
c.count = static_cast<int>(messages.size());
|
c.count = static_cast<int>(messages.size());
|
||||||
|
bool sentAny = false;
|
||||||
for (const auto& m : messages) { // pin to the EARLIEST (establishing) peer z-addr / key (B2 — the
|
for (const auto& m : messages) { // pin to the EARLIEST (establishing) peer z-addr / key (B2 — the
|
||||||
if (c.peerZaddr.empty() && !m.peer_zaddr.empty()) c.peerZaddr = m.peer_zaddr; // memo header rides
|
if (c.peerZaddr.empty() && !m.peer_zaddr.empty()) c.peerZaddr = m.peer_zaddr; // memo header rides
|
||||||
if (c.peerPubKey.empty() && !m.peer_public_key_hex.empty()) c.peerPubKey = m.peer_public_key_hex; // outside the AEAD)
|
if (c.peerPubKey.empty() && !m.peer_public_key_hex.empty()) c.peerPubKey = m.peer_public_key_hex; // outside the AEAD)
|
||||||
|
if (m.direction == chat::ChatDirection::Outgoing) sentAny = true;
|
||||||
}
|
}
|
||||||
const auto& last = messages.back();
|
const auto& last = messages.back();
|
||||||
c.lastBody = last.body;
|
c.lastBody = last.body;
|
||||||
@@ -805,6 +857,12 @@ void RenderChatTab(App* app)
|
|||||||
const int idx = c.peerZaddr.empty() ? -1 : book.findByAddress(c.peerZaddr);
|
const int idx = c.peerZaddr.empty() ? -1 : book.findByAddress(c.peerZaddr);
|
||||||
c.peerName = (idx >= 0) ? book.entries()[idx].label
|
c.peerName = (idx >= 0) ? book.entries()[idx].label
|
||||||
: shorten(!c.peerZaddr.empty() ? c.peerZaddr : cid);
|
: shorten(!c.peerZaddr.empty() ? c.peerZaddr : cid);
|
||||||
|
// Unsolicited: first message is an incoming contact request, we've never replied, and the peer
|
||||||
|
// isn't a saved contact → show an Accept/Block card instead of treating it as a normal chat.
|
||||||
|
const auto& first = messages.front();
|
||||||
|
c.pendingRequest = (idx < 0) && !sentAny && !c.peerZaddr.empty() &&
|
||||||
|
first.direction == chat::ChatDirection::Incoming &&
|
||||||
|
first.kind == chat::ChatMessageKind::ContactRequest;
|
||||||
s_convs.push_back(std::move(c));
|
s_convs.push_back(std::move(c));
|
||||||
}
|
}
|
||||||
std::sort(s_convs.begin(), s_convs.end(),
|
std::sort(s_convs.begin(), s_convs.end(),
|
||||||
@@ -916,6 +974,57 @@ void RenderChatTab(App* app)
|
|||||||
if (ImGui::SmallButton(bl.c_str())) s_show_blocked = true;
|
if (ImGui::SmallButton(bl.c_str())) s_show_blocked = true;
|
||||||
ImGui::PopStyleColor();
|
ImGui::PopStyleColor();
|
||||||
}
|
}
|
||||||
|
// Restore opener — reverse an on-chain "Delete everywhere". Shown when a delete is known from chain
|
||||||
|
// (works on a fresh seed import, which has no local state) or something is locally blocked; the
|
||||||
|
// dialog also offers paste-by-address + a quick scan for deletes made on another device.
|
||||||
|
const int onChainDelCount = (int)app->chatService().onChainDeletedConversations().size();
|
||||||
|
if (onChainDelCount > 0 || blockedCount > 0) {
|
||||||
|
if (blockedCount > 0) ImGui::SameLine();
|
||||||
|
const std::string rl = onChainDelCount > 0
|
||||||
|
? std::string(TR("chat_restore_manage")) + " (" + std::to_string(onChainDelCount) + ")"
|
||||||
|
: std::string(TR("chat_restore_manage"));
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, material::OnSurfaceMedium());
|
||||||
|
if (ImGui::SmallButton(rl.c_str())) s_show_restore = true;
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
}
|
||||||
|
// Sensitive-vault opener: lock / unlock the local key for sensitive conversations. Shown only once
|
||||||
|
// a vault exists (it's created on the first "Make sensitive"). While locked it shows the count of
|
||||||
|
// sensitive conversations currently withheld.
|
||||||
|
{
|
||||||
|
const auto vs = app->chatSensitiveVaultState();
|
||||||
|
if (vs != App::ChatVaultState::NotConfigured) {
|
||||||
|
if (blockedCount > 0 || onChainDelCount > 0) ImGui::SameLine();
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, material::OnSurfaceMedium());
|
||||||
|
if (vs == App::ChatVaultState::Unlocked) {
|
||||||
|
if (ImGui::SmallButton(TR("chat_sensitive_lock_btn"))) {
|
||||||
|
app->lockChatSensitiveVault();
|
||||||
|
s_convsKey = ~0ull; // sensitive threads vanish — rebuild the list
|
||||||
|
}
|
||||||
|
} else { // Locked
|
||||||
|
const int sn = app->settings() ? (int)app->settings()->sensitiveChatConversations().size() : 0;
|
||||||
|
const std::string ul = sn > 0
|
||||||
|
? std::string(TR("chat_sensitive_unlock_btn")) + " (" + std::to_string(sn) + ")"
|
||||||
|
: std::string(TR("chat_sensitive_unlock_btn"));
|
||||||
|
if (ImGui::SmallButton(ul.c_str())) {
|
||||||
|
s_sensitive_cid.clear(); s_sensitive_name.clear(); s_sensitive_peeraddr.clear();
|
||||||
|
s_sensitive_unmark = false;
|
||||||
|
s_sensitive_pass[0] = '\0'; s_sensitive_pass2[0] = '\0';
|
||||||
|
s_show_sensitive = true; // empty cid => unlock-only
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// "Copy my chat address" — the address others use to reach this user. The conversation header
|
||||||
|
// only copies a PEER's address, so without this there's no way to hand out your own. Copies on
|
||||||
|
// click only (chatMyAddress may persist a freshly chosen address).
|
||||||
|
{
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, material::OnSurfaceMedium());
|
||||||
|
// Text-only: SmallButton uses the body font, which has no Material icon glyph (would show "?").
|
||||||
|
if (ImGui::SmallButton(TR("chat_copy_my_address"))) copyMyChatAddress(app);
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("chat_my_address_tip"));
|
||||||
|
}
|
||||||
const std::string search = s_search;
|
const std::string search = s_search;
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
if (convs.empty()) {
|
if (convs.empty()) {
|
||||||
@@ -982,9 +1091,21 @@ void RenderChatTab(App* app)
|
|||||||
dl->AddText(nameFont, nameSz, ImVec2(textX, p.y + pad),
|
dl->AddText(nameFont, nameSz, ImVec2(textX, p.y + pad),
|
||||||
c.hidden ? material::OnSurfaceMedium() : material::OnSurface(), c.peerName.c_str());
|
c.hidden ? material::OnSurfaceMedium() : material::OnSurface(), c.peerName.c_str());
|
||||||
dl->PopClipRect();
|
dl->PopClipRect();
|
||||||
|
// Pending-request pill (bottom-right) for an unsolicited stranger; the preview is clipped to its left.
|
||||||
|
float previewRight = mx.x - pad;
|
||||||
|
if (c.pendingRequest) {
|
||||||
|
const char* rl = TR("chat_request_pill");
|
||||||
|
const ImVec2 rs = metaFont->CalcTextSizeA(metaSz, FLT_MAX, 0.0f, rl);
|
||||||
|
const float px = 6.0f * dp, py = 1.0f * dp;
|
||||||
|
const ImVec2 pmax(mx.x - pad, p.y + rowH - pad + py);
|
||||||
|
const ImVec2 pmin(pmax.x - rs.x - 2.0f * px, pmax.y - rs.y - 2.0f * py);
|
||||||
|
dl->AddRectFilled(pmin, pmax, material::WithAlpha(material::Warning(), 60), (rs.y + 2.0f * py) * 0.5f);
|
||||||
|
dl->AddText(metaFont, metaSz, ImVec2(pmin.x + px, pmin.y + py), material::Warning(), rl);
|
||||||
|
previewRight = pmin.x - pad;
|
||||||
|
}
|
||||||
// Preview (bottom, clipped to the text column, muted).
|
// Preview (bottom, clipped to the text column, muted).
|
||||||
const std::string preview = previewOf(c.lastBody);
|
const std::string preview = previewOf(c.lastBody);
|
||||||
dl->PushClipRect(ImVec2(textX, p.y), ImVec2(mx.x - pad, mx.y), true);
|
dl->PushClipRect(ImVec2(textX, p.y), ImVec2(std::max(textX, previewRight), mx.y), true);
|
||||||
dl->AddText(metaFont, metaSz, ImVec2(textX, p.y + rowH - pad - metaSz),
|
dl->AddText(metaFont, metaSz, ImVec2(textX, p.y + rowH - pad - metaSz),
|
||||||
material::OnSurfaceMedium(), preview.c_str());
|
material::OnSurfaceMedium(), preview.c_str());
|
||||||
dl->PopClipRect();
|
dl->PopClipRect();
|
||||||
@@ -1089,7 +1210,7 @@ void RenderChatTab(App* app)
|
|||||||
// The toolbar's left edge is known up front (from the button count). A rename (edit) icon is
|
// The toolbar's left edge is known up front (from the button count). A rename (edit) icon is
|
||||||
// shown whenever there's an address to save the contact under; the settings "notch" gear is
|
// shown whenever there's an address to save the contact under; the settings "notch" gear is
|
||||||
// always the rightmost icon.
|
// always the rightmost icon.
|
||||||
const int nBtns = 5 + (hasAddr ? 1 : 0); // export, mute, hide, delete, settings (+rename)
|
const int nBtns = 6 + (hasAddr ? 1 : 0); // export, mute, hide, sensitive, delete, settings (+rename)
|
||||||
const float toolbarLeft = rightX - (nBtns * ib + (nBtns - 1) * gap);
|
const float toolbarLeft = rightX - (nBtns * ib + (nBtns - 1) * gap);
|
||||||
|
|
||||||
// Compact address + lock (or waiting-chip) metrics, reserved to the right of the name.
|
// Compact address + lock (or waiting-chip) metrics, reserved to the right of the name.
|
||||||
@@ -1189,23 +1310,9 @@ void RenderChatTab(App* app)
|
|||||||
ImGui::SetCursorScreenPos(ImVec2(bx, by));
|
ImGui::SetCursorScreenPos(ImVec2(bx, by));
|
||||||
material::IconButtonStyle a = base; a.tooltip = TR("chat_export_warn");
|
material::IconButtonStyle a = base; a.tooltip = TR("chat_export_warn");
|
||||||
if (material::IconButton("##hdr_export", ICON_MD_FILE_DOWNLOAD, ifont, ImVec2(ib, ib), a)) {
|
if (material::IconButton("##hdr_export", ICON_MD_FILE_DOWNLOAD, ifont, ImVec2(ib, ib), a)) {
|
||||||
std::string content = "DragonX chat export\n";
|
// The export writes DECRYPTED plaintext — confirm first instead of writing on one click.
|
||||||
content += sel->peerName + " <" + sel->peerZaddr + ">\n";
|
s_export_cid = sel->cid;
|
||||||
content += "cid: " + sel->cid + "\n\n";
|
s_show_export_confirm = true;
|
||||||
for (const auto& m : store.conversation(sel->cid)) {
|
|
||||||
const bool out = (m.direction == chat::ChatDirection::Outgoing);
|
|
||||||
content += "[" + formatTime(m.timestamp) + "] " +
|
|
||||||
(out ? std::string(TR("chat_you")) : sel->peerName) + ": " + m.body + "\n";
|
|
||||||
}
|
|
||||||
std::string safe;
|
|
||||||
for (char ch : sel->peerName)
|
|
||||||
safe += (std::isalnum(static_cast<unsigned char>(ch)) ? ch : '_');
|
|
||||||
if (safe.empty()) safe = "chat";
|
|
||||||
const std::string path = util::Platform::getConfigDir() + "/dragonx-chat-" + safe + ".txt";
|
|
||||||
if (util::Platform::writeFileAtomically(path, content, /*restrictPermissions=*/true))
|
|
||||||
Notifications::instance().success(std::string(TR("chat_export_done")) + ": " + path);
|
|
||||||
else
|
|
||||||
Notifications::instance().error(TR("chat_export_failed"));
|
|
||||||
}
|
}
|
||||||
bx += ib + gap;
|
bx += ib + gap;
|
||||||
}
|
}
|
||||||
@@ -1246,6 +1353,24 @@ void RenderChatTab(App* app)
|
|||||||
}
|
}
|
||||||
bx += ib + gap;
|
bx += ib + gap;
|
||||||
}
|
}
|
||||||
|
// Sensitive — per-conversation local-key encryption. Opens a dialog that (by vault state)
|
||||||
|
// sets up / unlocks the vault and marks or unmarks this conversation.
|
||||||
|
{
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(bx, by));
|
||||||
|
material::IconButtonStyle a = base;
|
||||||
|
const bool isSens = app->chatService().isConversationSensitive(sel->cid);
|
||||||
|
a.tooltip = isSens ? TR("chat_sensitive_unmark_tip") : TR("chat_sensitive_mark_tip");
|
||||||
|
if (material::IconButton("##hdr_sensitive",
|
||||||
|
isSens ? ICON_MD_LOCK : ICON_MD_ENHANCED_ENCRYPTION, ifont, ImVec2(ib, ib), a)) {
|
||||||
|
s_sensitive_cid = sel->cid;
|
||||||
|
s_sensitive_name = sel->peerName;
|
||||||
|
s_sensitive_peeraddr = sel->peerZaddr;
|
||||||
|
s_sensitive_unmark = isSens;
|
||||||
|
s_sensitive_pass[0] = '\0'; s_sensitive_pass2[0] = '\0';
|
||||||
|
s_show_sensitive = true;
|
||||||
|
}
|
||||||
|
bx += ib + gap;
|
||||||
|
}
|
||||||
// Delete — clears this conversation's LOCAL history. Destructive (and offers a
|
// Delete — clears this conversation's LOCAL history. Destructive (and offers a
|
||||||
// "delete & block" variant), so it opens a confirm dialog rather than acting inline.
|
// "delete & block" variant), so it opens a confirm dialog rather than acting inline.
|
||||||
{
|
{
|
||||||
@@ -1276,13 +1401,22 @@ void RenderChatTab(App* app)
|
|||||||
// same row. Hidden while renaming (the field takes the whole span).
|
// same row. Hidden while renaming (the field takes the whole span).
|
||||||
if (hasAddr && !renaming) {
|
if (hasAddr && !renaming) {
|
||||||
float ax = textX + std::min(nameW, nameAvail) + nameSpacing;
|
float ax = textX + std::min(nameW, nameAvail) + nameSpacing;
|
||||||
// Key-verify lock — we hold the peer's identity key; hover shows a comparable fingerprint.
|
// Key-verify lock — we hold the peer's identity key; now clickable to open a dialog with the
|
||||||
|
// fingerprint for out-of-band comparison (the old hover-only lock was unexplained).
|
||||||
if (showLock) {
|
if (showLock) {
|
||||||
const float ly = hp.y + (rowH - icoSz) * 0.5f;
|
const float ly = hp.y + (rowH - icoSz) * 0.5f;
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(ax, ly));
|
||||||
|
const bool lockClicked = ImGui::InvisibleButton("##hdr_keylock",
|
||||||
|
ImVec2(std::max(1.0f, lockW - 5.0f * hdpi), icoSz));
|
||||||
|
const bool lockHov = ImGui::IsItemHovered();
|
||||||
hdl->AddText(icoFont, icoSz, ImVec2(ax, ly),
|
hdl->AddText(icoFont, icoSz, ImVec2(ax, ly),
|
||||||
material::WithAlpha(material::Primary(), 220), ICON_MD_LOCK);
|
lockHov ? material::Primary() : material::WithAlpha(material::Primary(), 220),
|
||||||
if (ImGui::IsMouseHoveringRect(ImVec2(ax, ly), ImVec2(ax + lockW, ly + icoSz)))
|
ICON_MD_LOCK);
|
||||||
material::Tooltip("%s\n%s", TR("chat_verify_key"), keyFingerprint(sel->peerPubKey).c_str());
|
if (lockHov) {
|
||||||
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
|
material::Tooltip("%s", TR("chat_verify_tip"));
|
||||||
|
}
|
||||||
|
if (lockClicked) { s_fingerprint_cid = sel->cid; s_show_fingerprint = true; }
|
||||||
ax += lockW;
|
ax += lockW;
|
||||||
}
|
}
|
||||||
// Click-to-copy compact address.
|
// Click-to-copy compact address.
|
||||||
@@ -1309,6 +1443,42 @@ void RenderChatTab(App* app)
|
|||||||
ImGui::SetCursorScreenPos(ImVec2(hp.x, hp.y + rowH + 4.0f * hdpi));
|
ImGui::SetCursorScreenPos(ImVec2(hp.x, hp.y + rowH + 4.0f * hdpi));
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
|
|
||||||
|
// Stranger contact-request card: Accept (add to contacts) or Block (delete + block). Normal
|
||||||
|
// ImGui flow, so ##ChatMessages below (sized from the remaining content height) shrinks to fit.
|
||||||
|
if (sel->pendingRequest) {
|
||||||
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(material::Warning()));
|
||||||
|
ImGui::TextUnformatted(TR("chat_request_title"));
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
ImGui::PushTextWrapPos(0.0f);
|
||||||
|
ImGui::TextWrapped("%s", TR("chat_request_body"));
|
||||||
|
ImGui::PopTextWrapPos();
|
||||||
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||||
|
if (ImGui::SmallButton(TR("chat_request_accept"))) {
|
||||||
|
data::AddressBookEntry e;
|
||||||
|
e.address = sel->peerZaddr;
|
||||||
|
e.label = sel->peerName;
|
||||||
|
book.addEntry(e); // book.revision() bumps → the request flag clears on the next rebuild
|
||||||
|
Notifications::instance().info(TR("chat_request_accepted"));
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::SmallButton(TR("chat_request_block"))) {
|
||||||
|
const std::string cid = sel->cid, nm = sel->peerName; // copy before deleteConversation
|
||||||
|
if (app->chatService().deleteConversation(cid, /*block=*/true)) {
|
||||||
|
if (app->settings()) {
|
||||||
|
app->settings()->setChatBlocked(cid, nm, true);
|
||||||
|
app->settings()->setChatHidden(cid, false);
|
||||||
|
app->settings()->save();
|
||||||
|
}
|
||||||
|
if (s_selected_cid == cid) s_selected_cid.clear();
|
||||||
|
Notifications::instance().info(TR("chat_blocked_toast"));
|
||||||
|
} else {
|
||||||
|
Notifications::instance().error(TR("chat_delete_failed"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::Separator();
|
||||||
|
}
|
||||||
|
|
||||||
const float dp = Layout::dpiScale();
|
const float dp = Layout::dpiScale();
|
||||||
// Small message-list inset (the ##ChatThread WindowPadding above already holds content off
|
// Small message-list inset (the ##ChatThread WindowPadding above already holds content off
|
||||||
// the glass edge; keep a little here so bubbles don't hug the scrollbar).
|
// the glass edge; keep a little here so bubbles don't hug the scrollbar).
|
||||||
@@ -1317,6 +1487,13 @@ void RenderChatTab(App* app)
|
|||||||
const float origSpacingX = ImGui::GetStyle().ItemSpacing.x;
|
const float origSpacingX = ImGui::GetStyle().ItemSpacing.x;
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(4.0f * dp, 8.0f * dp));
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(4.0f * dp, 8.0f * dp));
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(origSpacingX, 0.0f));
|
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(origSpacingX, 0.0f));
|
||||||
|
// Thicker, clearly-visible scrollbar for the message thread: the global one is a thin 6px with a
|
||||||
|
// ~6% grab that's hard to see and grab. Pushed for this child only (popped after EndChild below).
|
||||||
|
ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarSize, 12.0f * dp);
|
||||||
|
ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarRounding, 6.0f * dp);
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrab, material::WithAlpha(material::OnSurface(), 55));
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabHovered, material::WithAlpha(material::OnSurface(), 95));
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabActive, material::WithAlpha(material::OnSurface(), 135));
|
||||||
ImGui::BeginChild("##ChatMessages", ImVec2(0, ImGui::GetContentRegionAvail().y),
|
ImGui::BeginChild("##ChatMessages", ImVec2(0, ImGui::GetContentRegionAvail().y),
|
||||||
ImGuiChildFlags_AlwaysUseWindowPadding, ImGuiWindowFlags_NoScrollWithMouse);
|
ImGuiChildFlags_AlwaysUseWindowPadding, ImGuiWindowFlags_NoScrollWithMouse);
|
||||||
material::ApplySmoothScroll(); // smooth wheel scroll; syncs with the auto-scroll-to-bottom below
|
material::ApplySmoothScroll(); // smooth wheel scroll; syncs with the auto-scroll-to-bottom below
|
||||||
@@ -1640,7 +1817,10 @@ void RenderChatTab(App* app)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (ImGui::BeginPopup("##msgmenu")) {
|
if (ImGui::BeginPopup("##msgmenu")) {
|
||||||
if (ImGui::MenuItem(TR("copy"))) ImGui::SetClipboardText(m.body.c_str());
|
if (ImGui::MenuItem(TR("copy"))) {
|
||||||
|
ImGui::SetClipboardText(m.body.c_str());
|
||||||
|
Notifications::instance().success(TR("copied"));
|
||||||
|
}
|
||||||
ImGui::EndPopup();
|
ImGui::EndPopup();
|
||||||
} else if (s_msgmenu_index == static_cast<int>(mi)) {
|
} else if (s_msgmenu_index == static_cast<int>(mi)) {
|
||||||
s_msgmenu_index = -1; // popup closed
|
s_msgmenu_index = -1; // popup closed
|
||||||
@@ -1778,11 +1958,59 @@ void RenderChatTab(App* app)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ImGui::EndChild();
|
ImGui::EndChild();
|
||||||
ImGui::PopStyleVar(2); // ##ChatMessages WindowPadding + zeroed vertical ItemSpacing
|
ImGui::PopStyleColor(3); // ScrollbarGrab + Hovered + Active
|
||||||
|
ImGui::PopStyleVar(4); // WindowPadding + ItemSpacing + ScrollbarSize + ScrollbarRounding
|
||||||
|
// Larger-messages tier 3: while a big incoming message is still arriving across transactions,
|
||||||
|
// show a subtle centred progress line (it surfaces as an ordinary bubble once every part lands).
|
||||||
|
{
|
||||||
|
int rcv = 0, tot = 0;
|
||||||
|
for (const auto& pr : app->chatService().pendingLargeMessages())
|
||||||
|
if (pr.conversation_id == sel->cid) { rcv = pr.received; tot = pr.total; break; }
|
||||||
|
if (tot > 0) {
|
||||||
|
const float dp2 = Layout::dpiScale();
|
||||||
|
ImGui::Dummy(ImVec2(0.0f, 2.0f * dp2));
|
||||||
|
char buf[96];
|
||||||
|
std::snprintf(buf, sizeof buf, "%s %d / %d %s",
|
||||||
|
TR("chat_receiving_large"), rcv, tot, TR("chat_parts_label"));
|
||||||
|
ImFont* f = ImGui::GetFont();
|
||||||
|
const ImVec2 tsz = f->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, 0.0f, buf);
|
||||||
|
const float availX = ImGui::GetContentRegionAvail().x;
|
||||||
|
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + std::max(0.0f, (availX - tsz.x) * 0.5f));
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, material::OnSurfaceMedium());
|
||||||
|
ImGui::TextUnformatted(buf);
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
}
|
||||||
|
}
|
||||||
// Composer is rendered OUTSIDE this box, directly below it (see after EndChild).
|
// Composer is rendered OUTSIDE this box, directly below it (see after EndChild).
|
||||||
|
} else if (convs.empty() && app->settings() && !app->settings()->getChatIntroDismissed()) {
|
||||||
|
// First-run intro: explain how chat works + let the user copy their address, instead of a bare
|
||||||
|
// "no conversations yet" line. Dismissal is persisted.
|
||||||
|
const float idp = Layout::dpiScale();
|
||||||
|
const float availX = ImGui::GetContentRegionAvail().x;
|
||||||
|
const float cardW = std::min(availX - 40.0f * idp, 460.0f * idp);
|
||||||
|
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + std::max(0.0f, (availX - cardW) * 0.5f));
|
||||||
|
ImGui::BeginGroup();
|
||||||
|
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + cardW);
|
||||||
|
material::Type().textColored(material::TypeStyle::Subtitle1, material::OnSurface(), TR("chat_intro_title"));
|
||||||
|
ImGui::Spacing();
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, material::OnSurfaceMedium());
|
||||||
|
ImGui::TextWrapped("%s", TR("chat_intro_body"));
|
||||||
|
ImGui::Spacing();
|
||||||
|
ImGui::TextWrapped("%s", TR("chat_intro_fee"));
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
ImGui::PopTextWrapPos();
|
||||||
|
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
|
||||||
|
if (material::TactileButton(TR("chat_copy_my_address"), ImVec2(cardW, 0))) copyMyChatAddress(app);
|
||||||
|
ImGui::Spacing();
|
||||||
|
if (material::TactileButton(TR("chat_intro_dismiss"), ImVec2(cardW, 0))) {
|
||||||
|
app->settings()->setChatIntroDismissed(true);
|
||||||
|
app->settings()->save();
|
||||||
|
}
|
||||||
|
ImGui::EndGroup();
|
||||||
|
} else if (convs.empty()) {
|
||||||
|
centeredEmptyState(ICON_MD_FORUM, TR("chat_empty_title"), TR("chat_empty_start"));
|
||||||
} else {
|
} else {
|
||||||
if (convs.empty()) centeredEmptyState(ICON_MD_FORUM, TR("chat_empty_title"), TR("chat_empty_start"));
|
centeredEmptyState(ICON_MD_CHAT_BUBBLE_OUTLINE, TR("chat_select_hint"), nullptr);
|
||||||
else centeredEmptyState(ICON_MD_CHAT_BUBBLE_OUTLINE, TR("chat_select_hint"), nullptr);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ImGui::EndChild();
|
ImGui::EndChild();
|
||||||
@@ -1821,6 +2049,17 @@ void RenderChatTab(App* app)
|
|||||||
// Fraction of the on-chain cap used, for the radial gauge (drawn inside the input, below).
|
// Fraction of the on-chain cap used, for the radial gauge (drawn inside the input, below).
|
||||||
const float frac = kBodyMaxBytes > 0 ? static_cast<float>(used) / kBodyMaxBytes : 0.0f;
|
const float frac = kBodyMaxBytes > 0 ? static_cast<float>(used) / kBodyMaxBytes : 0.0f;
|
||||||
|
|
||||||
|
// How this message chunks on-chain (mirrors buildOutgoingMessage): the hex ciphertext is
|
||||||
|
// (plaintext + the 17-byte secretstream tag) * 2; ≤512 hex fits one memo output, otherwise it
|
||||||
|
// splits into 510-hex chunks (512 minus a 2-char part index). Up to kHushChatMaxMessageParts (16)
|
||||||
|
// chunks ride ONE transaction (tier 2); beyond that the message spills across several (tier 3).
|
||||||
|
const int chatMsgParts = [&]() -> int {
|
||||||
|
const std::size_t hexLen = (static_cast<std::size_t>(used) + 17) * 2;
|
||||||
|
return hexLen <= 512 ? 1 : static_cast<int>((hexLen + 509) / 510); // ceil(hexLen/510)
|
||||||
|
}();
|
||||||
|
// How many transactions those chunks need (tier 3 is >1): the real cost the user asked to see.
|
||||||
|
const int chatMsgTxns = (chatMsgParts + chat::kHushChatMaxMessageParts - 1) / chat::kHushChatMaxMessageParts;
|
||||||
|
|
||||||
// Input row: [emoji] [input] [Send]. The input's TOP is pinned 10px below the message box and
|
// Input row: [emoji] [input] [Send]. The input's TOP is pinned 10px below the message box and
|
||||||
// it grows DOWNWARD as it gets taller (the thread above shrinks), so the row bottom stays put.
|
// it grows DOWNWARD as it gets taller (the thread above shrinks), so the row bottom stays put.
|
||||||
const float rowY = boxMax.y + composerAreaH - composerBoxH;
|
const float rowY = boxMax.y + composerAreaH - composerBoxH;
|
||||||
@@ -1931,14 +2170,43 @@ void RenderChatTab(App* app)
|
|||||||
gdl->AddCircleFilled(ImVec2(ringC.x + std::cos(a1) * ringR, ringC.y + std::sin(a1) * ringR),
|
gdl->AddCircleFilled(ImVec2(ringC.x + std::cos(a1) * ringR, ringC.y + std::sin(a1) * ringR),
|
||||||
thick * 0.6f, fillCol, 10);
|
thick * 0.6f, fillCol, 10);
|
||||||
}
|
}
|
||||||
|
// Always-visible indicator beside the gauge once the message spans >1 memo output: the part
|
||||||
|
// count while it still fits one transaction (tier 2), or the TRANSACTION count once it spills
|
||||||
|
// across several (tier 3 — the real cost the user cares about). Full explanation on hover.
|
||||||
|
if (chatMsgParts > 1 && !overCap) {
|
||||||
|
ImFont* cf = ImGui::GetFont();
|
||||||
|
const float cfsz = std::max(10.0f, lineH * 0.42f);
|
||||||
|
char buf[32];
|
||||||
|
if (chatMsgTxns > 1)
|
||||||
|
std::snprintf(buf, sizeof buf, "%d %s", chatMsgTxns, TR("chat_txns_label"));
|
||||||
|
else
|
||||||
|
std::snprintf(buf, sizeof buf, "%d %s", chatMsgParts, TR("chat_parts_label"));
|
||||||
|
const ImVec2 tsz = cf->CalcTextSizeA(cfsz, FLT_MAX, 0.0f, buf);
|
||||||
|
gdl->AddText(cf, cfsz, ImVec2(ringC.x - ringR - 6.0f * tdp - tsz.x, ringC.y - tsz.y * 0.5f),
|
||||||
|
material::WithAlpha(material::OnSurface(), chatMsgTxns > 1 ? 205 : 150), buf);
|
||||||
|
}
|
||||||
// Exact count on hover (progressive disclosure; over-cap prefixes a label).
|
// Exact count on hover (progressive disclosure; over-cap prefixes a label).
|
||||||
const float hitR = ringR + thick;
|
const float hitR = ringR + thick;
|
||||||
if (ImGui::IsMouseHoveringRect(ImVec2(ringC.x - hitR, ringC.y - hitR),
|
if (ImGui::IsMouseHoveringRect(ImVec2(ringC.x - hitR, ringC.y - hitR),
|
||||||
ImVec2(ringC.x + hitR, ringC.y + hitR))) {
|
ImVec2(ringC.x + hitR, ringC.y + hitR))) {
|
||||||
std::string tip = std::to_string(used) + " / " + std::to_string(kBodyMaxBytes);
|
std::string tip = std::to_string(used) + " / " + std::to_string(kBodyMaxBytes);
|
||||||
if (overCap) tip = std::string(TR("chat_len_over")) + " " + tip;
|
if (overCap) {
|
||||||
|
tip = std::string(TR("chat_len_over")) + " " + tip;
|
||||||
|
} else if (chatMsgTxns > 1) {
|
||||||
|
tip += " \xC2\xB7 " + std::to_string(chatMsgParts) + " " + TR("chat_parts_label") +
|
||||||
|
" \xC2\xB7 " + std::to_string(chatMsgTxns) + " " + TR("chat_txns_label");
|
||||||
|
} else if (chatMsgParts > 1) {
|
||||||
|
tip += " \xC2\xB7 " + std::to_string(chatMsgParts) + " " + TR("chat_parts_label");
|
||||||
|
}
|
||||||
ImGui::BeginTooltip();
|
ImGui::BeginTooltip();
|
||||||
ImGui::TextUnformatted(tip.c_str());
|
ImGui::TextUnformatted(tip.c_str());
|
||||||
|
if (chatMsgParts > 1 && !overCap) {
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, material::OnSurfaceMedium());
|
||||||
|
ImGui::PushTextWrapPos(ImGui::GetFontSize() * 24.0f);
|
||||||
|
ImGui::TextUnformatted(TR(chatMsgTxns > 1 ? "chat_parts_explain_multi" : "chat_parts_explain"));
|
||||||
|
ImGui::PopTextWrapPos();
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
}
|
||||||
ImGui::EndTooltip();
|
ImGui::EndTooltip();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1956,7 +2224,7 @@ void RenderChatTab(App* app)
|
|||||||
// When Send is disabled *because the message is over the cap*, explain why on hover — the
|
// When Send is disabled *because the message is over the cap*, explain why on hover — the
|
||||||
// gauge went red but the numeric reason is no longer always-visible.
|
// gauge went red but the numeric reason is no longer always-visible.
|
||||||
if (overCap && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
|
if (overCap && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
|
||||||
ImGui::SetTooltip("%s %d / %d", TR("chat_len_over"), used, kBodyMaxBytes);
|
material::Tooltip("%s %d / %d", TR("chat_len_over"), used, kBodyMaxBytes);
|
||||||
ImGui::PopStyleColor(3);
|
ImGui::PopStyleColor(3);
|
||||||
|
|
||||||
if (submit && s_compose[0] != '\0' && !overCap) {
|
if (submit && s_compose[0] != '\0' && !overCap) {
|
||||||
@@ -2002,18 +2270,28 @@ void RenderChatTab(App* app)
|
|||||||
const std::string activeHash = app->activeWalletScopeId(); // per-wallet contact scope
|
const std::string activeHash = app->activeWalletScopeId(); // per-wallet contact scope
|
||||||
int shown = 0;
|
int shown = 0;
|
||||||
for (const auto& e : book.entries()) {
|
for (const auto& e : book.entries()) {
|
||||||
if (e.address.empty() || e.address[0] != 'z') continue; // chat requires a z-address
|
if (e.address.empty()) continue;
|
||||||
// Respect the same per-wallet scope the Contacts tab enforces — don't leak another
|
// Respect the same per-wallet scope the Contacts tab enforces — don't leak another
|
||||||
// wallet's scoped contact into this wallet's picker (global + legacy fail open).
|
// wallet's scoped contact into this wallet's picker (global + legacy fail open).
|
||||||
const bool visible = e.isGlobal() ||
|
const bool visible = e.isGlobal() ||
|
||||||
(e.scope.rfind("w:", 0) == 0 ? (!activeHash.empty() && e.scope == activeHash) : true);
|
(e.scope.rfind("w:", 0) == 0 ? (!activeHash.empty() && e.scope == activeHash) : true);
|
||||||
if (!visible) continue;
|
if (!visible) continue;
|
||||||
++shown;
|
|
||||||
const std::string item = e.label + " " + shorten(e.address, 16, 8);
|
const std::string item = e.label + " " + shorten(e.address, 16, 8);
|
||||||
|
// Show transparent contacts too, but DISABLED with a reason — chat needs a shielded
|
||||||
|
// address, so silently hiding them left the user wondering where a contact went.
|
||||||
|
if (dragonx::util::isShieldedAddress(e.address)) {
|
||||||
|
++shown;
|
||||||
if (ImGui::Selectable(item.c_str())) {
|
if (ImGui::Selectable(item.c_str())) {
|
||||||
std::strncpy(s_new_zaddr, e.address.c_str(), sizeof(s_new_zaddr) - 1);
|
std::strncpy(s_new_zaddr, e.address.c_str(), sizeof(s_new_zaddr) - 1);
|
||||||
s_new_zaddr[sizeof(s_new_zaddr) - 1] = '\0';
|
s_new_zaddr[sizeof(s_new_zaddr) - 1] = '\0';
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
ImGui::BeginDisabled();
|
||||||
|
ImGui::Selectable(item.c_str(), false);
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
|
||||||
|
material::Tooltip("%s", TR("chat_contact_needs_z"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (shown == 0) {
|
if (shown == 0) {
|
||||||
ImGui::PushStyleColor(ImGuiCol_Text, material::OnSurfaceMedium());
|
ImGui::PushStyleColor(ImGuiCol_Text, material::OnSurfaceMedium());
|
||||||
@@ -2053,6 +2331,113 @@ void RenderChatTab(App* app)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Export-as-plaintext confirm (unencrypted file on disk) ----
|
||||||
|
if (s_show_export_confirm) {
|
||||||
|
const ConvSummary* ex = nullptr;
|
||||||
|
for (const auto& c : convs) if (c.cid == s_export_cid) { ex = &c; break; }
|
||||||
|
if (!ex) {
|
||||||
|
s_show_export_confirm = false;
|
||||||
|
s_export_cid.clear();
|
||||||
|
} else {
|
||||||
|
material::OverlayDialogSpec ov;
|
||||||
|
ov.title = TR("chat_export_confirm_title");
|
||||||
|
ov.p_open = &s_show_export_confirm;
|
||||||
|
ov.style = material::OverlayStyle::BlurFloat;
|
||||||
|
ov.cardWidth = 500.0f;
|
||||||
|
ov.idSuffix = "chatexport";
|
||||||
|
if (material::BeginOverlayDialog(ov)) {
|
||||||
|
const float edp = Layout::dpiScale();
|
||||||
|
const std::string path = chatExportPathFor(ex->peerName);
|
||||||
|
ImGui::PushTextWrapPos(0.0f);
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(material::Warning()));
|
||||||
|
ImGui::TextWrapped("%s", TR("chat_export_confirm_body"));
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
ImGui::Spacing();
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(material::OnSurfaceMedium()));
|
||||||
|
ImGui::TextUnformatted(TR("chat_export_confirm_path"));
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
ImGui::TextWrapped("%s", path.c_str());
|
||||||
|
ImGui::PopTextWrapPos();
|
||||||
|
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
|
||||||
|
|
||||||
|
const float egap = ImGui::GetStyle().ItemSpacing.x;
|
||||||
|
float wExp = std::max(110.0f * edp, ImGui::CalcTextSize(TR("chat_export_confirm_btn")).x + 24.0f * edp);
|
||||||
|
float wCan = std::max(90.0f * edp, ImGui::CalcTextSize(TR("chat_cancel")).x + 24.0f * edp);
|
||||||
|
material::BeginOverlayDialogFooter(wExp + wCan + egap, false);
|
||||||
|
if (material::TactileButton(TR("chat_export_confirm_btn"), ImVec2(wExp, 0))) {
|
||||||
|
// Build + write exactly as the old inline export did (format byte-identical).
|
||||||
|
std::string content = "DragonX chat export\n";
|
||||||
|
content += ex->peerName + " <" + ex->peerZaddr + ">\n";
|
||||||
|
content += "cid: " + ex->cid + "\n\n";
|
||||||
|
for (const auto& m : store.conversation(ex->cid)) {
|
||||||
|
const bool out = (m.direction == chat::ChatDirection::Outgoing);
|
||||||
|
content += "[" + formatTime(m.timestamp) + "] " +
|
||||||
|
(out ? std::string(TR("chat_you")) : ex->peerName) + ": " + m.body + "\n";
|
||||||
|
}
|
||||||
|
if (util::Platform::writeFileAtomically(path, content, /*restrictPermissions=*/true))
|
||||||
|
Notifications::instance().success(std::string(TR("chat_export_done")) + ": " + path);
|
||||||
|
else
|
||||||
|
Notifications::instance().error(TR("chat_export_failed"));
|
||||||
|
s_show_export_confirm = false;
|
||||||
|
s_export_cid.clear();
|
||||||
|
}
|
||||||
|
ImGui::SameLine(0, egap);
|
||||||
|
if (material::TactileButton(TR("chat_cancel"), ImVec2(wCan, 0))) {
|
||||||
|
s_show_export_confirm = false;
|
||||||
|
s_export_cid.clear();
|
||||||
|
}
|
||||||
|
material::EndOverlayDialog();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Key-fingerprint verification dialog ----
|
||||||
|
if (s_show_fingerprint) {
|
||||||
|
const ConvSummary* fp = nullptr;
|
||||||
|
for (const auto& c : convs) if (c.cid == s_fingerprint_cid) { fp = &c; break; }
|
||||||
|
if (!fp || fp->peerPubKey.empty()) {
|
||||||
|
s_show_fingerprint = false;
|
||||||
|
s_fingerprint_cid.clear();
|
||||||
|
} else {
|
||||||
|
material::OverlayDialogSpec ov;
|
||||||
|
ov.title = TR("chat_verify_title");
|
||||||
|
ov.p_open = &s_show_fingerprint;
|
||||||
|
ov.style = material::OverlayStyle::BlurFloat;
|
||||||
|
ov.cardWidth = 500.0f;
|
||||||
|
ov.idSuffix = "chatfp";
|
||||||
|
if (material::BeginOverlayDialog(ov)) {
|
||||||
|
const float fdp = Layout::dpiScale();
|
||||||
|
const std::string fpStr = keyFingerprint(fp->peerPubKey);
|
||||||
|
ImGui::PushTextWrapPos(0.0f);
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(material::OnSurfaceMedium()));
|
||||||
|
ImGui::TextWrapped("%s", TR("chat_verify_body"));
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
ImGui::Spacing();
|
||||||
|
ImGui::TextWrapped("%s", fp->peerName.c_str());
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(material::Primary()));
|
||||||
|
ImGui::TextWrapped("%s", fpStr.c_str());
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
ImGui::PopTextWrapPos();
|
||||||
|
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
|
||||||
|
|
||||||
|
const float fgap = ImGui::GetStyle().ItemSpacing.x;
|
||||||
|
float wCopy = std::max(110.0f * fdp, ImGui::CalcTextSize(TR("chat_verify_copy")).x + 24.0f * fdp);
|
||||||
|
float wClose = std::max(90.0f * fdp, ImGui::CalcTextSize(TR("chat_cancel")).x + 24.0f * fdp);
|
||||||
|
material::BeginOverlayDialogFooter(wCopy + wClose + fgap, false);
|
||||||
|
if (material::TactileButton(TR("chat_verify_copy"), ImVec2(wCopy, 0))) {
|
||||||
|
ImGui::SetClipboardText(fp->peerPubKey.c_str());
|
||||||
|
Notifications::instance().success(TR("copied"));
|
||||||
|
}
|
||||||
|
ImGui::SameLine(0, fgap);
|
||||||
|
if (material::TactileButton(TR("chat_cancel"), ImVec2(wClose, 0))) {
|
||||||
|
s_show_fingerprint = false;
|
||||||
|
s_fingerprint_cid.clear();
|
||||||
|
}
|
||||||
|
material::EndOverlayDialog();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Delete-conversation confirm (revive-on-new-message vs delete & block) ----
|
// ---- Delete-conversation confirm (revive-on-new-message vs delete & block) ----
|
||||||
if (s_show_delete_confirm) {
|
if (s_show_delete_confirm) {
|
||||||
const float dp = Layout::dpiScale();
|
const float dp = Layout::dpiScale();
|
||||||
@@ -2098,6 +2483,57 @@ void RenderChatTab(App* app)
|
|||||||
s_delete_cid.clear(); s_delete_name.clear();
|
s_delete_cid.clear(); s_delete_name.clear();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// "Delete everywhere": block locally (instant, persistent — no sync flicker) AND broadcast a
|
||||||
|
// self-addressed Control(del) so other devices + a fresh seed import hide it too. Carries the
|
||||||
|
// peer z-address as the control label so Restore can match it by address.
|
||||||
|
auto doDeleteEverywhere = [&]() {
|
||||||
|
const std::string cid = s_delete_cid, name = s_delete_name;
|
||||||
|
std::string peerAddr;
|
||||||
|
for (const auto& m : app->chatService().store().conversation(cid)) {
|
||||||
|
if (!m.peer_zaddr.empty()) { peerAddr = m.peer_zaddr; break; }
|
||||||
|
}
|
||||||
|
if (!app->chatService().deleteConversation(cid, /*block=*/true)) {
|
||||||
|
Notifications::instance().error(TR("chat_delete_failed"));
|
||||||
|
s_show_delete_confirm = false; s_delete_cid.clear(); s_delete_name.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (app->settings()) {
|
||||||
|
app->settings()->setChatBlocked(cid, name, true);
|
||||||
|
app->settings()->setChatHidden(cid, false);
|
||||||
|
app->settings()->save();
|
||||||
|
}
|
||||||
|
app->sendChatControl(cid, "del", peerAddr.empty() ? name : peerAddr);
|
||||||
|
if (s_selected_cid == cid) s_selected_cid.clear();
|
||||||
|
Notifications::instance().info(TR("chat_delete_everywhere_toast"));
|
||||||
|
s_show_delete_confirm = false; s_delete_cid.clear(); s_delete_name.clear();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Full-width on-chain delete above the local footer, gated on a fully-synced wallet (so a
|
||||||
|
// conversation can't flicker in/out mid-sync while the delete signal is still catching up).
|
||||||
|
// Full node requires tip_known (isSynced); lite never sets it, so use its own "sync complete
|
||||||
|
// + at tip" signal there — otherwise the button would be permanently disabled in lite.
|
||||||
|
const auto& syncSt = app->state().sync;
|
||||||
|
const bool synced = app->isLiteBuild()
|
||||||
|
? (!syncSt.syncing && syncSt.blocks > 0 && syncSt.blocks >= syncSt.headers - 2)
|
||||||
|
: syncSt.isSynced();
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(material::WithAlpha(material::Error(), 205)));
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(material::Error()));
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImGui::ColorConvertU32ToFloat4(material::WithAlpha(material::Error(), 235)));
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(material::OnError()));
|
||||||
|
ImGui::BeginDisabled(!synced);
|
||||||
|
const bool doEvery = material::TactileButton(TR("chat_delete_everywhere"),
|
||||||
|
ImVec2(ImGui::GetContentRegionAvail().x, 0));
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
ImGui::PopStyleColor(4);
|
||||||
|
if (doEvery) doDeleteEverywhere();
|
||||||
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, material::OnSurfaceMedium());
|
||||||
|
ImGui::PushTextWrapPos(0.0f);
|
||||||
|
ImGui::TextUnformatted(synced ? TR("chat_delete_everywhere_note") : TR("chat_delete_needs_sync"));
|
||||||
|
ImGui::PopTextWrapPos();
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
|
||||||
|
|
||||||
auto textW = [&](const char* t) {
|
auto textW = [&](const char* t) {
|
||||||
return ImGui::CalcTextSize(t).x + ImGui::GetStyle().FramePadding.x * 2.0f + 20.0f * dp;
|
return ImGui::CalcTextSize(t).x + ImGui::GetStyle().FramePadding.x * 2.0f + 20.0f * dp;
|
||||||
};
|
};
|
||||||
@@ -2169,6 +2605,215 @@ void RenderChatTab(App* app)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Restore conversations (reverse an on-chain "Delete everywhere") ----
|
||||||
|
if (s_show_restore) {
|
||||||
|
const float dp = Layout::dpiScale();
|
||||||
|
material::OverlayDialogSpec ov;
|
||||||
|
ov.title = TR("chat_restore_title");
|
||||||
|
ov.p_open = &s_show_restore;
|
||||||
|
ov.style = material::OverlayStyle::BlurFloat;
|
||||||
|
ov.cardWidth = 520.0f; ov.idSuffix = "chatrestore";
|
||||||
|
if (material::BeginOverlayDialog(ov)) {
|
||||||
|
ImGui::PushTextWrapPos(0.0f);
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, material::OnSurfaceMedium());
|
||||||
|
ImGui::TextUnformatted(TR("chat_restore_desc"));
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
ImGui::PopTextWrapPos();
|
||||||
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||||
|
|
||||||
|
// Restore: clear any local block/hide here AND broadcast Control("res") so every device un-hides.
|
||||||
|
// The messages re-import from the chain on the next scan (the reducer no longer hides the cid).
|
||||||
|
auto doRestore = [&](const std::string& cid, const std::string& label) {
|
||||||
|
if (cid.empty()) return;
|
||||||
|
if (app->settings()) {
|
||||||
|
app->settings()->setChatBlocked(cid, "", false);
|
||||||
|
app->settings()->setChatHidden(cid, false);
|
||||||
|
app->settings()->save();
|
||||||
|
}
|
||||||
|
app->forgetChatConversationSeen(cid);
|
||||||
|
app->sendChatControl(cid, "res", label.empty() ? cid : label);
|
||||||
|
Notifications::instance().info(TR("chat_restore_toast"));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Chain-derived deleted conversations — present even on a wallet freshly restored from seed.
|
||||||
|
const auto deleted = app->chatService().onChainDeletedConversations();
|
||||||
|
if (deleted.empty()) {
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, material::OnSurfaceMedium());
|
||||||
|
ImGui::PushTextWrapPos(0.0f);
|
||||||
|
ImGui::TextUnformatted(TR("chat_restore_none"));
|
||||||
|
ImGui::PopTextWrapPos();
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
} else {
|
||||||
|
std::string restoreCid, restoreLabel;
|
||||||
|
for (const auto& d : deleted) {
|
||||||
|
ImGui::PushID(d.first.c_str());
|
||||||
|
const std::string label = d.second.empty() ? shorten(d.first, 10, 6) : shorten(d.second, 12, 6);
|
||||||
|
const float bw = ImGui::CalcTextSize(TR("chat_restore_btn")).x +
|
||||||
|
ImGui::GetStyle().FramePadding.x * 2.0f + 16.0f * dp;
|
||||||
|
ImGui::AlignTextToFramePadding();
|
||||||
|
ImGui::TextUnformatted(label.c_str());
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + ImGui::GetContentRegionAvail().x - bw);
|
||||||
|
if (material::TactileButton(TR("chat_restore_btn"), ImVec2(bw, 0))) {
|
||||||
|
restoreCid = d.first; restoreLabel = d.second;
|
||||||
|
}
|
||||||
|
ImGui::PopID();
|
||||||
|
}
|
||||||
|
if (!restoreCid.empty()) doRestore(restoreCid, restoreLabel);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||||
|
ImGui::Separator();
|
||||||
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||||
|
|
||||||
|
// Paste the chat (z) address to restore a conversation not in the list (matched against the
|
||||||
|
// address each delete signal carries; otherwise the input is treated as a raw conversation id).
|
||||||
|
ImGui::TextUnformatted(TR("chat_restore_paste_label"));
|
||||||
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||||
|
ImGui::SetNextItemWidth(-FLT_MIN);
|
||||||
|
ImGui::InputTextWithHint("##chatrestoreaddr", TR("chat_restore_paste_hint"),
|
||||||
|
s_restore_addr, sizeof(s_restore_addr));
|
||||||
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||||
|
const float gap = Layout::spacingSm();
|
||||||
|
const float wPaste = std::max(120.0f * dp, ImGui::CalcTextSize(TR("chat_restore_btn")).x + 44.0f * dp);
|
||||||
|
const float wScan = std::max(120.0f * dp, ImGui::CalcTextSize(TR("chat_restore_scan")).x + 44.0f * dp);
|
||||||
|
if (material::TactileButton(TR("chat_restore_btn"), ImVec2(wPaste, 0))) {
|
||||||
|
std::string input(s_restore_addr);
|
||||||
|
while (!input.empty() && (input.front() == ' ' || input.front() == '\n' || input.front() == '\t')) input.erase(input.begin());
|
||||||
|
while (!input.empty() && (input.back() == ' ' || input.back() == '\n' || input.back() == '\t')) input.pop_back();
|
||||||
|
if (!input.empty()) {
|
||||||
|
std::string cid, label;
|
||||||
|
for (const auto& d : deleted) { if (d.second == input) { cid = d.first; label = d.second; break; } }
|
||||||
|
if (cid.empty()) { cid = input; label = input; } // not in the list — treat as a raw cid
|
||||||
|
doRestore(cid, label);
|
||||||
|
s_restore_addr[0] = '\0';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::SameLine(0, gap);
|
||||||
|
if (material::TactileButton(TR("chat_restore_scan"), ImVec2(wScan, 0))) {
|
||||||
|
app->noteWalletMutation(); // re-run the change-gated scans → re-harvest control memos
|
||||||
|
Notifications::instance().info(TR("chat_restore_scanning"));
|
||||||
|
}
|
||||||
|
material::EndOverlayDialog();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Sensitive: setup / unlock / mark / unmark (per-conversation local-key encryption) ----
|
||||||
|
if (s_show_sensitive) {
|
||||||
|
const float dp = Layout::dpiScale();
|
||||||
|
const auto vs = app->chatSensitiveVaultState();
|
||||||
|
const bool unlockOnly = s_sensitive_cid.empty();
|
||||||
|
const bool needSetup = (vs == App::ChatVaultState::NotConfigured);
|
||||||
|
const bool needUnlock = (vs == App::ChatVaultState::Locked);
|
||||||
|
material::OverlayDialogSpec ov;
|
||||||
|
ov.title = unlockOnly ? TR("chat_sensitive_unlock_title")
|
||||||
|
: s_sensitive_unmark ? TR("chat_sensitive_unmark_title")
|
||||||
|
: TR("chat_sensitive_mark_title");
|
||||||
|
ov.p_open = &s_show_sensitive;
|
||||||
|
ov.style = material::OverlayStyle::BlurFloat;
|
||||||
|
ov.cardWidth = 520.0f; ov.idSuffix = "chatsensitive";
|
||||||
|
if (material::BeginOverlayDialog(ov)) {
|
||||||
|
ImGui::PushTextWrapPos(0.0f);
|
||||||
|
if (unlockOnly) {
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, material::OnSurfaceMedium());
|
||||||
|
ImGui::TextUnformatted(TR("chat_sensitive_unlock_body"));
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
} else if (s_sensitive_unmark) {
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, material::OnSurfaceMedium());
|
||||||
|
ImGui::TextUnformatted(TR("chat_sensitive_unmark_body"));
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
} else {
|
||||||
|
ImGui::TextUnformatted(TR("chat_sensitive_warn_1"));
|
||||||
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, material::Error());
|
||||||
|
ImGui::TextUnformatted(TR("chat_sensitive_warn_lose"));
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, material::OnSurfaceMedium());
|
||||||
|
ImGui::TextUnformatted(TR("chat_sensitive_warn_dir"));
|
||||||
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||||
|
ImGui::TextUnformatted(TR("chat_sensitive_warn_hist"));
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
}
|
||||||
|
ImGui::PopTextWrapPos();
|
||||||
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||||
|
|
||||||
|
if (needSetup) {
|
||||||
|
ImGui::TextUnformatted(TR("chat_sensitive_setup_label"));
|
||||||
|
ImGui::SetNextItemWidth(-FLT_MIN);
|
||||||
|
ImGui::InputTextWithHint("##senspass", TR("chat_sensitive_pass_hint"), s_sensitive_pass,
|
||||||
|
sizeof(s_sensitive_pass), ImGuiInputTextFlags_Password);
|
||||||
|
ImGui::SetNextItemWidth(-FLT_MIN);
|
||||||
|
ImGui::InputTextWithHint("##senspass2", TR("chat_sensitive_pass2_hint"), s_sensitive_pass2,
|
||||||
|
sizeof(s_sensitive_pass2), ImGuiInputTextFlags_Password);
|
||||||
|
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
|
||||||
|
} else if (needUnlock) {
|
||||||
|
ImGui::TextUnformatted(TR("chat_sensitive_unlock_label"));
|
||||||
|
ImGui::SetNextItemWidth(-FLT_MIN);
|
||||||
|
ImGui::InputTextWithHint("##senspass", TR("chat_sensitive_pass_hint"), s_sensitive_pass,
|
||||||
|
sizeof(s_sensitive_pass), ImGuiInputTextFlags_Password);
|
||||||
|
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* actLabel =
|
||||||
|
needSetup ? TR("chat_sensitive_btn_setup")
|
||||||
|
: needUnlock ? (unlockOnly ? TR("chat_sensitive_btn_unlock")
|
||||||
|
: s_sensitive_unmark ? TR("chat_sensitive_btn_unlock_unmark")
|
||||||
|
: TR("chat_sensitive_btn_unlock_mark"))
|
||||||
|
: (unlockOnly ? TR("chat_sensitive_btn_unlock")
|
||||||
|
: s_sensitive_unmark ? TR("chat_sensitive_btn_unmark") : TR("chat_sensitive_btn_mark"));
|
||||||
|
|
||||||
|
auto closeWipe = [&]() {
|
||||||
|
std::memset(s_sensitive_pass, 0, sizeof(s_sensitive_pass));
|
||||||
|
std::memset(s_sensitive_pass2, 0, sizeof(s_sensitive_pass2));
|
||||||
|
s_show_sensitive = false;
|
||||||
|
s_sensitive_cid.clear(); s_sensitive_name.clear(); s_sensitive_peeraddr.clear();
|
||||||
|
};
|
||||||
|
|
||||||
|
const float gap2 = Layout::spacingSm();
|
||||||
|
const float wAct = std::max(160.0f * dp, ImGui::CalcTextSize(actLabel).x + 40.0f * dp);
|
||||||
|
const float wCan = std::max(90.0f * dp, ImGui::CalcTextSize(TR("chat_cancel")).x + 32.0f * dp);
|
||||||
|
material::BeginOverlayDialogFooter(wAct + wCan + gap2, /*drawSeparator=*/false);
|
||||||
|
const bool doAct = material::TactileButton(actLabel, ImVec2(wAct, 0));
|
||||||
|
ImGui::SameLine(0, gap2);
|
||||||
|
const bool doCancel = material::TactileButton(TR("chat_cancel"), ImVec2(wCan, 0));
|
||||||
|
|
||||||
|
if (doAct) {
|
||||||
|
bool ok = true;
|
||||||
|
if (needSetup) {
|
||||||
|
const std::string p1(s_sensitive_pass), p2(s_sensitive_pass2);
|
||||||
|
if (p1.empty() || p1 != p2) {
|
||||||
|
Notifications::instance().error(TR("chat_sensitive_err_pass")); ok = false;
|
||||||
|
} else if (!app->createChatSensitiveVault(p1)) {
|
||||||
|
Notifications::instance().error(TR("chat_sensitive_err_create")); ok = false;
|
||||||
|
}
|
||||||
|
} else if (needUnlock) {
|
||||||
|
const std::string p1(s_sensitive_pass);
|
||||||
|
if (p1.empty() || !app->unlockChatSensitiveVault(p1)) {
|
||||||
|
Notifications::instance().error(TR("chat_sensitive_err_unlock")); ok = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ok) {
|
||||||
|
if (!unlockOnly) {
|
||||||
|
if (s_sensitive_unmark) {
|
||||||
|
app->unmarkChatConversationSensitive(s_sensitive_cid);
|
||||||
|
Notifications::instance().info(TR("chat_sensitive_toast_unmarked"));
|
||||||
|
} else {
|
||||||
|
app->markChatConversationSensitive(s_sensitive_cid, s_sensitive_peeraddr);
|
||||||
|
Notifications::instance().info(TR("chat_sensitive_toast_marked"));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Notifications::instance().info(TR("chat_sensitive_toast_unlocked"));
|
||||||
|
}
|
||||||
|
s_convsKey = ~0ull; // sensitive threads appear/disappear — rebuild the list
|
||||||
|
closeWipe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (doCancel) closeWipe();
|
||||||
|
material::EndOverlayDialog();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Chat customization modal (opened by the header settings "notch") — house BlurFloat overlay ----
|
// ---- Chat customization modal (opened by the header settings "notch") — house BlurFloat overlay ----
|
||||||
if (s_show_chat_settings) {
|
if (s_show_chat_settings) {
|
||||||
material::OverlayDialogSpec ov;
|
material::OverlayDialogSpec ov;
|
||||||
@@ -2224,6 +2869,8 @@ void ResetChatTab()
|
|||||||
sodium_memzero(s_compose, sizeof(s_compose));
|
sodium_memzero(s_compose, sizeof(s_compose));
|
||||||
sodium_memzero(s_new_zaddr, sizeof(s_new_zaddr));
|
sodium_memzero(s_new_zaddr, sizeof(s_new_zaddr));
|
||||||
sodium_memzero(s_new_msg, sizeof(s_new_msg));
|
sodium_memzero(s_new_msg, sizeof(s_new_msg));
|
||||||
|
sodium_memzero(s_pending_new_zaddr, sizeof(s_pending_new_zaddr));
|
||||||
|
s_pending_new_set = false;
|
||||||
sodium_memzero(s_rename_buf, sizeof(s_rename_buf));
|
sodium_memzero(s_rename_buf, sizeof(s_rename_buf));
|
||||||
s_selected_cid.clear();
|
s_selected_cid.clear();
|
||||||
s_scroll_to_cid.clear();
|
s_scroll_to_cid.clear();
|
||||||
@@ -2238,11 +2885,16 @@ void ResetChatTab()
|
|||||||
s_show_blocked = false;
|
s_show_blocked = false;
|
||||||
// Drop the per-frame memoization caches so the next wallet doesn't briefly render the previous one's
|
// Drop the per-frame memoization caches so the next wallet doesn't briefly render the previous one's
|
||||||
// conversations/thread (store.revision() is monotonic and would rebuild anyway, but be explicit).
|
// conversations/thread (store.revision() is monotonic and would rebuild anyway, but be explicit).
|
||||||
|
// Zero the decrypted plaintext the memo caches hold (conversation previews + full thread bodies)
|
||||||
|
// before dropping them, so the previous wallet's messages don't linger in freed heap.
|
||||||
|
auto wipeStr2 = [](std::string& s) { if (!s.empty()) sodium_memzero(&s[0], s.size()); s.clear(); };
|
||||||
|
for (auto& c : s_convs) wipeStr2(c.lastBody);
|
||||||
s_convs.clear();
|
s_convs.clear();
|
||||||
s_convsHidden = 0;
|
s_convsHidden = 0;
|
||||||
s_convsKey = ~0ull;
|
s_convsKey = ~0ull;
|
||||||
s_threadCid.clear();
|
s_threadCid.clear();
|
||||||
s_threadRev = ~0ull;
|
s_threadRev = ~0ull;
|
||||||
|
for (auto& m : s_threadMsgs) { wipeStr2(m.body); wipeStr2(m.fail_reason); wipeStr2(m.peer_zaddr); }
|
||||||
s_threadMsgs.clear();
|
s_threadMsgs.clear();
|
||||||
s_threadFlags.clear(); s_threadRows.clear(); s_threadHeights.clear();
|
s_threadFlags.clear(); s_threadRows.clear(); s_threadHeights.clear();
|
||||||
s_threadPrefix.assign(1, 0.0f); s_threadFailed.clear(); s_threadBodySizes.clear();
|
s_threadPrefix.assign(1, 0.0f); s_threadFailed.clear(); s_threadBodySizes.clear();
|
||||||
@@ -2250,6 +2902,13 @@ void ResetChatTab()
|
|||||||
s_msgmenu_index = -1;
|
s_msgmenu_index = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void OpenChatNewConversation(const std::string& zaddr)
|
||||||
|
{
|
||||||
|
std::strncpy(s_pending_new_zaddr, zaddr.c_str(), sizeof(s_pending_new_zaddr) - 1);
|
||||||
|
s_pending_new_zaddr[sizeof(s_pending_new_zaddr) - 1] = '\0';
|
||||||
|
s_pending_new_set = true;
|
||||||
|
}
|
||||||
|
|
||||||
void RenderChatSettingsControls(App* app, float contentWidth, bool drawCards)
|
void RenderChatSettingsControls(App* app, float contentWidth, bool drawCards)
|
||||||
{
|
{
|
||||||
auto* st = app ? app->settings() : nullptr;
|
auto* st = app ? app->settings() : nullptr;
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
namespace dragonx {
|
namespace dragonx {
|
||||||
|
|
||||||
class App;
|
class App;
|
||||||
@@ -44,5 +46,11 @@ void RenderChatSettingsControls(App* app, float contentWidth = 0.0f, bool drawCa
|
|||||||
*/
|
*/
|
||||||
void ResetChatTab();
|
void ResetChatTab();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Ask the Chat tab to open its New-conversation dialog prefilled with this z-address (consumed
|
||||||
|
* on the next RenderChatTab frame). Used by the Contacts tab's "Message" action.
|
||||||
|
*/
|
||||||
|
void OpenChatNewConversation(const std::string& zaddr);
|
||||||
|
|
||||||
} // namespace ui
|
} // namespace ui
|
||||||
} // namespace dragonx
|
} // namespace dragonx
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
// tab completion, daemon log display, and color-coded output.
|
// tab completion, daemon log display, and color-coded output.
|
||||||
|
|
||||||
#include "console_tab.h"
|
#include "console_tab.h"
|
||||||
|
#include <sodium.h> // sodium_memzero — scrub masked secret-result plaintext
|
||||||
#include "console_command_executor.h"
|
#include "console_command_executor.h"
|
||||||
#include "console_command_reference.h"
|
#include "console_command_reference.h"
|
||||||
#include "console_input_model.h"
|
#include "console_input_model.h"
|
||||||
@@ -298,7 +299,26 @@ void ConsoleTab::render(ConsoleCommandExecutor& exec)
|
|||||||
{
|
{
|
||||||
std::string result;
|
std::string result;
|
||||||
bool isError = false;
|
bool isError = false;
|
||||||
while (exec.pollResult(result, isError)) addFormattedResult(result, isError);
|
while (exec.pollResult(result, isError)) {
|
||||||
|
const bool secret = secret_result_armed_ && !isError;
|
||||||
|
secret_result_armed_ = false; // the first (non-error) result consumes the arming
|
||||||
|
if (secret) {
|
||||||
|
// Hold the plaintext ONLY in secret_result_ and show a masked placeholder — it never
|
||||||
|
// enters the scrollback model, so Copy All / Ctrl+C / the filter can't leak it.
|
||||||
|
if (!secret_result_.empty()) sodium_memzero(&secret_result_[0], secret_result_.size());
|
||||||
|
secret_result_ = result;
|
||||||
|
secret_result_deadline_ = std::chrono::steady_clock::now() + std::chrono::seconds(60);
|
||||||
|
addLine(TR("console_secret_result_hidden"), ConsoleChannel::Warning);
|
||||||
|
} else {
|
||||||
|
addFormattedResult(result, isError);
|
||||||
|
}
|
||||||
|
if (!result.empty()) sodium_memzero(&result[0], result.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Auto-wipe a revealed secret result after its deadline so it doesn't linger for the session.
|
||||||
|
if (!secret_result_.empty() && std::chrono::steady_clock::now() >= secret_result_deadline_) {
|
||||||
|
wipeSecretResult();
|
||||||
|
addLine(TR("console_secret_result_expired"), ConsoleChannel::Info);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Drain the ingest queue into the visible model once per frame, after this frame's own
|
// Drain the ingest queue into the visible model once per frame, after this frame's own
|
||||||
@@ -548,6 +568,20 @@ void ConsoleTab::renderToolbar(ConsoleCommandExecutor& exec)
|
|||||||
material::Tooltip("%s", selection_.active() ? TR("console_copy_selected") : TR("console_copy_all"));
|
material::Tooltip("%s", selection_.active() ? TR("console_copy_selected") : TR("console_copy_all"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When a key-revealing result is masked, offer an explicit Copy-secret (through the auto-clearing
|
||||||
|
// clipboard) and a Hide action. The secret is never in the model, so the normal Copy above is safe.
|
||||||
|
if (hasSecretResult()) {
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (TactileButton(TR("console_copy_secret"), ImVec2(0, 0), schema::UI().resolveFont("button"))) {
|
||||||
|
if (copy_secret_cb_) copy_secret_cb_(secret_result_);
|
||||||
|
else ImGui::SetClipboardText(secret_result_.c_str());
|
||||||
|
}
|
||||||
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("console_copy_secret_tip"));
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (TactileButton(TR("console_hide_secret"), ImVec2(0, 0), schema::UI().resolveFont("button")))
|
||||||
|
wipeSecretResult();
|
||||||
|
}
|
||||||
|
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
|
|
||||||
// Commands reference button — shown whenever the backend offers a reference table (full-node
|
// Commands reference button — shown whenever the backend offers a reference table (full-node
|
||||||
@@ -1552,6 +1586,12 @@ bool IsDestructiveConsoleCommand(const std::string& firstLower, bool fullNode)
|
|||||||
if (n == firstLower) return cat.commands[i].destructive;
|
if (n == firstLower) return cat.commands[i].destructive;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Secret-revealing full-node commands that aren't in the reference catalog still deserve the gate
|
||||||
|
// (e.g. z_exportmnemonic dumps the seed phrase; the lite "seed" equivalent is already flagged).
|
||||||
|
if (fullNode) {
|
||||||
|
static const char* kSecretReveal[] = { "z_exportmnemonic", "z_exportviewingkey" };
|
||||||
|
for (const char* s : kSecretReveal) if (firstLower == s) return true;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
@@ -1559,6 +1599,7 @@ bool IsDestructiveConsoleCommand(const std::string& firstLower, bool fullNode)
|
|||||||
bool ConsoleTab::submitConsoleCommand(ConsoleCommandExecutor& exec, const std::string& cmd)
|
bool ConsoleTab::submitConsoleCommand(ConsoleCommandExecutor& exec, const std::string& cmd)
|
||||||
{
|
{
|
||||||
if (cmd.empty()) return false;
|
if (cmd.empty()) return false;
|
||||||
|
secret_result_armed_ = false; // cleared each submit so a no-result command can't mask the next one
|
||||||
|
|
||||||
// Redact secret-bearing commands (walletpassphrase, z_importkey, …) before they reach the visible
|
// Redact secret-bearing commands (walletpassphrase, z_importkey, …) before they reach the visible
|
||||||
// log and the recall history. The real `cmd` below is still executed unredacted.
|
// log and the recall history. The real `cmd` below is still executed unredacted.
|
||||||
@@ -1616,6 +1657,8 @@ bool ConsoleTab::submitConsoleCommand(ConsoleCommandExecutor& exec, const std::s
|
|||||||
addLine(TR("console_destructive_confirm"), ConsoleChannel::Warning);
|
addLine(TR("console_destructive_confirm"), ConsoleChannel::Warning);
|
||||||
} else {
|
} else {
|
||||||
destructive_confirm_cmd_.clear();
|
destructive_confirm_cmd_.clear();
|
||||||
|
// Arm result-masking for key-revealing commands so their OUTPUT is hidden in the scrollback.
|
||||||
|
secret_result_armed_ = ConsoleCommandRevealsSecret(cmd, exec.hasRpcReference());
|
||||||
exec.submit(cmd);
|
exec.submit(cmd);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -1882,6 +1925,9 @@ void ConsoleTab::renderCommandDetail(const ConsoleCommandEntry& cmd, const char*
|
|||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
if (material::TactileButton(TR("console_ref_run"), ImVec2(cbw, 0))) {
|
if (material::TactileButton(TR("console_ref_run"), ImVec2(cbw, 0))) {
|
||||||
pending_submit_ = built;
|
pending_submit_ = built;
|
||||||
|
// The reference dialog already confirmed this run, so pre-arm the destructive gate to an exact
|
||||||
|
// match — otherwise the replayed submit re-arms it and silently does nothing until retyped.
|
||||||
|
destructive_confirm_cmd_ = built;
|
||||||
command_search_[0] = '\0';
|
command_search_[0] = '\0';
|
||||||
run_confirm_cmd_ = nullptr;
|
run_confirm_cmd_ = nullptr;
|
||||||
show_commands_popup_ = false;
|
show_commands_popup_ = false;
|
||||||
@@ -2193,8 +2239,17 @@ void ConsoleTab::addRpcTraceLine(const std::string& source, const std::string& m
|
|||||||
addLine("[" + rpcTraceTimestamp() + "] [" + source + "] " + method, ConsoleChannel::Rpc);
|
addLine("[" + rpcTraceTimestamp() + "] [" + source + "] " + method, ConsoleChannel::Rpc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ConsoleTab::wipeSecretResult()
|
||||||
|
{
|
||||||
|
if (!secret_result_.empty()) sodium_memzero(&secret_result_[0], secret_result_.size());
|
||||||
|
secret_result_.clear();
|
||||||
|
secret_result_armed_ = false;
|
||||||
|
secret_result_deadline_ = {};
|
||||||
|
}
|
||||||
|
|
||||||
void ConsoleTab::clear()
|
void ConsoleTab::clear()
|
||||||
{
|
{
|
||||||
|
wipeSecretResult(); // the clear command / toolbar button also drops any held secret result
|
||||||
// View-only clear (main thread). The executor keeps its own log cursors, so new output
|
// View-only clear (main thread). The executor keeps its own log cursors, so new output
|
||||||
// still appends. The "cleared" line is ingested and appears on the next frame's drain.
|
// still appends. The "cleared" line is ingested and appears on the next frame's drain.
|
||||||
model_.clear();
|
model_.clear();
|
||||||
|
|||||||
@@ -20,6 +20,8 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
#include <deque>
|
#include <deque>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
|
#include <functional>
|
||||||
|
#include <chrono>
|
||||||
|
|
||||||
namespace dragonx {
|
namespace dragonx {
|
||||||
namespace ui {
|
namespace ui {
|
||||||
@@ -66,6 +68,13 @@ public:
|
|||||||
*/
|
*/
|
||||||
void clear();
|
void clear();
|
||||||
|
|
||||||
|
// Masked secret-result support: the plaintext of a key-revealing command's output is held ONLY here
|
||||||
|
// (never in the scrollback model), so Copy All / Ctrl+C / the filter can't leak it. An explicit
|
||||||
|
// "Copy secret" action routes through this handler (the app wires it to the auto-clearing clipboard).
|
||||||
|
void setCopySecretHandler(std::function<void(const std::string&)> f) { copy_secret_cb_ = std::move(f); }
|
||||||
|
bool hasSecretResult() const { return !secret_result_.empty(); }
|
||||||
|
void wipeSecretResult(); // zero + drop the buffer, disarm, reset the deadline (call on lock/clear)
|
||||||
|
|
||||||
// Ask the console to place the keyboard focus in the command input on the next render (consumed once).
|
// Ask the console to place the keyboard focus in the command input on the next render (consumed once).
|
||||||
// Called when the user switches to the Console tab, gated by the console_auto_focus setting.
|
// Called when the user switches to the Console tab, gated by the console_auto_focus setting.
|
||||||
void requestInputFocus() { focus_input_pending_ = true; }
|
void requestInputFocus() { focus_input_pending_ = true; }
|
||||||
@@ -162,6 +171,10 @@ private:
|
|||||||
char input_buffer_[4096] = {0};
|
char input_buffer_[4096] = {0};
|
||||||
bool stop_confirm_pending_ = false; // 'stop' typed once, awaiting a confirming second 'stop'
|
bool stop_confirm_pending_ = false; // 'stop' typed once, awaiting a confirming second 'stop'
|
||||||
std::string destructive_confirm_cmd_; // a destructive command typed once, awaiting an identical re-entry
|
std::string destructive_confirm_cmd_; // a destructive command typed once, awaiting an identical re-entry
|
||||||
|
bool secret_result_armed_ = false; // the just-submitted command reveals key material in its result
|
||||||
|
std::string secret_result_; // plaintext of the masked result (member-only; never in model_)
|
||||||
|
std::chrono::steady_clock::time_point secret_result_deadline_{}; // auto-wipe the buffer at this time
|
||||||
|
std::function<void(const std::string&)> copy_secret_cb_; // routes "Copy secret" to the app
|
||||||
bool focus_input_pending_ = false; // one-shot: focus the command input next render (tab-open auto-focus)
|
bool focus_input_pending_ = false; // one-shot: focus the command input next render (tab-open auto-focus)
|
||||||
// (log-ingestion cursors + result queue moved to the ConsoleCommandExecutor)
|
// (log-ingestion cursors + result queue moved to the ConsoleCommandExecutor)
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ const char* const kSecretConsoleCommands[] = {
|
|||||||
"importprivkey", "importwallet", "importmulti",
|
"importprivkey", "importwallet", "importmulti",
|
||||||
"z_importkey", "z_importviewingkey", "z_importwallet",
|
"z_importkey", "z_importviewingkey", "z_importwallet",
|
||||||
"signrawtransaction", "magicrecoverkey", "sethdseed", "importmnemonic",
|
"signrawtransaction", "magicrecoverkey", "sethdseed", "importmnemonic",
|
||||||
|
// Lite-wallet verbs (console_command_reference kLiteKeyCommands): these take a spending/viewing key
|
||||||
|
// or a password as their argument. Full-node has no bare import/encrypt/decrypt/unlock verb, so a
|
||||||
|
// flat match is safe.
|
||||||
|
"import", "timport", "encrypt", "decrypt", "unlock",
|
||||||
};
|
};
|
||||||
|
|
||||||
std::string firstConsoleTokenLower(const std::string& cmd, size_t& tokenEnd) {
|
std::string firstConsoleTokenLower(const std::string& cmd, size_t& tokenEnd) {
|
||||||
@@ -67,6 +71,25 @@ bool ConsoleCommandCarriesSecret(const std::string& cmd)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool ConsoleCommandRevealsSecret(const std::string& cmd, bool fullNode)
|
||||||
|
{
|
||||||
|
size_t end = 0;
|
||||||
|
const std::string name = firstConsoleTokenLower(cmd, end);
|
||||||
|
if (name.empty()) return false;
|
||||||
|
if (fullNode) {
|
||||||
|
static const char* const kFull[] = {
|
||||||
|
"dumpprivkey", "z_exportkey", "z_exportviewingkey",
|
||||||
|
"z_exportmnemonic", "dumpwallet", "z_exportwallet",
|
||||||
|
};
|
||||||
|
for (const char* s : kFull) if (name == s) return true;
|
||||||
|
} else {
|
||||||
|
// Lite backend verbs whose output is the seed phrase / private keys.
|
||||||
|
static const char* const kLite[] = { "seed", "export" };
|
||||||
|
for (const char* s : kLite) if (name == s) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
std::string RedactConsoleCommand(const std::string& cmd)
|
std::string RedactConsoleCommand(const std::string& cmd)
|
||||||
{
|
{
|
||||||
size_t end = 0;
|
size_t end = 0;
|
||||||
|
|||||||
@@ -29,6 +29,12 @@ bool ShouldPrintDaemonErrorLine(bool wasError, bool isError,
|
|||||||
// z_exportkey, z_exportmnemonic) are NOT covered — their secret is in the result, a separate concern.
|
// z_exportkey, z_exportmnemonic) are NOT covered — their secret is in the result, a separate concern.
|
||||||
bool ConsoleCommandCarriesSecret(const std::string& cmd);
|
bool ConsoleCommandCarriesSecret(const std::string& cmd);
|
||||||
|
|
||||||
|
// True when the command's RESULT (not its args) reveals key material — dumpprivkey / z_exportkey /
|
||||||
|
// z_exportviewingkey / z_exportmnemonic / dumpwallet / z_exportwallet on the full node, or the lite
|
||||||
|
// `seed` / `export` verbs. Used to mask the output in the console scrollback. `fullNode` selects the
|
||||||
|
// command set (the bare lite verbs `seed`/`export` must not match full-node commands).
|
||||||
|
bool ConsoleCommandRevealsSecret(const std::string& cmd, bool fullNode);
|
||||||
|
|
||||||
// A display/history-safe copy of `cmd`: the command name with its arguments replaced by "****" when
|
// A display/history-safe copy of `cmd`: the command name with its arguments replaced by "****" when
|
||||||
// it carries a secret, else `cmd` unchanged. The real command is still executed unredacted.
|
// it carries a secret, else `cmd` unchanged. The real command is still executed unredacted.
|
||||||
std::string RedactConsoleCommand(const std::string& cmd);
|
std::string RedactConsoleCommand(const std::string& cmd);
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
#include "../../util/platform.h"
|
#include "../../util/platform.h"
|
||||||
#include "image_picker.h"
|
#include "image_picker.h"
|
||||||
#include "transactions_tab.h"
|
#include "transactions_tab.h"
|
||||||
|
#include "chat_tab.h" // OpenChatNewConversation — Contacts "Message" action
|
||||||
#include "../sidebar.h"
|
#include "../sidebar.h"
|
||||||
#include "../layout.h"
|
#include "../layout.h"
|
||||||
#include "imgui.h"
|
#include "imgui.h"
|
||||||
@@ -537,6 +538,15 @@ void RenderContactsTab(App* app)
|
|||||||
SetTransactionsAddressFilter(book.entries()[s_selected_index].address);
|
SetTransactionsAddressFilter(book.entries()[s_selected_index].address);
|
||||||
app->setCurrentPage(ui::NavPage::History);
|
app->setCurrentPage(ui::NavPage::History);
|
||||||
};
|
};
|
||||||
|
// Message a contact in Chat — only z-contacts (chat needs a shielded address to carry encrypted
|
||||||
|
// memos). Mirrors doViewTransactions: hand the address to the Chat tab and switch there.
|
||||||
|
auto doMessage = [&]() {
|
||||||
|
if (!selValid()) return;
|
||||||
|
const auto& e = book.entries()[s_selected_index];
|
||||||
|
if (!isShieldedAddr(e.address)) return;
|
||||||
|
OpenChatNewConversation(e.address);
|
||||||
|
app->setCurrentPage(ui::NavPage::Chat);
|
||||||
|
};
|
||||||
|
|
||||||
// Add/edit form — a modal popup layered over the tab, with a live contact preview and an avatar
|
// Add/edit form — a modal popup layered over the tab, with a live contact preview and an avatar
|
||||||
// picker (default Z/T badge, a Material icon, or a custom image via the in-app image picker).
|
// picker (default Z/T badge, a Material icon, or a custom image via the in-app image picker).
|
||||||
@@ -1465,6 +1475,14 @@ void RenderContactsTab(App* app)
|
|||||||
if (ImGui::BeginPopup("##contactCtx")) {
|
if (ImGui::BeginPopup("##contactCtx")) {
|
||||||
if (ImGui::MenuItem(TR("copy_address"))) doCopy();
|
if (ImGui::MenuItem(TR("copy_address"))) doCopy();
|
||||||
if (ImGui::MenuItem(TR("view_transactions"))) doViewTransactions();
|
if (ImGui::MenuItem(TR("view_transactions"))) doViewTransactions();
|
||||||
|
#if DRAGONX_ENABLE_CHAT
|
||||||
|
{
|
||||||
|
const bool canMsg = selValid() && isShieldedAddr(book.entries()[s_selected_index].address);
|
||||||
|
if (ImGui::MenuItem(TR("contacts_message"), nullptr, false, canMsg)) doMessage();
|
||||||
|
if (!canMsg && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
|
||||||
|
material::Tooltip("%s", TR("contacts_message_needs_z"));
|
||||||
|
}
|
||||||
|
#endif
|
||||||
if (ImGui::MenuItem(TR("edit"))) openEdit();
|
if (ImGui::MenuItem(TR("edit"))) openEdit();
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
if (ImGui::MenuItem(TR("delete"))) doDelete();
|
if (ImGui::MenuItem(TR("delete"))) doDelete();
|
||||||
|
|||||||
@@ -445,11 +445,20 @@ private:
|
|||||||
|
|
||||||
static void renderDone(float, const Progress& p) {
|
static void renderDone(float, const Progress& p) {
|
||||||
using namespace material;
|
using namespace material;
|
||||||
if (!s_notified) { s_installed_flag = true; s_notified = true; }
|
if (!s_notified) {
|
||||||
|
s_installed_flag = true;
|
||||||
|
s_notified = true;
|
||||||
|
// Record a persistent "restart pending" reminder so it isn't lost if the user clicks Later.
|
||||||
|
if (s_app) s_app->markDaemonRestartPending();
|
||||||
|
}
|
||||||
Type().textColored(TypeStyle::Subtitle2, Success(), TR("daemon_update_installed_ok"));
|
Type().textColored(TypeStyle::Subtitle2, Success(), TR("daemon_update_installed_ok"));
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::Text("%s %s", TR("daemon_update_version"), p.latest_tag.c_str());
|
ImGui::Text("%s %s", TR("daemon_update_version"), p.latest_tag.c_str());
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
// Lead with a warning-level headline that a restart is required, then the detail note.
|
||||||
|
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + ImGui::GetContentRegionAvail().x);
|
||||||
|
Type().textColored(TypeStyle::Subtitle2, Warning(), TR("daemon_update_restart_needed"));
|
||||||
|
ImGui::PopTextWrapPos();
|
||||||
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("daemon_update_restart_note"));
|
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("daemon_update_restart_note"));
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
if (s_app->isUsingEmbeddedDaemon()) {
|
if (s_app->isUsingEmbeddedDaemon()) {
|
||||||
@@ -459,6 +468,7 @@ private:
|
|||||||
}
|
}
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
if (TactileButton(TR("daemon_update_later"), ImVec2(fullW(), 0))) s_open = false;
|
if (TactileButton(TR("daemon_update_later"), ImVec2(fullW(), 0))) s_open = false;
|
||||||
|
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("daemon_update_later_hint"));
|
||||||
} else {
|
} else {
|
||||||
if (TactileButton(TR("close"), ImVec2(fullW(), 0))) s_open = false;
|
if (TactileButton(TR("close"), ImVec2(fullW(), 0))) s_open = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -269,7 +269,7 @@ static void fetchBlockDetail(App* app, int height) {
|
|||||||
auto hashResult = rpc->call("getblockhash", {height});
|
auto hashResult = rpc->call("getblockhash", {height});
|
||||||
std::string hash = hashResult.get<std::string>();
|
std::string hash = hashResult.get<std::string>();
|
||||||
result = rpc->call("getblock", {hash});
|
result = rpc->call("getblock", {hash});
|
||||||
} catch (const std::exception& e) { error = e.what(); }
|
} catch (const std::exception& e) { error = util::friendlyRpcError(e.what()); }
|
||||||
return [result, error]() {
|
return [result, error]() {
|
||||||
applyBlockDetailResult(result, error);
|
applyBlockDetailResult(result, error);
|
||||||
};
|
};
|
||||||
@@ -383,19 +383,27 @@ static void navigateToHash(App* app, const std::string& hash) {
|
|||||||
worker->post([rpc, hash]() -> rpc::RPCWorker::MainCb {
|
worker->post([rpc, hash]() -> rpc::RPCWorker::MainCb {
|
||||||
json blockResult, txResult;
|
json blockResult, txResult;
|
||||||
bool gotBlock = false, gotTx = false;
|
bool gotBlock = false, gotTx = false;
|
||||||
|
bool rpcFailed = false; std::string rpcErr;
|
||||||
|
// Distinguish a genuine "not found" (RPC -5/-8) from a transport/timeout failure, so a flaky
|
||||||
|
// connection isn't reported as "no block or transaction found".
|
||||||
|
auto noteErr = [&](const std::exception& e) {
|
||||||
|
const auto* re = dynamic_cast<const rpc::RpcError*>(&e);
|
||||||
|
if (re && (re->code == -5 || re->code == -8)) return; // not-found: a real negative answer
|
||||||
|
if (!rpcFailed) { rpcFailed = true; rpcErr = e.what(); }
|
||||||
|
};
|
||||||
try {
|
try {
|
||||||
rpc::RPCClient::TraceScope trace("Explorer tab / Search");
|
rpc::RPCClient::TraceScope trace("Explorer tab / Search");
|
||||||
blockResult = rpc->call("getblock", {hash});
|
blockResult = rpc->call("getblock", {hash});
|
||||||
gotBlock = true;
|
gotBlock = true;
|
||||||
} catch (...) {}
|
} catch (const std::exception& e) { noteErr(e); }
|
||||||
if (!gotBlock) {
|
if (!gotBlock) {
|
||||||
try {
|
try {
|
||||||
rpc::RPCClient::TraceScope trace("Explorer tab / Search");
|
rpc::RPCClient::TraceScope trace("Explorer tab / Search");
|
||||||
txResult = rpc->call("getrawtransaction", {hash, 1});
|
txResult = rpc->call("getrawtransaction", {hash, 1});
|
||||||
gotTx = true;
|
gotTx = true;
|
||||||
} catch (...) {}
|
} catch (const std::exception& e) { noteErr(e); }
|
||||||
}
|
}
|
||||||
return [blockResult, txResult, gotBlock, gotTx]() {
|
return [blockResult, txResult, gotBlock, gotTx, rpcFailed, rpcErr]() {
|
||||||
s_search_loading = false;
|
s_search_loading = false;
|
||||||
if (gotBlock) {
|
if (gotBlock) {
|
||||||
int h = blockResult.value("height", -1);
|
int h = blockResult.value("height", -1);
|
||||||
@@ -407,6 +415,8 @@ static void navigateToHash(App* app, const std::string& hash) {
|
|||||||
s_tx_loading = false;
|
s_tx_loading = false;
|
||||||
s_expanded_tx_idx = 0;
|
s_expanded_tx_idx = 0;
|
||||||
s_show_detail_modal = false;
|
s_show_detail_modal = false;
|
||||||
|
} else if (rpcFailed) {
|
||||||
|
s_search_error = std::string(TR("explorer_lookup_failed")) + ": " + util::friendlyRpcError(rpcErr);
|
||||||
} else {
|
} else {
|
||||||
s_search_error = TR("explorer_hash_not_found");
|
s_search_error = TR("explorer_hash_not_found");
|
||||||
}
|
}
|
||||||
@@ -836,7 +846,8 @@ static void renderRecentBlocks(App* app, float availWidth) {
|
|||||||
|
|
||||||
std::map<int, BlockSummary> cachedBlocks;
|
std::map<int, BlockSummary> cachedBlocks;
|
||||||
if (!pageHeights.empty()) {
|
if (!pageHeights.empty()) {
|
||||||
cachedBlocks = s_recent_block_cache.loadRange(pageHeights.back(), pageHeights.front());
|
// Memoized on the cache revision: SQLite is only hit after the cache changed, not every frame.
|
||||||
|
cachedBlocks = s_recent_block_cache.loadRangeCached(pageHeights.back(), pageHeights.front());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fuzzy mode: a non-numeric, non-full-hash query filters the list to cached blocks whose hash
|
// Fuzzy mode: a non-numeric, non-full-hash query filters the list to cached blocks whose hash
|
||||||
@@ -880,7 +891,7 @@ static void renderRecentBlocks(App* app, float availWidth) {
|
|||||||
}
|
}
|
||||||
std::map<int, BlockSummary> nextCachedBlocks;
|
std::map<int, BlockSummary> nextCachedBlocks;
|
||||||
if (!nextPageHeights.empty()) {
|
if (!nextPageHeights.empty()) {
|
||||||
nextCachedBlocks = s_recent_block_cache.loadRange(nextPageHeights.back(), nextPageHeights.front());
|
nextCachedBlocks = s_recent_block_cache.loadRangeCached(nextPageHeights.back(), nextPageHeights.front());
|
||||||
}
|
}
|
||||||
for (int height : nextPageHeights) {
|
for (int height : nextPageHeights) {
|
||||||
if (nextCachedBlocks.find(height) == nextCachedBlocks.end()) {
|
if (nextCachedBlocks.find(height) == nextCachedBlocks.end()) {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include "../../rpc/rpc_worker.h"
|
#include "../../rpc/rpc_worker.h"
|
||||||
#include "../../util/i18n.h"
|
#include "../../util/i18n.h"
|
||||||
#include "../../util/platform.h"
|
#include "../../util/platform.h"
|
||||||
|
#include "../../util/export_filename.h"
|
||||||
#include "../notifications.h"
|
#include "../notifications.h"
|
||||||
#include "../schema/ui_schema.h"
|
#include "../schema/ui_schema.h"
|
||||||
#include "../material/draw_helpers.h"
|
#include "../material/draw_helpers.h"
|
||||||
@@ -117,7 +118,7 @@ void ExportAllKeysDialog::render(App* app)
|
|||||||
} else if (!s_status.empty()) {
|
} else if (!s_status.empty()) {
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::PushTextWrapPos(0.0f);
|
ImGui::PushTextWrapPos(0.0f);
|
||||||
ImGui::TextColored(ImVec4(0.8f, 0.3f, 0.3f, 1.0f), "%s", s_status.c_str());
|
ImGui::TextColored(material::ErrorVec4(), "%s", s_status.c_str());
|
||||||
ImGui::PopTextWrapPos();
|
ImGui::PopTextWrapPos();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,7 +161,10 @@ void ExportAllKeysDialog::render(App* app)
|
|||||||
if (s_include_t) {
|
if (s_include_t) {
|
||||||
for (const auto& a : state.t_addresses) t_addrs.push_back(a.address);
|
for (const auto& a : state.t_addresses) t_addrs.push_back(a.address);
|
||||||
}
|
}
|
||||||
std::string filename(s_filename);
|
// Sanitize the typed filename (basename only, no traversal) and reflect it back in the
|
||||||
|
// buffer so the user sees exactly what gets written. Mirrors the CSV export dialog.
|
||||||
|
std::string filename = util::sanitizeExportFilename(s_filename, ".txt", "dragonx_keys.txt");
|
||||||
|
snprintf(s_filename, sizeof(s_filename), "%s", filename.c_str());
|
||||||
|
|
||||||
// Run all key exports on worker thread
|
// Run all key exports on worker thread
|
||||||
if (app->worker()) {
|
if (app->worker()) {
|
||||||
|
|||||||
@@ -6,7 +6,9 @@
|
|||||||
#include "../../app.h"
|
#include "../../app.h"
|
||||||
#include "../../util/i18n.h"
|
#include "../../util/i18n.h"
|
||||||
#include "../../util/platform.h"
|
#include "../../util/platform.h"
|
||||||
|
#include "../../util/export_filename.h"
|
||||||
#include "../../data/transaction_csv.h"
|
#include "../../data/transaction_csv.h"
|
||||||
|
#include "transactions_tab.h"
|
||||||
#include "../notifications.h"
|
#include "../notifications.h"
|
||||||
#include "../schema/ui_schema.h"
|
#include "../schema/ui_schema.h"
|
||||||
#include "../material/draw_helpers.h"
|
#include "../material/draw_helpers.h"
|
||||||
@@ -18,6 +20,8 @@
|
|||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <ctime>
|
#include <ctime>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
|
||||||
namespace dragonx {
|
namespace dragonx {
|
||||||
namespace ui {
|
namespace ui {
|
||||||
@@ -27,12 +31,34 @@ static bool s_open = false;
|
|||||||
static char s_filename[256] = "";
|
static char s_filename[256] = "";
|
||||||
static std::string s_saved_path; // full path on success (shown as a copy field); empty otherwise
|
static std::string s_saved_path; // full path on success (shown as a copy field); empty otherwise
|
||||||
static std::string s_error; // error message on failure; empty otherwise
|
static std::string s_error; // error message on failure; empty otherwise
|
||||||
|
static int s_scope = 0; // 0 = all, 1 = current History filter, 2 = date range
|
||||||
|
static char s_from[16] = ""; // YYYY-MM-DD (date-range scope)
|
||||||
|
static char s_to[16] = "";
|
||||||
|
|
||||||
|
// Parse a YYYY-MM-DD string to the local-time start-of-day unix seconds. Returns false on empty/invalid.
|
||||||
|
static bool parseYmd(const char* s, std::int64_t& outStartOfDay) {
|
||||||
|
if (!s || !*s) return false;
|
||||||
|
int y = 0, mo = 0, d = 0;
|
||||||
|
if (std::sscanf(s, "%d-%d-%d", &y, &mo, &d) != 3) return false;
|
||||||
|
if (y < 1970 || y > 9999 || mo < 1 || mo > 12 || d < 1 || d > 31) return false;
|
||||||
|
std::tm tm{}; tm.tm_year = y - 1900; tm.tm_mon = mo - 1; tm.tm_mday = d; tm.tm_isdst = -1;
|
||||||
|
std::time_t t = std::mktime(&tm);
|
||||||
|
if (t == (std::time_t)-1) return false;
|
||||||
|
// Reject rollover (e.g. Feb 31 → Mar) by requiring the normalized fields to round-trip.
|
||||||
|
if (tm.tm_year != y - 1900 || tm.tm_mon != mo - 1 || tm.tm_mday != d) return false;
|
||||||
|
outStartOfDay = (std::int64_t)t;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void ExportTransactionsDialog::show()
|
void ExportTransactionsDialog::show()
|
||||||
{
|
{
|
||||||
s_open = true;
|
s_open = true;
|
||||||
s_saved_path.clear();
|
s_saved_path.clear();
|
||||||
s_error.clear();
|
s_error.clear();
|
||||||
|
// Pre-select the current History filter only when one is actually applied.
|
||||||
|
s_scope = GetTransactionsFilterSnapshot().active ? 1 : 0;
|
||||||
|
s_from[0] = '\0';
|
||||||
|
s_to[0] = '\0';
|
||||||
|
|
||||||
// Generate default filename with timestamp
|
// Generate default filename with timestamp
|
||||||
std::time_t now = std::time(nullptr);
|
std::time_t now = std::time(nullptr);
|
||||||
@@ -74,8 +100,64 @@ void ExportTransactionsDialog::render(App* app)
|
|||||||
if (!m::BeginOverlayDialog(ov)) return;
|
if (!m::BeginOverlayDialog(ov)) return;
|
||||||
|
|
||||||
const auto& state = app->getWalletState();
|
const auto& state = app->getWalletState();
|
||||||
|
const TxFilterSnapshot& snap = GetTransactionsFilterSnapshot();
|
||||||
|
const bool filterUsable = snap.valid && snap.active && snap.txRevision == state.tx_revision;
|
||||||
|
|
||||||
ImGui::Text(TR("export_tx_count"), state.transactions.size());
|
// ---- Export scope ----
|
||||||
|
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(m::OnSurfaceMedium()), "%s", TR("export_tx_scope"));
|
||||||
|
ImGui::RadioButton(TR("export_tx_scope_all"), &s_scope, 0);
|
||||||
|
if (!filterUsable) ImGui::BeginDisabled();
|
||||||
|
ImGui::RadioButton(TR("export_tx_scope_filter"), &s_scope, 1);
|
||||||
|
if (!filterUsable) ImGui::EndDisabled();
|
||||||
|
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) && !filterUsable)
|
||||||
|
m::Tooltip("%s", TR("export_tx_scope_filter_none"));
|
||||||
|
if (s_scope == 1 && filterUsable && !snap.description.empty()) {
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(m::OnSurfaceDisabled()), "(%s)", snap.description.c_str());
|
||||||
|
}
|
||||||
|
ImGui::RadioButton(TR("export_tx_scope_range"), &s_scope, 2);
|
||||||
|
|
||||||
|
std::int64_t fromT = 0, toT = 0; bool fromOk = false, toOk = false, rangeBad = false;
|
||||||
|
if (s_scope == 2) {
|
||||||
|
ImGui::Indent();
|
||||||
|
const float dw = 130.0f * Layout::dpiScale();
|
||||||
|
ImGui::SetNextItemWidth(dw);
|
||||||
|
ImGui::InputTextWithHint("##exfrom", TR("export_tx_range_from"), s_from, sizeof(s_from));
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::SetNextItemWidth(dw);
|
||||||
|
ImGui::InputTextWithHint("##exto", TR("export_tx_range_to"), s_to, sizeof(s_to));
|
||||||
|
fromOk = parseYmd(s_from, fromT);
|
||||||
|
toOk = parseYmd(s_to, toT);
|
||||||
|
if (toOk) toT += 24 * 3600 - 1; // inclusive end-of-day
|
||||||
|
if ((s_from[0] && !fromOk) || (s_to[0] && !toOk)) rangeBad = true;
|
||||||
|
if (fromOk && toOk && fromT > toT) rangeBad = true;
|
||||||
|
if (rangeBad)
|
||||||
|
ImGui::TextColored(m::ErrorVec4(), "%s", TR("export_tx_range_invalid"));
|
||||||
|
else
|
||||||
|
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(m::OnSurfaceDisabled()), "%s", TR("export_tx_range_pending_note"));
|
||||||
|
ImGui::Unindent();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve the subset for the chosen scope (scopeOk=false ⇒ can't export: stale snapshot / bad range).
|
||||||
|
std::vector<size_t> subsetIdx; bool scopeOk = true;
|
||||||
|
if (s_scope == 1) {
|
||||||
|
if (!filterUsable) scopeOk = false;
|
||||||
|
else for (size_t i : snap.indices) if (i < state.transactions.size()) subsetIdx.push_back(i);
|
||||||
|
} else if (s_scope == 2) {
|
||||||
|
if (rangeBad) scopeOk = false;
|
||||||
|
else for (size_t i = 0; i < state.transactions.size(); ++i) {
|
||||||
|
const auto& tx = state.transactions[i];
|
||||||
|
if (tx.timestamp <= 0) continue; // pending rows have no date
|
||||||
|
if (fromOk && tx.timestamp < fromT) continue;
|
||||||
|
if (toOk && tx.timestamp > toT) continue;
|
||||||
|
subsetIdx.push_back(i);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (size_t i = 0; i < state.transactions.size(); ++i) subsetIdx.push_back(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::Spacing();
|
||||||
|
ImGui::Text(TR("export_tx_count"), scopeOk ? subsetIdx.size() : (size_t)0);
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
|
||||||
// Filename + save-location hint.
|
// Filename + save-location hint.
|
||||||
@@ -94,6 +176,8 @@ void ExportTransactionsDialog::render(App* app)
|
|||||||
ImGui::SameLine(0, 4.0f * Layout::dpiScale());
|
ImGui::SameLine(0, 4.0f * Layout::dpiScale());
|
||||||
ImGui::TextColored(m::SuccessVec4(), "%s", TR("export_tx_success"));
|
ImGui::TextColored(m::SuccessVec4(), "%s", TR("export_tx_success"));
|
||||||
widgets::AddressCopyField("##exportedpath", s_saved_path);
|
widgets::AddressCopyField("##exportedpath", s_saved_path);
|
||||||
|
if (m::TactileButton(TR("export_tx_open_folder"), ImVec2(0, 0)))
|
||||||
|
util::Platform::openFolder(util::Platform::getConfigDir());
|
||||||
} else if (!s_error.empty()) {
|
} else if (!s_error.empty()) {
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::PushTextWrapPos(0.0f);
|
ImGui::PushTextWrapPos(0.0f);
|
||||||
@@ -109,14 +193,29 @@ void ExportTransactionsDialog::render(App* app)
|
|||||||
if (doExport) {
|
if (doExport) {
|
||||||
s_saved_path.clear();
|
s_saved_path.clear();
|
||||||
s_error.clear();
|
s_error.clear();
|
||||||
if (state.transactions.empty()) {
|
if (!scopeOk) {
|
||||||
|
// Stale filter snapshot (tx list changed) or an invalid date range — never silently export all.
|
||||||
|
Notifications::instance().warning(s_scope == 1 ? TR("export_tx_scope_stale") : TR("export_tx_range_invalid"));
|
||||||
|
} else if (subsetIdx.empty()) {
|
||||||
Notifications::instance().warning(TR("export_tx_none"));
|
Notifications::instance().warning(TR("export_tx_none"));
|
||||||
} else {
|
} else {
|
||||||
std::string configDir = util::Platform::getConfigDir();
|
std::string configDir = util::Platform::getConfigDir();
|
||||||
std::string filepath = configDir + "/" + s_filename;
|
// Sanitize in-buffer so the user sees exactly what was written (and we can't be pointed outside
|
||||||
// Build in memory, then write atomically (temp + fsync + rename): a disk-full /
|
// the config dir by a pasted path).
|
||||||
// removable-media failure is reported instead of leaving a truncated CSV behind a success toast.
|
std::string safeName = util::sanitizeExportFilename(s_filename, ".csv", "dragonx_transactions.csv");
|
||||||
const std::string csv = data::formatTransactionsCsv(state.transactions);
|
snprintf(s_filename, sizeof(s_filename), "%s", safeName.c_str());
|
||||||
|
std::string filepath = configDir + "/" + safeName;
|
||||||
|
// Build the chosen subset (scope 0 exports the whole list with no copy), then write atomically
|
||||||
|
// (temp + fsync + rename) so a disk-full failure is reported instead of a truncated CSV.
|
||||||
|
std::string csv;
|
||||||
|
if (s_scope == 0) {
|
||||||
|
csv = data::formatTransactionsCsv(state.transactions);
|
||||||
|
} else {
|
||||||
|
std::vector<TransactionInfo> subset;
|
||||||
|
subset.reserve(subsetIdx.size());
|
||||||
|
for (size_t i : subsetIdx) subset.push_back(state.transactions[i]);
|
||||||
|
csv = data::formatTransactionsCsv(subset);
|
||||||
|
}
|
||||||
if (!util::Platform::writeFileAtomically(filepath, csv)) {
|
if (!util::Platform::writeFileAtomically(filepath, csv)) {
|
||||||
s_error = TR("export_tx_file_fail");
|
s_error = TR("export_tx_file_fail");
|
||||||
Notifications::instance().error(TR("export_tx_file_fail"));
|
Notifications::instance().error(TR("export_tx_file_fail"));
|
||||||
|
|||||||
@@ -121,9 +121,9 @@ void KeyExportDialog::render(App* app)
|
|||||||
ImGui::Text("%s", key_label);
|
ImGui::Text("%s", key_label);
|
||||||
|
|
||||||
if (s_fetching) {
|
if (s_fetching) {
|
||||||
ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f), "%s", TR("key_export_fetching"));
|
ImGui::TextColored(material::WarningVec4(), "%s%s", TR("key_export_fetching_base"), material::LoadingDots());
|
||||||
} else if (!s_error.empty()) {
|
} else if (!s_error.empty()) {
|
||||||
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), TR("error_format"), s_error.c_str());
|
ImGui::TextColored(material::ErrorVec4(), TR("error_format"), s_error.c_str());
|
||||||
// Not a dead-end: clearing the error falls back through to the Reveal button next frame.
|
// Not a dead-end: clearing the error falls back through to the Reveal button next frame.
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
if (material::TactileButton(TR("retry"), ImVec2(revealBtn.width, 0), S.resolveFont(revealBtn.font))) {
|
if (material::TactileButton(TR("retry"), ImVec2(revealBtn.width, 0), S.resolveFont(revealBtn.font))) {
|
||||||
@@ -132,7 +132,17 @@ void KeyExportDialog::render(App* app)
|
|||||||
} else if (s_key.empty() && s_address.length() < 26) {
|
} else if (s_key.empty() && s_address.length() < 26) {
|
||||||
// Guard against an empty/blatantly-malformed address before dispatching a doomed reveal.
|
// Guard against an empty/blatantly-malformed address before dispatching a doomed reveal.
|
||||||
// (Also avoids reading s_address[0] on an empty string in the Reveal branch below.)
|
// (Also avoids reading s_address[0] on an empty string in the Reveal branch below.)
|
||||||
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "%s", TR("invalid_address"));
|
ImGui::TextColored(material::ErrorVec4(), "%s", TR("invalid_address"));
|
||||||
|
} else if (s_key.empty() && app->getWalletState().isLocked()) {
|
||||||
|
// Reveal would just fail with a locked-wallet RPC error — say so and offer to unlock.
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, material::Warning());
|
||||||
|
ImGui::TextWrapped("%s", TR("key_export_unlock_first"));
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
ImGui::Spacing();
|
||||||
|
if (material::TactileButton(TR("lite_unlock"), ImVec2(revealBtn.width, 0), S.resolveFont(revealBtn.font))) {
|
||||||
|
if (app->liteWallet()) app->requestLiteUnlock();
|
||||||
|
else s_open = false; // full node: closing reveals the lock screen already shown while locked
|
||||||
|
}
|
||||||
} else if (s_key.empty()) {
|
} else if (s_key.empty()) {
|
||||||
// Show button to fetch key
|
// Show button to fetch key
|
||||||
if (material::TactileButton(TR("key_export_reveal"), ImVec2(revealBtn.width, 0), S.resolveFont(revealBtn.font))) {
|
if (material::TactileButton(TR("key_export_reveal"), ImVec2(revealBtn.width, 0), S.resolveFont(revealBtn.font))) {
|
||||||
@@ -173,7 +183,7 @@ void KeyExportDialog::render(App* app)
|
|||||||
s_key = found;
|
s_key = found;
|
||||||
s_show_key = wantViewing; // viewing keys are less sensitive
|
s_show_key = wantViewing; // viewing keys are less sensitive
|
||||||
} else {
|
} else {
|
||||||
s_error = r.ok ? std::string(TR("grpc_key_not_available")) : r.error;
|
s_error = r.ok ? std::string(TR("grpc_key_not_available")) : util::friendlyRpcError(r.error);
|
||||||
}
|
}
|
||||||
wallet::secureWipeLiteSecret(found);
|
wallet::secureWipeLiteSecret(found);
|
||||||
s_fetching = false;
|
s_fetching = false;
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <ctime>
|
#include <ctime>
|
||||||
|
#include <chrono>
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
|
|
||||||
namespace dragonx {
|
namespace dragonx {
|
||||||
@@ -1430,7 +1431,7 @@ static void mktDrawPairSelector(App* app, const std::vector<data::ExchangeInfo>&
|
|||||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("market_attribution"));
|
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("market_attribution"));
|
||||||
if (!market.last_updated.empty()) {
|
if (!market.last_updated.empty()) {
|
||||||
ImGui::SameLine(0, 12);
|
ImGui::SameLine(0, 12.0f * Layout::dpiScale());
|
||||||
snprintf(buf, sizeof(buf), TR("market_updated"), market.last_updated.c_str());
|
snprintf(buf, sizeof(buf), TR("market_updated"), market.last_updated.c_str());
|
||||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), buf);
|
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), buf);
|
||||||
}
|
}
|
||||||
@@ -1496,7 +1497,18 @@ static void mktDrawPriceHero(const MktCtx& cx)
|
|||||||
// ---- HERO PRICE (large, prominent) ----
|
// ---- HERO PRICE (large, prominent) ----
|
||||||
ImFont* h3 = Type().h3();
|
ImFont* h3 = Type().h3();
|
||||||
std::string priceStr = FormatPrice(heroPrice);
|
std::string priceStr = FormatPrice(heroPrice);
|
||||||
ImU32 priceCol = Success();
|
// Stale-price cue: the aggregate price is kept on a failed/old refresh (it's never cleared), so a
|
||||||
|
// green number can be hours out of date. Flag it amber when the last refresh failed or the
|
||||||
|
// aggregate is >=5 min old. Only when the hero IS the aggregate (an exchange pair has its own feed).
|
||||||
|
const bool haveFetch = market.last_fetch_time.time_since_epoch().count() != 0;
|
||||||
|
const long ageMin = haveFetch
|
||||||
|
? (long)std::chrono::duration_cast<std::chrono::minutes>(
|
||||||
|
std::chrono::steady_clock::now() - market.last_fetch_time).count()
|
||||||
|
: 0;
|
||||||
|
const bool usingAggregate = (heroPrice == market.price_usd);
|
||||||
|
const bool priceStale = usingAggregate && haveFetch &&
|
||||||
|
(!market.price_error.empty() || ageMin >= 5);
|
||||||
|
ImU32 priceCol = priceStale ? Warning() : Success();
|
||||||
DrawTextShadow(dl, h3, h3->LegacySize, ImVec2(cx0, cy), priceCol, priceStr.c_str());
|
DrawTextShadow(dl, h3, h3->LegacySize, ImVec2(cx0, cy), priceCol, priceStr.c_str());
|
||||||
|
|
||||||
// Ticker label after price
|
// Ticker label after price
|
||||||
@@ -1519,6 +1531,29 @@ static void mktDrawPriceHero(const MktCtx& cx)
|
|||||||
dl->AddRectFilled(bMin, bMax, badgeBg, 4.0f * dp);
|
dl->AddRectFilled(bMin, bMax, badgeBg, 4.0f * dp);
|
||||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(bMin.x + badgePadH, bMin.y + badgePadV), chgCol, buf);
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(bMin.x + badgePadH, bMin.y + badgePadV), chgCol, buf);
|
||||||
|
|
||||||
|
// Stale "as of Nh ago" caption + hover tooltip, right of the change badge (cursor saved/restored
|
||||||
|
// so the custom draw-list layout below is undisturbed).
|
||||||
|
if (priceStale) {
|
||||||
|
char age[64];
|
||||||
|
if (ageMin >= 60) snprintf(age, sizeof(age), TR("market_price_stale_hour"), (int)(ageMin / 60));
|
||||||
|
else snprintf(age, sizeof(age), TR("market_price_stale_min"), (int)std::max(1L, ageMin));
|
||||||
|
ImVec2 asz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, age);
|
||||||
|
float ax = bMax.x + Layout::spacingMd();
|
||||||
|
float limit = cardMax.x - pad - 260.0f * dp; // reserve the trade pill + chart toggles
|
||||||
|
if (ax + asz.x < limit) {
|
||||||
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(ax, bMin.y + badgePadV), Warning(), age);
|
||||||
|
ImVec2 sv = ImGui::GetCursorScreenPos();
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(ax, bMin.y));
|
||||||
|
ImGui::InvisibleButton("##PriceStale", ImVec2(asz.x, bMax.y - bMin.y));
|
||||||
|
if (ImGui::IsItemHovered()) {
|
||||||
|
std::string tip = market.price_error.empty()
|
||||||
|
? std::string(TR("market_price_fetch_failed")) : market.price_error;
|
||||||
|
material::Tooltip(TR("market_price_stale_tip"), tip.c_str());
|
||||||
|
}
|
||||||
|
ImGui::SetCursorScreenPos(sv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// (Divider is drawn in the chart block, below the interval buttons.)
|
// (Divider is drawn in the chart block, below the interval buttons.)
|
||||||
|
|
||||||
// ---- TRADE BUTTON (top-right of card) ----
|
// ---- TRADE BUTTON (top-right of card) ----
|
||||||
|
|||||||
@@ -957,6 +957,8 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
|
|||||||
material::Tooltip("%s", isMiningActive ? TR("mining_stopping_tooltip") : TR("mining_starting_tooltip"));
|
material::Tooltip("%s", isMiningActive ? TR("mining_stopping_tooltip") : TR("mining_starting_tooltip"));
|
||||||
else if (isSyncing && !s_pool_mode)
|
else if (isSyncing && !s_pool_mode)
|
||||||
material::Tooltip(TR("mining_syncing_tooltip"), state.sync.verification_progress * 100.0);
|
material::Tooltip(TR("mining_syncing_tooltip"), state.sync.verification_progress * 100.0);
|
||||||
|
else if (!s_pool_mode && !poolStillRunning && !app->isConnected())
|
||||||
|
material::Tooltip("%s", TR("mining_not_connected_tooltip")); // was falling through to "Start Mining" on a dead button
|
||||||
else if (poolBlockedBySolo)
|
else if (poolBlockedBySolo)
|
||||||
material::Tooltip("%s", TR("mining_stop_solo_for_pool"));
|
material::Tooltip("%s", TR("mining_stop_solo_for_pool"));
|
||||||
else if (poolNeedsPayout)
|
else if (poolNeedsPayout)
|
||||||
@@ -989,6 +991,30 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
|
|||||||
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
|
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
|
||||||
ImGui::Dummy(ImVec2(availWidth, 0));
|
ImGui::Dummy(ImVec2(availWidth, 0));
|
||||||
|
|
||||||
|
// Persistent error caption (a crashed/AV-blocked/failed miner otherwise leaves a button that
|
||||||
|
// "did nothing" once its toast faded). Normal ImGui flow, OUTSIDE the hand-drawn button card.
|
||||||
|
const std::string& merr = app->miningLastError();
|
||||||
|
if (!merr.empty() && !app->isMiningToggleInProgress()) {
|
||||||
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||||
|
ImGui::PushTextWrapPos(cardMin.x + availWidth);
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Error()));
|
||||||
|
ImGui::TextWrapped("%s", merr.c_str());
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium()));
|
||||||
|
ImGui::TextUnformatted(TR("mining_error_retry_hint"));
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
ImGui::PopTextWrapPos();
|
||||||
|
#ifdef _WIN32
|
||||||
|
if (app->miningErrorIsAntivirus()) {
|
||||||
|
ImGui::SameLine(0, Layout::spacingSm());
|
||||||
|
if (ImGui::SmallButton(TR("mining_error_av_help"))) app->showAntivirusHelp();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
ImGui::SameLine(0, Layout::spacingSm());
|
||||||
|
if (ImGui::SmallButton(TR("mining_error_dismiss"))) app->clearMiningError();
|
||||||
|
}
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, gap));
|
ImGui::Dummy(ImVec2(0, gap));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -191,18 +191,18 @@ void RenderMiningEarnings(App* app, const WalletState& state, const MiningInfo&
|
|||||||
};
|
};
|
||||||
|
|
||||||
snprintf(valBuf, sizeof(valBuf), "+%.4f", minedToday);
|
snprintf(valBuf, sizeof(valBuf), "+%.4f", minedToday);
|
||||||
snprintf(subBuf2, sizeof(subBuf2), "(%d txn)", minedTodayCount);
|
snprintf(subBuf2, sizeof(subBuf2), TR("mining_txn_count"), minedTodayCount);
|
||||||
char todayVal[64], todaySub[64];
|
char todayVal[64], todaySub[64];
|
||||||
strncpy(todayVal, valBuf, sizeof(todayVal));
|
strncpy(todayVal, valBuf, sizeof(todayVal));
|
||||||
strncpy(todaySub, subBuf2, sizeof(todaySub));
|
strncpy(todaySub, subBuf2, sizeof(todaySub));
|
||||||
|
|
||||||
char yesterdayVal[64], yesterdaySub[64];
|
char yesterdayVal[64], yesterdaySub[64];
|
||||||
snprintf(yesterdayVal, sizeof(yesterdayVal), "+%.4f", minedYesterday);
|
snprintf(yesterdayVal, sizeof(yesterdayVal), "+%.4f", minedYesterday);
|
||||||
snprintf(yesterdaySub, sizeof(yesterdaySub), "(%d txn)", minedYesterdayCount);
|
snprintf(yesterdaySub, sizeof(yesterdaySub), TR("mining_txn_count"), minedYesterdayCount);
|
||||||
|
|
||||||
char allVal[64], allSub[64];
|
char allVal[64], allSub[64];
|
||||||
snprintf(allVal, sizeof(allVal), "+%.4f", minedAllTime);
|
snprintf(allVal, sizeof(allVal), "+%.4f", minedAllTime);
|
||||||
snprintf(allSub, sizeof(allSub), "(%d txn)", minedAllTimeCount);
|
snprintf(allSub, sizeof(allSub), TR("mining_txn_count"), minedAllTimeCount);
|
||||||
|
|
||||||
char estVal[64];
|
char estVal[64];
|
||||||
if (estActive)
|
if (estActive)
|
||||||
@@ -324,6 +324,11 @@ void RenderMiningEarnings(App* app, const WalletState& state, const MiningInfo&
|
|||||||
|
|
||||||
// -- Mining Address --
|
// -- Mining Address --
|
||||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(col3X, cy), OnSurfaceMedium(), TR("mining_mining_addr"));
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(col3X, cy), OnSurfaceMedium(), TR("mining_mining_addr"));
|
||||||
|
{
|
||||||
|
ImVec2 lblSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, TR("mining_mining_addr"));
|
||||||
|
if (material::IsRectHovered(ImVec2(col3X, cy), ImVec2(col3X + lblSz.x, cy + lblSz.y)))
|
||||||
|
material::Tooltip("%s", TR("mining_addr_explainer"));
|
||||||
|
}
|
||||||
std::string mining_address = "";
|
std::string mining_address = "";
|
||||||
for (const auto& addr : state.addresses) {
|
for (const auto& addr : state.addresses) {
|
||||||
if (addr.type == "transparent" && !addr.address.empty()) {
|
if (addr.type == "transparent" && !addr.address.empty()) {
|
||||||
@@ -351,6 +356,12 @@ void RenderMiningEarnings(App* app, const WalletState& state, const MiningInfo&
|
|||||||
ImVec2(Layout::spacingMd(), 4 * dp))) {
|
ImVec2(Layout::spacingMd(), 4 * dp))) {
|
||||||
Notifications::instance().info(TR("mining_address_copied"));
|
Notifications::instance().info(TR("mining_address_copied"));
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// No mining (transparent) address yet — explain where rewards go instead of a blank.
|
||||||
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(col3X + valOffX, cy), OnSurfaceMedium(), TR("mining_addr_none"));
|
||||||
|
ImVec2 emSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, TR("mining_addr_none"));
|
||||||
|
if (material::IsRectHovered(ImVec2(col3X + valOffX, cy), ImVec2(col3X + valOffX + emSz.x, cy + emSz.y)))
|
||||||
|
material::Tooltip("%s", TR("mining_addr_none_tip"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -146,6 +146,8 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo
|
|||||||
ImGui::SetCursorScreenPos(ImVec2(tMin.x, tMax.y));
|
ImGui::SetCursorScreenPos(ImVec2(tMin.x, tMax.y));
|
||||||
ImGui::Dummy(ImVec2(totalW, 0));
|
ImGui::Dummy(ImVec2(totalW, 0));
|
||||||
|
|
||||||
|
bool payoutBadThisFrame = false; // set when the inline payout error is drawn; reserves a row below
|
||||||
|
|
||||||
// Pool URL + Worker inputs inline next to toggle (pool mode only)
|
// Pool URL + Worker inputs inline next to toggle (pool mode only)
|
||||||
if (s_pool_mode && soloMiningActive) {
|
if (s_pool_mode && soloMiningActive) {
|
||||||
// Solo mining is active — show disabled message instead of inputs
|
// Solo mining is active — show disabled message instead of inputs
|
||||||
@@ -427,6 +429,24 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Persistent inline payout error — a hover-only tooltip is missed by anyone who doesn't hover,
|
||||||
|
// so outline the field in red and draw a short reason below it (draw-list only; no layout calls
|
||||||
|
// here, the cursor is hand-positioned). The field height is reserved at the end of the function.
|
||||||
|
{
|
||||||
|
const ImVec2 wMin = ImGui::GetItemRectMin();
|
||||||
|
const ImVec2 wMax = ImGui::GetItemRectMax();
|
||||||
|
const std::string wk(s_pool_worker);
|
||||||
|
if (!wk.empty() && wk != "x" && !util::isValidRecipientAddress(wk)) {
|
||||||
|
payoutBadThisFrame = true;
|
||||||
|
dl->AddRect(wMin, wMax, Error(), 4.0f * dp, 0, 1.5f * dp);
|
||||||
|
const char* msg = util::isTransparentAddress(wk)
|
||||||
|
? TR("mining_payout_needs_shielded_short") : TR("mining_payout_invalid_short");
|
||||||
|
std::string shown = material::TruncateToWidth(msg, capFont, capFont->LegacySize, wrkGroupW);
|
||||||
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(wMin.x, wMax.y + 1.0f * dp),
|
||||||
|
ReadableError(), shown.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- Worker: Dropdown arrow button ---
|
// --- Worker: Dropdown arrow button ---
|
||||||
ImGui::SameLine(0, 0);
|
ImGui::SameLine(0, 0);
|
||||||
{
|
{
|
||||||
@@ -642,7 +662,9 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo
|
|||||||
// Ensure cursor Y is at toggle bottom regardless of pool input widgets,
|
// Ensure cursor Y is at toggle bottom regardless of pool input widgets,
|
||||||
// so the cards below stay at the same position in both solo and pool modes.
|
// so the cards below stay at the same position in both solo and pool modes.
|
||||||
ImGui::SetCursorScreenPos(ImVec2(tMin.x, tMax.y));
|
ImGui::SetCursorScreenPos(ImVec2(tMin.x, tMax.y));
|
||||||
ImGui::Dummy(ImVec2(0, gap * 0.5f));
|
// Reserve a row for the inline payout error so it doesn't overlap the card below (cards shift down
|
||||||
|
// transiently while the error is shown).
|
||||||
|
ImGui::Dummy(ImVec2(0, gap * 0.5f + (payoutBadThisFrame ? capFont->LegacySize + 2.0f * dp : 0.0f)));
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace ui
|
} // namespace ui
|
||||||
|
|||||||
@@ -243,11 +243,26 @@ static void RenderLeftPoolCard(App* app, const WalletState& state, ImDrawList* d
|
|||||||
{
|
{
|
||||||
char uptBuf[64];
|
char uptBuf[64];
|
||||||
int64_t up = pm.uptime_sec;
|
int64_t up = pm.uptime_sec;
|
||||||
if (up <= 0) snprintf(uptBuf, sizeof(uptBuf), "N/A");
|
if (up <= 0) snprintf(uptBuf, sizeof(uptBuf), "%s", TR("grpc_na"));
|
||||||
else if (up < 3600) snprintf(uptBuf, sizeof(uptBuf), "%lldm %llds", (long long)(up / 60), (long long)(up % 60));
|
else if (up < 3600) snprintf(uptBuf, sizeof(uptBuf), TR("mining_uptime_ms"), (long long)(up / 60), (long long)(up % 60));
|
||||||
else snprintf(uptBuf, sizeof(uptBuf), "%lldh %lldm", (long long)(up / 3600), (long long)((up % 3600) / 60));
|
else snprintf(uptBuf, sizeof(uptBuf), TR("mining_uptime_hm"), (long long)(up / 3600), (long long)((up % 3600) / 60));
|
||||||
metricRow(TR("mining_uptime"), uptBuf, OnSurface());
|
metricRow(TR("mining_uptime"), uptBuf, OnSurface());
|
||||||
}
|
}
|
||||||
|
// Connection status — replaces the old transient "Connecting" banner, with a timeout so a bad URL /
|
||||||
|
// blocked port flips from "Connecting" to "Can't reach pool" instead of spinning on red forever.
|
||||||
|
{
|
||||||
|
static constexpr int kPoolConnectTimeoutSec = 30;
|
||||||
|
const bool shares = pm.accepted + pm.rejected > 0;
|
||||||
|
const bool ok = pm.connected || shares || pm.hashrate_10s > 0;
|
||||||
|
const bool failed = pm.xmrig_running && !ok && pm.running_since > 0 &&
|
||||||
|
(std::time(nullptr) - pm.running_since) >= kPoolConnectTimeoutSec;
|
||||||
|
const char* stateText; ImU32 col;
|
||||||
|
if (ok) { stateText = TR("mining_status_connected"); col = Success(); }
|
||||||
|
else if (failed) { stateText = TR("mining_pool_unreachable"); col = Error(); }
|
||||||
|
else if (pm.xmrig_running) { stateText = TR("mining_status_connecting"); col = Warning(); }
|
||||||
|
else { stateText = TR("mining_status_stopped"); col = OnSurfaceDisabled(); }
|
||||||
|
metricRow(TR("mining_status_label"), stateText, col);
|
||||||
|
}
|
||||||
y += gap * 0.5f;
|
y += gap * 0.5f;
|
||||||
|
|
||||||
// The pool list = official pools ∪ user-saved favorites ∪ the current custom pool.
|
// The pool list = official pools ∪ user-saved favorites ∪ the current custom pool.
|
||||||
@@ -397,7 +412,7 @@ void RenderMiningStats(const WalletState& state, const MiningInfo& mining,
|
|||||||
const float mRowH = sub1->LegacySize + 7 * dp;
|
const float mRowH = sub1->LegacySize + 7 * dp;
|
||||||
const float listMinH = (capFont->LegacySize + 10 * dp) * 2.6f; // ~2–3 rows before scroll
|
const float listMinH = (capFont->LegacySize + 10 * dp) * 2.6f; // ~2–3 rows before scroll
|
||||||
float leftContentH = pad * 0.75f + toggleH + gap
|
float leftContentH = pad * 0.75f + toggleH + gap
|
||||||
+ mRowH * 4.0f + gap * 0.5f
|
+ mRowH * 5.0f + gap * 0.5f // 4 metric rows + the connection-status row
|
||||||
+ (ovFont->LegacySize + 4 * dp) + listMinH + pad * 0.75f;
|
+ (ovFont->LegacySize + 4 * dp) + listMinH + pad * 0.75f;
|
||||||
float cardH = std::max(leftContentH, statRowH + chartBudgetH + pad);
|
float cardH = std::max(leftContentH, statRowH + chartBudgetH + pad);
|
||||||
|
|
||||||
|
|||||||
@@ -335,24 +335,9 @@ static void RenderMiningTabContent(App* app)
|
|||||||
isMiningActive, s_pool_mode, availWidth, glassSpec, sHdr, gapOver);
|
isMiningActive, s_pool_mode, availWidth, glassSpec, sHdr, gapOver);
|
||||||
|
|
||||||
// ================================================================
|
// ================================================================
|
||||||
// POOL CONNECTION STATUS — inline indicator (pool mode, no log)
|
// (Pool connection status now lives as a persistent, timeout-aware row in RenderLeftPoolCard
|
||||||
// ================================================================
|
// (mining_stats.cpp) — the old transient "Connecting" banner that vanished once logs arrived and
|
||||||
if (s_pool_mode && state.pool_mining.log_lines.empty() && state.pool_mining.xmrig_running) {
|
// sat on red forever on failure was removed.)
|
||||||
ImFont* iconFont = Type().iconSmall();
|
|
||||||
const char* dotIcon = ICON_MD_CIRCLE;
|
|
||||||
ImU32 dotCol = state.pool_mining.connected ? WithAlpha(Success(), 200) : WithAlpha(Error(), 200);
|
|
||||||
const char* statusText = state.pool_mining.connected
|
|
||||||
? (state.pool_mining.pool_url.empty() ? TR("mining_connected") : state.pool_mining.pool_url.c_str())
|
|
||||||
: TR("mining_connecting");
|
|
||||||
|
|
||||||
ImVec2 pos = ImGui::GetCursorScreenPos();
|
|
||||||
ImVec2 dotSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, dotIcon);
|
|
||||||
dl->AddText(iconFont, iconFont->LegacySize, pos, dotCol, dotIcon);
|
|
||||||
dl->AddText(capFont, capFont->LegacySize,
|
|
||||||
ImVec2(pos.x + dotSz.x + 4 * dp, pos.y + (dotSz.y - capFont->LegacySize) * 0.5f),
|
|
||||||
OnSurfaceMedium(), statusText);
|
|
||||||
ImGui::Dummy(ImVec2(availWidth, capFont->LegacySize + 4 * dp));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace ui
|
} // namespace ui
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ using namespace material;
|
|||||||
// Track selected peer for ban action
|
// Track selected peer for ban action
|
||||||
static int s_selected_peer_idx = -1;
|
static int s_selected_peer_idx = -1;
|
||||||
static int s_selected_banned_idx = -1;
|
static int s_selected_banned_idx = -1;
|
||||||
|
static bool s_confirm_clear_bans = false; // inline confirm for the bulk Clear-All-Bans action
|
||||||
|
|
||||||
// Helper: Extract IP without port
|
// Helper: Extract IP without port
|
||||||
static std::string ExtractIP(const std::string& addr)
|
static std::string ExtractIP(const std::string& addr)
|
||||||
@@ -775,9 +776,11 @@ void RenderPeersTab(App* app)
|
|||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
if (ImGui::MenuItem(TR("copy_address"))) {
|
if (ImGui::MenuItem(TR("copy_address"))) {
|
||||||
ImGui::SetClipboardText(peer.addr.c_str());
|
ImGui::SetClipboardText(peer.addr.c_str());
|
||||||
|
ui::Notifications::instance().info(TR("copied_to_clipboard"));
|
||||||
}
|
}
|
||||||
if (ImGui::MenuItem(TR("peers_copy_ip"))) {
|
if (ImGui::MenuItem(TR("peers_copy_ip"))) {
|
||||||
ImGui::SetClipboardText(ExtractIP(peer.addr).c_str());
|
ImGui::SetClipboardText(ExtractIP(peer.addr).c_str());
|
||||||
|
ui::Notifications::instance().info(TR("copied_to_clipboard"));
|
||||||
}
|
}
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
if (ImGui::MenuItem(TR("peers_ban_24h"))) {
|
if (ImGui::MenuItem(TR("peers_ban_24h"))) {
|
||||||
@@ -891,6 +894,7 @@ void RenderPeersTab(App* app)
|
|||||||
if (effects::ImGuiAcrylic::BeginAcrylicContextItem(nullptr, 0, acrylicTheme2.menu)) {
|
if (effects::ImGuiAcrylic::BeginAcrylicContextItem(nullptr, 0, acrylicTheme2.menu)) {
|
||||||
if (ImGui::MenuItem(TR("copy_address"))) {
|
if (ImGui::MenuItem(TR("copy_address"))) {
|
||||||
ImGui::SetClipboardText(banned.address.c_str());
|
ImGui::SetClipboardText(banned.address.c_str());
|
||||||
|
ui::Notifications::instance().info(TR("copied_to_clipboard"));
|
||||||
}
|
}
|
||||||
if (ImGui::MenuItem(TR("peers_unban"))) {
|
if (ImGui::MenuItem(TR("peers_unban"))) {
|
||||||
app->unbanPeer(banned.address);
|
app->unbanPeer(banned.address);
|
||||||
@@ -931,10 +935,25 @@ void RenderPeersTab(App* app)
|
|||||||
// ================================================================
|
// ================================================================
|
||||||
if (s_show_banned && !state.bannedPeers.empty()) {
|
if (s_show_banned && !state.bannedPeers.empty()) {
|
||||||
ImGui::BeginDisabled(!app->isConnected());
|
ImGui::BeginDisabled(!app->isConnected());
|
||||||
if (TactileSmallButton(TR("peers_clear_all_bans"), S.resolveFont("button"))) {
|
if (!s_confirm_clear_bans) {
|
||||||
|
if (TactileSmallButton(TR("peers_clear_all_bans"), S.resolveFont("button")))
|
||||||
|
s_confirm_clear_bans = true; // bulk action takes a confirming second click
|
||||||
|
} else {
|
||||||
|
char cb[96];
|
||||||
|
snprintf(cb, sizeof(cb), TR("peers_clear_bans_confirm"), (int)state.bannedPeers.size());
|
||||||
|
ImGui::TextColored(material::WarningVec4(), "%s", cb);
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (TactileSmallButton(TR("peers_clear_bans_confirm_yes"), S.resolveFont("button"))) {
|
||||||
app->clearBans();
|
app->clearBans();
|
||||||
|
s_confirm_clear_bans = false;
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (TactileSmallButton(TR("cancel"), S.resolveFont("button")))
|
||||||
|
s_confirm_clear_bans = false;
|
||||||
}
|
}
|
||||||
ImGui::EndDisabled();
|
ImGui::EndDisabled();
|
||||||
|
} else {
|
||||||
|
s_confirm_clear_bans = false; // no stale confirm across tab toggles / after bans clear
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::EndChild(); // ##PeersScroll
|
ImGui::EndChild(); // ##PeersScroll
|
||||||
|
|||||||
@@ -74,7 +74,9 @@ void QRPopupDialog::render(App* app)
|
|||||||
const float addrBoxW = ImGui::CalcTextSize(chunkedAddr.c_str()).x + addrPadX * 2.0f + 2.0f;
|
const float addrBoxW = ImGui::CalcTextSize(chunkedAddr.c_str()).x + addrPadX * 2.0f + 2.0f;
|
||||||
// The BlurFloat overlay card insets content by 28px per side (draw_helpers BeginOverlayDialog); add
|
// The BlurFloat overlay card insets content by 28px per side (draw_helpers BeginOverlayDialog); add
|
||||||
// that (+ a little slack) so the address box fits on one line under its label instead of wrapping.
|
// that (+ a little slack) so the address box fits on one line under its label instead of wrapping.
|
||||||
const float wantW = addrBoxW + 28.0f * 2.0f + 12.0f;
|
// Scale the overlay inset (BeginOverlayDialog insets 28 LOGICAL px/side) into the same scaled space
|
||||||
|
// as addrBoxW before the /dp conversion below, so the address box doesn't wrap at HiDPI.
|
||||||
|
const float wantW = addrBoxW + (28.0f * 2.0f + 12.0f) * dp;
|
||||||
const float maxW = 0.85f * ImGui::GetMainViewport()->Size.x;
|
const float maxW = 0.85f * ImGui::GetMainViewport()->Size.x;
|
||||||
const float cardW = (wantW < maxW ? wantW : maxW) / dp;
|
const float cardW = (wantW < maxW ? wantW : maxW) / dp;
|
||||||
material::OverlayDialogSpec ov;
|
material::OverlayDialogSpec ov;
|
||||||
|
|||||||
@@ -333,10 +333,11 @@ static void RenderAddressDropdown(App* app, float width) {
|
|||||||
} else {
|
} else {
|
||||||
s_generating_address = true;
|
s_generating_address = true;
|
||||||
if (s_addr_type_filter != 2) {
|
if (s_addr_type_filter != 2) {
|
||||||
app->createNewZAddress([](const std::string& addr) {
|
app->createNewZAddress([app](const std::string& addr) {
|
||||||
s_generating_address = false;
|
s_generating_address = false;
|
||||||
if (addr.empty())
|
if (addr.empty())
|
||||||
Notifications::instance().error(TR("failed_create_shielded"));
|
Notifications::instance().error(app->lastAddressError().empty()
|
||||||
|
? std::string(TR("failed_create_shielded")) : app->lastAddressError());
|
||||||
else {
|
else {
|
||||||
s_pending_select_address = addr;
|
s_pending_select_address = addr;
|
||||||
Notifications::instance().success(TR("new_shielded_created"));
|
Notifications::instance().success(TR("new_shielded_created"));
|
||||||
@@ -400,10 +401,11 @@ static void RenderMiningAddressNotice(App* app, const WalletState& state, float
|
|||||||
s_cached_qr_data.clear();
|
s_cached_qr_data.clear();
|
||||||
} else {
|
} else {
|
||||||
s_generating_address = true; // no shielded address yet: make one, as "New" does
|
s_generating_address = true; // no shielded address yet: make one, as "New" does
|
||||||
app->createNewZAddress([](const std::string& addr) {
|
app->createNewZAddress([app](const std::string& addr) {
|
||||||
s_generating_address = false;
|
s_generating_address = false;
|
||||||
if (addr.empty())
|
if (addr.empty())
|
||||||
Notifications::instance().error(TR("failed_create_shielded"));
|
Notifications::instance().error(app->lastAddressError().empty()
|
||||||
|
? std::string(TR("failed_create_shielded")) : app->lastAddressError());
|
||||||
else {
|
else {
|
||||||
s_pending_select_address = addr;
|
s_pending_select_address = addr;
|
||||||
Notifications::instance().success(TR("new_shielded_created"));
|
Notifications::instance().success(TR("new_shielded_created"));
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ static uintptr_t s_qr_texture = 0;
|
|||||||
static int s_qr_width = 0;
|
static int s_qr_width = 0;
|
||||||
static int s_qr_height = 0;
|
static int s_qr_height = 0;
|
||||||
static bool s_uri_dirty = true;
|
static bool s_uri_dirty = true;
|
||||||
|
static bool s_creating_address = false; // true while a Create-shielded-address RPC is in flight
|
||||||
|
|
||||||
// Helper to build payment URI
|
// Helper to build payment URI
|
||||||
static std::string buildPaymentUri()
|
static std::string buildPaymentUri()
|
||||||
@@ -48,6 +49,7 @@ void RequestPaymentDialog::show(const std::string& address)
|
|||||||
s_label[0] = '\0';
|
s_label[0] = '\0';
|
||||||
s_payment_uri.clear();
|
s_payment_uri.clear();
|
||||||
s_uri_dirty = true;
|
s_uri_dirty = true;
|
||||||
|
s_creating_address = false;
|
||||||
|
|
||||||
// Only a shielded address can be paid (DragonX is a private chain): never prefill a mining address.
|
// Only a shielded address can be paid (DragonX is a private chain): never prefill a mining address.
|
||||||
if (!address.empty() && !util::isTransparentAddress(address)) {
|
if (!address.empty() && !util::isTransparentAddress(address)) {
|
||||||
@@ -67,6 +69,7 @@ void RequestPaymentDialog::show(const std::string& address)
|
|||||||
void RequestPaymentDialog::hide()
|
void RequestPaymentDialog::hide()
|
||||||
{
|
{
|
||||||
s_open = false;
|
s_open = false;
|
||||||
|
s_creating_address = false;
|
||||||
s_payment_uri.clear();
|
s_payment_uri.clear();
|
||||||
if (s_qr_texture != 0) {
|
if (s_qr_texture != 0) {
|
||||||
FreeQRTexture(s_qr_texture);
|
FreeQRTexture(s_qr_texture);
|
||||||
@@ -100,17 +103,55 @@ void RequestPaymentDialog::render(App* app)
|
|||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
|
||||||
// Address selection
|
// Address selection
|
||||||
|
const bool noShielded = state.z_addresses.empty();
|
||||||
|
if (noShielded) {
|
||||||
|
// Nothing to request to yet — offer to create a shielded address instead of dead-ending.
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(material::OnSurfaceMedium()));
|
||||||
|
ImGui::TextWrapped("%s", TR("request_no_shielded"));
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
ImGui::Spacing();
|
||||||
|
if (s_creating_address) {
|
||||||
|
ImGui::BeginDisabled();
|
||||||
|
material::TactileButton(TR("generating"), ImVec2(0, 0), S.resolveFont(actionBtn.font));
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
} else {
|
||||||
|
ImGui::BeginDisabled(!app->isConnected());
|
||||||
|
if (material::TactileButton(TR("request_create_shielded"), ImVec2(0, 0), S.resolveFont(actionBtn.font))) {
|
||||||
|
if (app->isConnected()) {
|
||||||
|
s_creating_address = true;
|
||||||
|
app->createNewZAddress([app](const std::string& addr) {
|
||||||
|
s_creating_address = false;
|
||||||
|
if (addr.empty()) {
|
||||||
|
Notifications::instance().error(app->lastAddressError().empty()
|
||||||
|
? std::string(TR("failed_create_shielded")) : app->lastAddressError());
|
||||||
|
} else {
|
||||||
|
Notifications::instance().success(TR("new_shielded_created"));
|
||||||
|
// Pre-select the new address if the dialog is still open and none chosen.
|
||||||
|
if (s_open && s_address[0] == '\0') {
|
||||||
|
strncpy(s_address, addr.c_str(), sizeof(s_address) - 1);
|
||||||
|
s_address[sizeof(s_address) - 1] = '\0';
|
||||||
|
s_uri_dirty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
ImGui::Text("%s", TR("request_receive_address"));
|
ImGui::Text("%s", TR("request_receive_address"));
|
||||||
|
|
||||||
std::string addr_display = s_address[0] ? s_address : TR("request_select_address");
|
std::string addr_display = s_address[0] ? s_address : std::string(TR("request_select_address"));
|
||||||
if (addr_display.length() > static_cast<size_t>(zAddrLbl.truncate)) {
|
if (addr_display.length() > static_cast<size_t>(zAddrLbl.truncate)) {
|
||||||
addr_display = addr_display.substr(0, zAddrFrontLbl.truncate) + "..." + addr_display.substr(addr_display.length() - zAddrBackLbl.truncate);
|
addr_display = addr_display.substr(0, zAddrFrontLbl.truncate) + "..." + addr_display.substr(addr_display.length() - zAddrBackLbl.truncate);
|
||||||
}
|
}
|
||||||
|
if (s_address[0]) {
|
||||||
|
std::string l = app->getAddressLabel(s_address);
|
||||||
|
if (!l.empty()) addr_display = l + " " + addr_display;
|
||||||
|
}
|
||||||
|
|
||||||
ImGui::SetNextItemWidth(-1);
|
ImGui::SetNextItemWidth(-1);
|
||||||
if (ImGui::BeginCombo("##Address", addr_display.c_str())) {
|
if (ImGui::BeginCombo("##Address", addr_display.c_str())) {
|
||||||
// Z-addresses
|
|
||||||
if (!state.z_addresses.empty()) {
|
|
||||||
ImGui::TextDisabled("%s", TR("request_shielded_addrs"));
|
ImGui::TextDisabled("%s", TR("request_shielded_addrs"));
|
||||||
for (size_t i = 0; i < state.z_addresses.size(); i++) {
|
for (size_t i = 0; i < state.z_addresses.size(); i++) {
|
||||||
const auto& addr = state.z_addresses[i];
|
const auto& addr = state.z_addresses[i];
|
||||||
@@ -118,18 +159,19 @@ void RequestPaymentDialog::render(App* app)
|
|||||||
if (label.length() > static_cast<size_t>(zAddrLbl.truncate)) {
|
if (label.length() > static_cast<size_t>(zAddrLbl.truncate)) {
|
||||||
label = label.substr(0, zAddrFrontLbl.truncate) + "..." + label.substr(label.length() - zAddrBackLbl.truncate);
|
label = label.substr(0, zAddrFrontLbl.truncate) + "..." + label.substr(label.length() - zAddrBackLbl.truncate);
|
||||||
}
|
}
|
||||||
|
std::string clbl = app->getAddressLabel(addr.address);
|
||||||
|
std::string shown = (clbl.empty() ? label : (clbl + " " + label)) + "##" + addr.address;
|
||||||
|
|
||||||
if (ImGui::Selectable(label.c_str(), addr.address == s_address)) {
|
if (ImGui::Selectable(shown.c_str(), addr.address == s_address)) {
|
||||||
strncpy(s_address, addr.address.c_str(), sizeof(s_address) - 1);
|
strncpy(s_address, addr.address.c_str(), sizeof(s_address) - 1);
|
||||||
s_uri_dirty = true;
|
s_uri_dirty = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// No mining (transparent) addresses: they only receive mining rewards, so a request to one
|
// No mining (transparent) addresses: they only receive mining rewards, so a request to one
|
||||||
// could never be paid.
|
// could never be paid.
|
||||||
ImGui::EndCombo();
|
ImGui::EndCombo();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
#include <sodium.h>
|
||||||
|
|
||||||
#include "imgui.h"
|
#include "imgui.h"
|
||||||
#include "../layout.h" // Layout::dpiScale()
|
#include "../layout.h" // Layout::dpiScale()
|
||||||
@@ -15,19 +16,31 @@
|
|||||||
namespace dragonx {
|
namespace dragonx {
|
||||||
namespace ui {
|
namespace ui {
|
||||||
|
|
||||||
|
// Best-effort zero of a seed-word vector (each word holds part of the phrase). Call after rendering.
|
||||||
|
inline void WipeSeedWords(std::vector<std::string>& words)
|
||||||
|
{
|
||||||
|
for (auto& w : words) { if (!w.empty()) sodium_memzero(&w[0], w.size()); w.clear(); }
|
||||||
|
words.clear();
|
||||||
|
}
|
||||||
|
|
||||||
// Split a whitespace-separated seed phrase into its individual words.
|
// Split a whitespace-separated seed phrase into its individual words.
|
||||||
inline std::vector<std::string> SplitSeedWords(const std::string& phrase)
|
inline std::vector<std::string> SplitSeedWords(const std::string& phrase)
|
||||||
{
|
{
|
||||||
std::vector<std::string> words;
|
std::vector<std::string> words;
|
||||||
|
words.reserve(32); // avoid reallocation stranding un-wiped moved-from copies in freed heap
|
||||||
std::string cur;
|
std::string cur;
|
||||||
for (char c : phrase) {
|
for (char c : phrase) {
|
||||||
if (c == ' ' || c == '\t' || c == '\n' || c == '\r') {
|
if (c == ' ' || c == '\t' || c == '\n' || c == '\r') {
|
||||||
if (!cur.empty()) { words.push_back(cur); cur.clear(); }
|
if (!cur.empty()) {
|
||||||
|
words.push_back(cur);
|
||||||
|
sodium_memzero(&cur[0], cur.size()); // zero the scratch copy before reuse
|
||||||
|
cur.clear();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
cur.push_back(c);
|
cur.push_back(c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!cur.empty()) words.push_back(cur);
|
if (!cur.empty()) { words.push_back(cur); sodium_memzero(&cur[0], cur.size()); cur.clear(); }
|
||||||
return words;
|
return words;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,6 +58,7 @@ inline void RenderSeedWordGrid(const std::vector<std::string>& words, float colS
|
|||||||
std::snprintf(cell, sizeof(cell), "%2zu. %s", i + 1, words[i].c_str());
|
std::snprintf(cell, sizeof(cell), "%2zu. %s", i + 1, words[i].c_str());
|
||||||
const float need = ImGui::CalcTextSize(cell).x + 20.0f * dp; // cell width + inter-column gap
|
const float need = ImGui::CalcTextSize(cell).x + 20.0f * dp; // cell width + inter-column gap
|
||||||
if (need > colStride) colStride = need;
|
if (need > colStride) colStride = need;
|
||||||
|
sodium_memzero(cell, sizeof(cell)); // the cell held a seed word
|
||||||
}
|
}
|
||||||
for (size_t i = 0; i < words.size(); ++i) {
|
for (size_t i = 0; i < words.size(); ++i) {
|
||||||
char cell[96];
|
char cell[96];
|
||||||
@@ -52,6 +66,7 @@ inline void RenderSeedWordGrid(const std::vector<std::string>& words, float colS
|
|||||||
ImGui::TextUnformatted(cell);
|
ImGui::TextUnformatted(cell);
|
||||||
if ((i % 4) != 3 && i + 1 < words.size())
|
if ((i % 4) != 3 && i + 1 < words.size())
|
||||||
ImGui::SameLine(((i % 4) + 1) * colStride);
|
ImGui::SameLine(((i % 4) + 1) * colStride);
|
||||||
|
sodium_memzero(cell, sizeof(cell));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+87
-46
@@ -14,6 +14,8 @@
|
|||||||
#include "../../util/address_validation.h"
|
#include "../../util/address_validation.h"
|
||||||
#include "../../util/i18n.h"
|
#include "../../util/i18n.h"
|
||||||
#include "../../util/text_format.h"
|
#include "../../util/text_format.h"
|
||||||
|
#include "../../util/amount_format.h" // toSats / canAfford — integer-satoshi affordability check
|
||||||
|
#include "../../rpc/rpc_failure.h" // isSendMaybeInFlight — lost-reply (outcome unknown) send tag
|
||||||
#include "../notifications.h"
|
#include "../notifications.h"
|
||||||
#include "../layout.h"
|
#include "../layout.h"
|
||||||
#include "contact_picker.h"
|
#include "contact_picker.h"
|
||||||
@@ -77,7 +79,6 @@ static constexpr double FEE_LOW = DRAGONX_DEFAULT_FEE * 0.5;
|
|||||||
static constexpr double FEE_NORMAL = DRAGONX_DEFAULT_FEE;
|
static constexpr double FEE_NORMAL = DRAGONX_DEFAULT_FEE;
|
||||||
static constexpr double FEE_HIGH = DRAGONX_DEFAULT_FEE * 2.0;
|
static constexpr double FEE_HIGH = DRAGONX_DEFAULT_FEE * 2.0;
|
||||||
|
|
||||||
static bool s_clear_confirm_pending = false;
|
|
||||||
|
|
||||||
// Source dropdown preview string
|
// Source dropdown preview string
|
||||||
static std::string s_source_preview;
|
static std::string s_source_preview;
|
||||||
@@ -268,8 +269,12 @@ static void RenderSourceDropdown(App* app, float width) {
|
|||||||
const char* tag = isZ ? "[Z]" : "[T]";
|
const char* tag = isZ ? "[Z]" : "[T]";
|
||||||
std::string trunc = util::truncateMiddle(addr.address,
|
std::string trunc = util::truncateMiddle(addr.address,
|
||||||
static_cast<int>(std::max(S.drawElement("tabs.send", "addr-preview-trunc-min").size, width / S.drawElement("tabs.send", "addr-preview-trunc-divisor").size)));
|
static_cast<int>(std::max(S.drawElement("tabs.send", "addr-preview-trunc-min").size, width / S.drawElement("tabs.send", "addr-preview-trunc-divisor").size)));
|
||||||
snprintf(buf, sizeof(buf), "%s %s — %s",
|
std::string lbl = app->getAddressLabel(addr.address);
|
||||||
tag, trunc.c_str(),
|
if (!lbl.empty())
|
||||||
|
snprintf(buf, sizeof(buf), "%s %s (%s) — %s", tag, lbl.c_str(), trunc.c_str(),
|
||||||
|
FormatSourceBalance(addr.balance, addr.spendableBalance).c_str());
|
||||||
|
else
|
||||||
|
snprintf(buf, sizeof(buf), "%s %s — %s", tag, trunc.c_str(),
|
||||||
FormatSourceBalance(addr.balance, addr.spendableBalance).c_str());
|
FormatSourceBalance(addr.balance, addr.spendableBalance).c_str());
|
||||||
s_source_preview = buf;
|
s_source_preview = buf;
|
||||||
} else if (s_from_address[0] != '\0') {
|
} else if (s_from_address[0] != '\0') {
|
||||||
@@ -313,8 +318,12 @@ static void RenderSourceDropdown(App* app, float width) {
|
|||||||
const char* tag = isZ ? "[Z]" : "[T]";
|
const char* tag = isZ ? "[Z]" : "[T]";
|
||||||
|
|
||||||
std::string trunc = util::truncateMiddle(addr.address, (int)addrTruncLen);
|
std::string trunc = util::truncateMiddle(addr.address, (int)addrTruncLen);
|
||||||
snprintf(buf, sizeof(buf), "%s %s — %s",
|
std::string lbl = app->getAddressLabel(addr.address);
|
||||||
tag, trunc.c_str(),
|
if (!lbl.empty())
|
||||||
|
snprintf(buf, sizeof(buf), "%s %s (%s) — %s", tag, lbl.c_str(), trunc.c_str(),
|
||||||
|
FormatSourceBalance(addr.balance, addr.spendableBalance).c_str());
|
||||||
|
else
|
||||||
|
snprintf(buf, sizeof(buf), "%s %s — %s", tag, trunc.c_str(),
|
||||||
FormatSourceBalance(addr.balance, addr.spendableBalance).c_str());
|
FormatSourceBalance(addr.balance, addr.spendableBalance).c_str());
|
||||||
|
|
||||||
ImGui::PushID(static_cast<int>(i));
|
ImGui::PushID(static_cast<int>(i));
|
||||||
@@ -324,8 +333,11 @@ static void RenderSourceDropdown(App* app, float width) {
|
|||||||
addr.address.c_str());
|
addr.address.c_str());
|
||||||
}
|
}
|
||||||
if (ImGui::IsItemHovered()) {
|
if (ImGui::IsItemHovered()) {
|
||||||
material::Tooltip("%s\n%s",
|
if (!lbl.empty())
|
||||||
addr.address.c_str(),
|
material::Tooltip("%s\n%s\n%s", lbl.c_str(), addr.address.c_str(),
|
||||||
|
FormatSourceBalance(addr.balance, addr.spendableBalance).c_str());
|
||||||
|
else
|
||||||
|
material::Tooltip("%s\n%s", addr.address.c_str(),
|
||||||
FormatSourceBalance(addr.balance, addr.spendableBalance).c_str());
|
FormatSourceBalance(addr.balance, addr.spendableBalance).c_str());
|
||||||
}
|
}
|
||||||
ImGui::PopID();
|
ImGui::PopID();
|
||||||
@@ -335,6 +347,20 @@ static void RenderSourceDropdown(App* app, float width) {
|
|||||||
ImGui::EndCombo();
|
ImGui::EndCombo();
|
||||||
}
|
}
|
||||||
ImGui::PopFont();
|
ImGui::PopFont();
|
||||||
|
|
||||||
|
// Warn when the chosen source is a transparent address: spending from it is not private on this
|
||||||
|
// ac_private chain (sender, amount and recipient become visible on-chain).
|
||||||
|
if (app->isConnected() && s_from_address[0] != '\0') {
|
||||||
|
bool srcT = (s_selected_from_idx >= 0 && s_selected_from_idx < (int)state.addresses.size())
|
||||||
|
? (state.addresses[s_selected_from_idx].type != "shielded")
|
||||||
|
: IsValidTransparentAddr(s_from_address);
|
||||||
|
if (srcT) {
|
||||||
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Warning()));
|
||||||
|
Type().textWrapped(TypeStyle::Caption, TR("send_transparent_source_warn"));
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -891,8 +917,9 @@ void RenderSendConfirmPopup(App* app) {
|
|||||||
// since Review. Don't broadcast a now-invalid transaction.
|
// since Review. Don't broadcast a now-invalid transaction.
|
||||||
const auto& liveState = app->getWalletState();
|
const auto& liveState = app->getWalletState();
|
||||||
double liveAvail = GetAvailableBalance(app);
|
double liveAvail = GetAvailableBalance(app);
|
||||||
if (!app->isConnected() || liveState.sync.syncing || s_amount <= 0 ||
|
if (!app->isConnected() || liveState.sync.syncing ||
|
||||||
(s_amount + s_fee) > liveAvail) {
|
!liveState.sync.sendTipReady(app->isLiteBuild()) || s_amount <= 0 ||
|
||||||
|
!util::canAfford(s_amount, s_fee, liveAvail)) {
|
||||||
s_tx_status = TR("send_cannot_send_now");
|
s_tx_status = TR("send_cannot_send_now");
|
||||||
s_status_success = false;
|
s_status_success = false;
|
||||||
s_status_timestamp = ImGui::GetTime();
|
s_status_timestamp = ImGui::GetTime();
|
||||||
@@ -930,6 +957,18 @@ void RenderSendConfirmPopup(App* app) {
|
|||||||
s_amount = 0.0;
|
s_amount = 0.0;
|
||||||
s_memo[0] = '\0';
|
s_memo[0] = '\0';
|
||||||
s_send_max = false;
|
s_send_max = false;
|
||||||
|
} else if (rpc::isSendMaybeInFlight(result)) {
|
||||||
|
// Outcome unknown: the reply was lost after the request left. Do NOT present a
|
||||||
|
// plain failure (it invites a retry/double-send). Warn, point at History, and
|
||||||
|
// clear the form so a resend has to be retyped deliberately.
|
||||||
|
s_tx_status = std::string(TR("send_maybe_sent")) + "\n\n" + rpc::stripSendMaybeInFlight(result);
|
||||||
|
s_result_txid.clear();
|
||||||
|
s_status_success = false;
|
||||||
|
Notifications::instance().warning(TR("send_maybe_sent"), 15.0f);
|
||||||
|
s_to_address[0] = '\0';
|
||||||
|
s_amount = 0.0;
|
||||||
|
s_memo[0] = '\0';
|
||||||
|
s_send_max = false;
|
||||||
} else {
|
} else {
|
||||||
// Show a friendly cause in the toast; keep the raw backend text below it in
|
// Show a friendly cause in the toast; keep the raw backend text below it in
|
||||||
// the overlay (copyable) so the detail isn't lost for diagnostics.
|
// the overlay (copyable) so the detail isn't lost for diagnostics.
|
||||||
@@ -1010,10 +1049,11 @@ static void RenderActionButtons(App* app, float width, float vScale,
|
|||||||
}
|
}
|
||||||
bool can_send = app->isConnected() &&
|
bool can_send = app->isConnected() &&
|
||||||
!state.sync.syncing &&
|
!state.sync.syncing &&
|
||||||
|
state.sync.sendTipReady(app->isLiteBuild()) &&
|
||||||
is_valid_address &&
|
is_valid_address &&
|
||||||
s_amount > 0 &&
|
s_amount > 0 &&
|
||||||
s_from_address[0] != '\0' &&
|
s_from_address[0] != '\0' &&
|
||||||
total <= available &&
|
util::canAfford(s_amount, s_fee, available) &&
|
||||||
sourceSpendable &&
|
sourceSpendable &&
|
||||||
!s_sending;
|
!s_sending;
|
||||||
|
|
||||||
@@ -1033,6 +1073,12 @@ static void RenderActionButtons(App* app, float width, float vScale,
|
|||||||
float sendBtnW = width - cancelBtnW - btnGap;
|
float sendBtnW = width - cancelBtnW - btnGap;
|
||||||
float btnH = std::max(schema::UI().drawElement("tabs.send", "action-btn-min-height").size, schema::UI().drawElement("tabs.send", "action-btn-height").size * vScale);
|
float btnH = std::max(schema::UI().drawElement("tabs.send", "action-btn-min-height").size, schema::UI().drawElement("tabs.send", "action-btn-height").size * vScale);
|
||||||
|
|
||||||
|
// Explain why Review/Send is greyed while syncing (the fields above are usable now).
|
||||||
|
if (state.sync.syncing || (app->isConnected() && !state.sync.sendTipReady(app->isLiteBuild()))) {
|
||||||
|
Type().textColored(TypeStyle::Caption, Warning(), TR("send_available_when_synced"));
|
||||||
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Send button ----
|
// ---- Send button ----
|
||||||
if (!can_send) {
|
if (!can_send) {
|
||||||
// The disabled fill must contrast the surface: a white overlay reads on dark skins but vanished
|
// The disabled fill must contrast the surface: a white overlay reads on dark skins but vanished
|
||||||
@@ -1057,6 +1103,8 @@ static void RenderActionButtons(App* app, float width, float vScale,
|
|||||||
material::Tooltip("%s", TR(app->isLiteBuild() ? "lite_no_wallet_short" : "send_tooltip_not_connected"));
|
material::Tooltip("%s", TR(app->isLiteBuild() ? "lite_no_wallet_short" : "send_tooltip_not_connected"));
|
||||||
else if (state.sync.syncing)
|
else if (state.sync.syncing)
|
||||||
material::Tooltip("%s", TR("send_tooltip_syncing"));
|
material::Tooltip("%s", TR("send_tooltip_syncing"));
|
||||||
|
else if (!state.sync.sendTipReady(app->isLiteBuild()))
|
||||||
|
material::Tooltip("%s", TR("send_tooltip_no_tip"));
|
||||||
else if (s_from_address[0] == '\0')
|
else if (s_from_address[0] == '\0')
|
||||||
material::Tooltip("%s", TR("send_tooltip_select_source"));
|
material::Tooltip("%s", TR("send_tooltip_select_source"));
|
||||||
else if (is_mining_addr)
|
else if (is_mining_addr)
|
||||||
@@ -1065,7 +1113,7 @@ static void RenderActionButtons(App* app, float width, float vScale,
|
|||||||
material::Tooltip("%s", TR("send_tooltip_invalid_address"));
|
material::Tooltip("%s", TR("send_tooltip_invalid_address"));
|
||||||
else if (s_amount <= 0)
|
else if (s_amount <= 0)
|
||||||
material::Tooltip("%s", TR("send_tooltip_enter_amount"));
|
material::Tooltip("%s", TR("send_tooltip_enter_amount"));
|
||||||
else if (total > available)
|
else if (!util::canAfford(s_amount, s_fee, available))
|
||||||
material::Tooltip("%s", TR("send_tooltip_exceeds_balance"));
|
material::Tooltip("%s", TR("send_tooltip_exceeds_balance"));
|
||||||
else if (!sourceSpendable)
|
else if (!sourceSpendable)
|
||||||
material::Tooltip("%s", TR("send_tooltip_view_only"));
|
material::Tooltip("%s", TR("send_tooltip_view_only"));
|
||||||
@@ -1081,38 +1129,20 @@ static void RenderActionButtons(App* app, float width, float vScale,
|
|||||||
ImGui::PushStyleColor(ImGuiCol_Border, ImGui::ColorConvertU32ToFloat4(OnSurfaceDisabled()));
|
ImGui::PushStyleColor(ImGuiCol_Border, ImGui::ColorConvertU32ToFloat4(OnSurfaceDisabled()));
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, S.drawElement("tabs.send", "cancel-btn-border-size").size);
|
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, S.drawElement("tabs.send", "cancel-btn-border-size").size);
|
||||||
char clearId[64];
|
char clearId[64];
|
||||||
snprintf(clearId, sizeof(clearId), "%s##Cancel%s", TR("cancel"), suffix);
|
// This button never cancels an in-progress send (there is none here) — it clears the form. Relabel it
|
||||||
|
// honestly, disable it when the form is empty, and drop the redundant confirm (ClearFormWithUndo
|
||||||
|
// already offers a timed Undo). The "##Cancel" id suffix is kept so the widget id stays stable.
|
||||||
|
snprintf(clearId, sizeof(clearId), "%s##Cancel%s", TR("send_clear_form"), suffix);
|
||||||
|
ImGui::BeginDisabled(!FormHasData());
|
||||||
if (TactileButton(clearId, ImVec2(cancelBtnW, btnH), S.resolveFont(S.button("tabs.send", "clear-button").font))) {
|
if (TactileButton(clearId, ImVec2(cancelBtnW, btnH), S.resolveFont(S.button("tabs.send", "clear-button").font))) {
|
||||||
if (FormHasData()) {
|
|
||||||
s_clear_confirm_pending = true;
|
|
||||||
} else {
|
|
||||||
ClearFormWithUndo();
|
ClearFormWithUndo();
|
||||||
}
|
}
|
||||||
}
|
ImGui::EndDisabled();
|
||||||
|
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
|
||||||
|
material::Tooltip("%s", TR("send_clear_form_tip"));
|
||||||
ImGui::PopStyleVar(); // FrameBorderSize
|
ImGui::PopStyleVar(); // FrameBorderSize
|
||||||
ImGui::PopStyleColor(3);
|
ImGui::PopStyleColor(3);
|
||||||
|
|
||||||
// Clear confirmation popup
|
|
||||||
char confirmClearId[32];
|
|
||||||
snprintf(confirmClearId, sizeof(confirmClearId), "##ConfirmClear%s", suffix);
|
|
||||||
if (s_clear_confirm_pending) {
|
|
||||||
ImGui::OpenPopup(confirmClearId);
|
|
||||||
s_clear_confirm_pending = false;
|
|
||||||
}
|
|
||||||
if (ImGui::BeginPopup(confirmClearId)) {
|
|
||||||
ImGui::Text("%s", TR("send_clear_fields"));
|
|
||||||
ImGui::Spacing();
|
|
||||||
if (TactileButton(TR("send_yes_clear"), ImVec2(schema::UI().drawElement("tabs.send", "clear-confirm-yes-width").size * dp, 0), S.resolveFont("button"))) {
|
|
||||||
ClearFormWithUndo();
|
|
||||||
ImGui::CloseCurrentPopup();
|
|
||||||
}
|
|
||||||
ImGui::SameLine();
|
|
||||||
if (TactileButton(TR("send_keep"), ImVec2(schema::UI().drawElement("tabs.send", "clear-confirm-keep-width").size * dp, 0), S.resolveFont("button"))) {
|
|
||||||
ImGui::CloseCurrentPopup();
|
|
||||||
}
|
|
||||||
ImGui::EndPopup();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Undo button (shown briefly after clear)
|
// Undo button (shown briefly after clear)
|
||||||
if (s_undo_timestamp > 0.0) {
|
if (s_undo_timestamp > 0.0) {
|
||||||
double undoElapsed = ImGui::GetTime() - s_undo_timestamp;
|
double undoElapsed = ImGui::GetTime() - s_undo_timestamp;
|
||||||
@@ -1270,9 +1300,9 @@ void RenderSendTab(App* app)
|
|||||||
|
|
||||||
RenderSyncBanner(state);
|
RenderSyncBanner(state);
|
||||||
|
|
||||||
bool sendSyncing = state.sync.syncing && !state.sync.isSynced();
|
// The form stays EDITABLE while syncing so the user can pre-fill it; only the Review/Send button is
|
||||||
ImGui::BeginDisabled(sendSyncing);
|
// gated (can_send includes !syncing, and the confirm step re-checks live state), so nothing can be
|
||||||
|
// sent until the wallet is synced.
|
||||||
ImVec2 sendAvail = ImGui::GetContentRegionAvail();
|
ImVec2 sendAvail = ImGui::GetContentRegionAvail();
|
||||||
float hs = Layout::hScale(sendAvail.x);
|
float hs = Layout::hScale(sendAvail.x);
|
||||||
float vScale = Layout::vScale(sendAvail.y);
|
float vScale = Layout::vScale(sendAvail.y);
|
||||||
@@ -1329,7 +1359,6 @@ void RenderSendTab(App* app)
|
|||||||
// Zero balance CTA
|
// Zero balance CTA
|
||||||
if (RenderZeroBalanceCTA(app, dl, formW)) {
|
if (RenderZeroBalanceCTA(app, dl, formW)) {
|
||||||
ImGui::EndGroup();
|
ImGui::EndGroup();
|
||||||
ImGui::EndDisabled(); // sendSyncing guard
|
|
||||||
ImGui::EndChild();
|
ImGui::EndChild();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1404,7 +1433,9 @@ void RenderSendTab(App* app)
|
|||||||
Type().textColored(TypeStyle::Caption, Error(), TR("send_mining_addr_short"));
|
Type().textColored(TypeStyle::Caption, Error(), TR("send_mining_addr_short"));
|
||||||
if (vt && ImGui::IsItemHovered())
|
if (vt && ImGui::IsItemHovered())
|
||||||
material::Tooltip("%s", TR("send_mining_addr_blocked"));
|
material::Tooltip("%s", TR("send_mining_addr_blocked"));
|
||||||
} else if (!checkPreview) {
|
} else if (!checkPreview && ImGui::GetActiveID() != ImGui::GetID("##ToAddr")) {
|
||||||
|
// Defer the "invalid" error until the field loses focus, so it doesn't flash red
|
||||||
|
// from the first keystroke while the user is still typing the address.
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
Type().textColored(TypeStyle::Caption, Error(), TR("invalid_address"));
|
Type().textColored(TypeStyle::Caption, Error(), TR("invalid_address"));
|
||||||
}
|
}
|
||||||
@@ -1517,6 +1548,7 @@ void RenderSendTab(App* app)
|
|||||||
if (ImGui::InputDouble("##AmountUSD", &s_usd_amount, 0, 0, "$%.2f")) {
|
if (ImGui::InputDouble("##AmountUSD", &s_usd_amount, 0, 0, "$%.2f")) {
|
||||||
// Normalize to 8dp (satoshi precision) so the reviewed/sent DRGX matches the preview.
|
// Normalize to 8dp (satoshi precision) so the reviewed/sent DRGX matches the preview.
|
||||||
s_amount = std::round((s_usd_amount / market.price_usd) * 1e8) / 1e8;
|
s_amount = std::round((s_usd_amount / market.price_usd) * 1e8) / 1e8;
|
||||||
|
s_send_max = false; // a manual edit is no longer "max" — don't let a later fee bump overwrite it
|
||||||
}
|
}
|
||||||
// Draw DRGX equivalent inside the input field (right-aligned overlay)
|
// Draw DRGX equivalent inside the input field (right-aligned overlay)
|
||||||
{
|
{
|
||||||
@@ -1542,6 +1574,7 @@ void RenderSendTab(App* app)
|
|||||||
if (s_amount < 0) s_amount = 0;
|
if (s_amount < 0) s_amount = 0;
|
||||||
if (market.price_usd > 0)
|
if (market.price_usd > 0)
|
||||||
s_usd_amount = s_amount * market.price_usd;
|
s_usd_amount = s_amount * market.price_usd;
|
||||||
|
s_send_max = false; // a manual edit is no longer "max" — don't let a later fee bump overwrite it
|
||||||
}
|
}
|
||||||
// Draw USD equivalent inside the input field (right-aligned overlay)
|
// Draw USD equivalent inside the input field (right-aligned overlay)
|
||||||
if (market.price_usd > 0 && s_amount > 0) {
|
if (market.price_usd > 0 && s_amount > 0) {
|
||||||
@@ -1606,14 +1639,17 @@ void RenderSendTab(App* app)
|
|||||||
RenderAmountBar(dl, available, colW, capFont, market.price_usd, "##f");
|
RenderAmountBar(dl, available, colW, capFont, market.price_usd, "##f");
|
||||||
|
|
||||||
// Amount error
|
// Amount error
|
||||||
if (s_amount > 0 && s_amount + s_fee > available && available > 0) {
|
if (s_amount > 0 && !util::canAfford(s_amount, s_fee, available) && available > 0) {
|
||||||
snprintf(buf, sizeof(buf), TR("send_exceeds_available"), available - s_fee);
|
snprintf(buf, sizeof(buf), TR("send_exceeds_available"), available - s_fee);
|
||||||
Type().textColored(TypeStyle::Caption, Error(), buf);
|
Type().textColored(TypeStyle::Caption, Error(), buf);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- MEMO (shielded only) ----
|
// ---- MEMO (shielded only; shown disabled + explained for non-shielded recipients) ----
|
||||||
if (is_valid_z || s_to_address[0] == '\0') {
|
{
|
||||||
|
// Keep the memo visible (and the typed text intact) even for a transparent recipient, with an
|
||||||
|
// explainer, instead of silently hiding the field and dropping what the user wrote.
|
||||||
|
const bool memoDisabled = (s_to_address[0] != '\0' && !is_valid_z);
|
||||||
ImGui::Dummy(ImVec2(0, innerGap * 0.5f));
|
ImGui::Dummy(ImVec2(0, innerGap * 0.5f));
|
||||||
{
|
{
|
||||||
ImVec2 divPos = ImGui::GetCursorScreenPos();
|
ImVec2 divPos = ImGui::GetCursorScreenPos();
|
||||||
@@ -1628,8 +1664,10 @@ void RenderSendTab(App* app)
|
|||||||
|
|
||||||
float memoInputH = std::max(S.drawElement("tabs.send", "memo-min-height").size, S.drawElement("tabs.send", "memo-base-height").size * vScale);
|
float memoInputH = std::max(S.drawElement("tabs.send", "memo-min-height").size, S.drawElement("tabs.send", "memo-base-height").size * vScale);
|
||||||
ImGui::PushItemWidth(colW);
|
ImGui::PushItemWidth(colW);
|
||||||
|
if (memoDisabled) ImGui::BeginDisabled();
|
||||||
ImGui::InputTextMultiline("##Memo", s_memo, sizeof(s_memo),
|
ImGui::InputTextMultiline("##Memo", s_memo, sizeof(s_memo),
|
||||||
ImVec2(colW, memoInputH));
|
ImVec2(colW, memoInputH));
|
||||||
|
if (memoDisabled) ImGui::EndDisabled();
|
||||||
ImGui::PopItemWidth();
|
ImGui::PopItemWidth();
|
||||||
|
|
||||||
// The Sapling memo limit is in BYTES (multi-byte UTF-8 fills it faster than visible chars);
|
// The Sapling memo limit is in BYTES (multi-byte UTF-8 fills it faster than visible chars);
|
||||||
@@ -1639,6 +1677,10 @@ void RenderSendTab(App* app)
|
|||||||
bool memoAtCap = memo_len + 1 >= memoMax;
|
bool memoAtCap = memo_len + 1 >= memoMax;
|
||||||
snprintf(buf, sizeof(buf), TR("byte_count_fmt"), memo_len, memoMax);
|
snprintf(buf, sizeof(buf), TR("byte_count_fmt"), memo_len, memoMax);
|
||||||
Type().textColored(TypeStyle::Caption, memoAtCap ? Warning() : OnSurfaceDisabled(), buf);
|
Type().textColored(TypeStyle::Caption, memoAtCap ? Warning() : OnSurfaceDisabled(), buf);
|
||||||
|
if (memoDisabled) {
|
||||||
|
const char* mk = s_memo[0] ? "send_memo_will_not_be_sent" : "send_memo_requires_shielded";
|
||||||
|
Type().textColored(TypeStyle::Caption, Warning(), TR(mk));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Divider before action buttons
|
// Divider before action buttons
|
||||||
@@ -1706,7 +1748,6 @@ void RenderSendTab(App* app)
|
|||||||
RenderRecentSends(state, formW, capFont, app);
|
RenderRecentSends(state, formW, capFont, app);
|
||||||
|
|
||||||
ImGui::EndGroup();
|
ImGui::EndGroup();
|
||||||
ImGui::EndDisabled(); // sendSyncing guard
|
|
||||||
// Round to nearest pixel to prevent sub-pixel oscillation (vibration)
|
// Round to nearest pixel to prevent sub-pixel oscillation (vibration)
|
||||||
// when content like "recent sends" elapsed-time text changes width each frame
|
// when content like "recent sends" elapsed-time text changes width each frame
|
||||||
float measuredH = ImGui::GetCursorPosY() - contentStartY;
|
float measuredH = ImGui::GetCursorPosY() - contentStartY;
|
||||||
|
|||||||
@@ -1,608 +0,0 @@
|
|||||||
// DragonX Wallet - ImGui Edition
|
|
||||||
// Copyright 2024-2026 The Hush Developers
|
|
||||||
// Released under the GPLv3
|
|
||||||
|
|
||||||
#include "settings_window.h"
|
|
||||||
#include "../../app.h"
|
|
||||||
#include "../../config/version.h"
|
|
||||||
#include "../../config/settings.h"
|
|
||||||
#include "../../util/i18n.h"
|
|
||||||
#include "../../util/platform.h"
|
|
||||||
#include "../../rpc/rpc_client.h"
|
|
||||||
#include "../theme.h"
|
|
||||||
#include "../schema/ui_schema.h"
|
|
||||||
#include "../schema/skin_manager.h"
|
|
||||||
#include "../notifications.h"
|
|
||||||
#include "../effects/imgui_acrylic.h"
|
|
||||||
#include "../material/draw_helpers.h"
|
|
||||||
#include "../../embedded/IconsMaterialDesign.h"
|
|
||||||
#include "imgui.h"
|
|
||||||
#include <vector>
|
|
||||||
#include <filesystem>
|
|
||||||
|
|
||||||
// Icon text for settings UI
|
|
||||||
#define ICON_CUSTOM_THEME ICON_MD_TUNE
|
|
||||||
#define ICON_REFRESH_THEMES ICON_MD_REFRESH
|
|
||||||
|
|
||||||
namespace dragonx {
|
|
||||||
namespace ui {
|
|
||||||
|
|
||||||
// Helper: build "TranslatedLabel##id" for ImGui widgets that use label as ID
|
|
||||||
static std::string TrId(const char* tr_key, const char* id) {
|
|
||||||
std::string s = TR(tr_key);
|
|
||||||
s += "##";
|
|
||||||
s += id;
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Settings state - these get loaded from Settings on window open
|
|
||||||
static bool s_initialized = false;
|
|
||||||
static int s_language_index = 0;
|
|
||||||
static bool s_save_ztxs = true;
|
|
||||||
static bool s_allow_custom_fees = false;
|
|
||||||
static bool s_auto_shield = false;
|
|
||||||
static bool s_fetch_prices = true;
|
|
||||||
static bool s_use_tor = false;
|
|
||||||
static char s_rpc_host[128] = DRAGONX_DEFAULT_RPC_HOST;
|
|
||||||
static char s_rpc_port[16] = DRAGONX_DEFAULT_RPC_PORT;
|
|
||||||
static char s_rpc_user[64] = "";
|
|
||||||
static char s_rpc_password[64] = "";
|
|
||||||
static char s_tx_explorer[256] = "https://explorer.dragonx.is/tx/";
|
|
||||||
static char s_addr_explorer[256] = "https://explorer.dragonx.is/address/";
|
|
||||||
|
|
||||||
// Acrylic settings
|
|
||||||
static bool s_acrylic_enabled = true;
|
|
||||||
static float s_blur_amount = 1.5f; // 0.0=Off, continuous blur multiplier
|
|
||||||
static float s_noise_opacity = 0.5f;
|
|
||||||
static bool s_reduced_transparency = false; // Accessibility option
|
|
||||||
static bool s_gradient_background = false; // Gradient background mode
|
|
||||||
|
|
||||||
// Saved skin ID for cancel/revert
|
|
||||||
static std::string s_saved_skin_id;
|
|
||||||
|
|
||||||
// Load current settings into UI state
|
|
||||||
static void loadSettingsToUI(config::Settings* settings) {
|
|
||||||
if (!settings) return;
|
|
||||||
|
|
||||||
s_saved_skin_id = settings->getSkinId();
|
|
||||||
s_save_ztxs = settings->getSaveZtxs();
|
|
||||||
s_allow_custom_fees = settings->getAllowCustomFees();
|
|
||||||
s_auto_shield = settings->getAutoShield();
|
|
||||||
s_fetch_prices = settings->getFetchPrices();
|
|
||||||
s_use_tor = settings->getUseTor();
|
|
||||||
|
|
||||||
strncpy(s_tx_explorer, settings->getTxExplorerUrl().c_str(), sizeof(s_tx_explorer) - 1);
|
|
||||||
strncpy(s_addr_explorer, settings->getAddressExplorerUrl().c_str(), sizeof(s_addr_explorer) - 1);
|
|
||||||
|
|
||||||
// Set language index
|
|
||||||
auto& i18n = util::I18n::instance();
|
|
||||||
const auto& languages = i18n.getAvailableLanguages();
|
|
||||||
std::string current_lang = settings->getLanguage();
|
|
||||||
if (current_lang.empty()) current_lang = "en";
|
|
||||||
|
|
||||||
s_language_index = 0;
|
|
||||||
int idx = 0;
|
|
||||||
for (const auto& lang : languages) {
|
|
||||||
if (lang.first == current_lang) {
|
|
||||||
s_language_index = idx;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
idx++;
|
|
||||||
}
|
|
||||||
|
|
||||||
s_initialized = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save UI state to settings
|
|
||||||
static void saveSettingsFromUI(config::Settings* settings) {
|
|
||||||
if (!settings) return;
|
|
||||||
|
|
||||||
settings->setTheme(settings->getSkinId()); // Theme now synced with skin
|
|
||||||
settings->setSaveZtxs(s_save_ztxs);
|
|
||||||
settings->setAllowCustomFees(s_allow_custom_fees);
|
|
||||||
settings->setAutoShield(s_auto_shield);
|
|
||||||
settings->setFetchPrices(s_fetch_prices);
|
|
||||||
settings->setUseTor(s_use_tor);
|
|
||||||
settings->setTxExplorerUrl(s_tx_explorer);
|
|
||||||
settings->setAddressExplorerUrl(s_addr_explorer);
|
|
||||||
|
|
||||||
// Save language
|
|
||||||
auto& i18n = util::I18n::instance();
|
|
||||||
const auto& languages = i18n.getAvailableLanguages();
|
|
||||||
auto it = languages.begin();
|
|
||||||
std::advance(it, s_language_index);
|
|
||||||
if (it != languages.end()) {
|
|
||||||
settings->setLanguage(it->first);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save acrylic / visual effects settings
|
|
||||||
settings->setAcrylicEnabled(s_acrylic_enabled);
|
|
||||||
settings->setAcrylicQuality(s_blur_amount > 0.001f ? static_cast<int>(effects::AcrylicQuality::Low) : static_cast<int>(effects::AcrylicQuality::Off));
|
|
||||||
settings->setBlurMultiplier(s_blur_amount);
|
|
||||||
settings->setReducedTransparency(s_reduced_transparency);
|
|
||||||
settings->setNoiseOpacity(s_noise_opacity);
|
|
||||||
settings->setGradientBackground(s_gradient_background);
|
|
||||||
|
|
||||||
settings->save();
|
|
||||||
}
|
|
||||||
|
|
||||||
void RenderSettingsWindow(App* app, bool* p_open)
|
|
||||||
{
|
|
||||||
// Load settings on first open
|
|
||||||
if (!s_initialized && app->settings()) {
|
|
||||||
loadSettingsToUI(app->settings());
|
|
||||||
// Initialize acrylic settings from current state
|
|
||||||
s_acrylic_enabled = effects::ImGuiAcrylic::IsEnabled();
|
|
||||||
s_blur_amount = effects::ImGuiAcrylic::GetBlurMultiplier();
|
|
||||||
s_noise_opacity = effects::ImGuiAcrylic::GetNoiseOpacity();
|
|
||||||
s_reduced_transparency = effects::ImGuiAcrylic::GetReducedTransparency();
|
|
||||||
s_gradient_background = schema::SkinManager::instance().isGradientMode();
|
|
||||||
}
|
|
||||||
|
|
||||||
auto& S = schema::UI();
|
|
||||||
auto win = S.window("dialogs.settings");
|
|
||||||
auto lbl = S.label("dialogs.settings", "label");
|
|
||||||
auto cmb = S.combo("dialogs.settings", "combo");
|
|
||||||
auto connLbl = S.label("dialogs.settings", "connection-label");
|
|
||||||
auto portInput = S.input("dialogs.settings", "port-input");
|
|
||||||
auto walletBtn = S.button("dialogs.settings", "wallet-button");
|
|
||||||
auto saveBtn = S.button("dialogs.settings", "save-button");
|
|
||||||
auto cancelBtn = S.button("dialogs.settings", "cancel-button");
|
|
||||||
|
|
||||||
if (!material::BeginOverlayDialog(TR("settings"), p_open, win.width, 0.94f)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ImGui::BeginTabBar("SettingsTabs")) {
|
|
||||||
// General settings tab
|
|
||||||
if (ImGui::BeginTabItem(TR("general"))) {
|
|
||||||
ImGui::Spacing();
|
|
||||||
|
|
||||||
// Skin/theme selection
|
|
||||||
ImGui::Text("%s", TR("theme"));
|
|
||||||
ImGui::SameLine(lbl.position);
|
|
||||||
|
|
||||||
// Active skin combo (populated from SkinManager)
|
|
||||||
auto& skinMgr = schema::SkinManager::instance();
|
|
||||||
const auto& skins = skinMgr.available();
|
|
||||||
|
|
||||||
// Find active skin for preview text
|
|
||||||
std::string active_preview = "DragonX";
|
|
||||||
bool active_is_custom = false;
|
|
||||||
for (const auto& skin : skins) {
|
|
||||||
if (skin.id == skinMgr.activeSkinId()) {
|
|
||||||
active_preview = skin.name;
|
|
||||||
active_is_custom = !skin.bundled;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::SetNextItemWidth(cmb.width);
|
|
||||||
if (ImGui::BeginCombo("##Theme", active_preview.c_str())) {
|
|
||||||
// Bundled themes header
|
|
||||||
ImGui::TextDisabled("%s", TR("settings_builtin"));
|
|
||||||
ImGui::Separator();
|
|
||||||
for (size_t i = 0; i < skins.size(); i++) {
|
|
||||||
const auto& skin = skins[i];
|
|
||||||
if (!skin.bundled) continue;
|
|
||||||
bool is_selected = (skin.id == skinMgr.activeSkinId());
|
|
||||||
|
|
||||||
if (ImGui::Selectable(skin.name.c_str(), is_selected)) {
|
|
||||||
skinMgr.setActiveSkin(skin.id);
|
|
||||||
if (app->settings()) {
|
|
||||||
app->settings()->setSkinId(skin.id);
|
|
||||||
app->settings()->save();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (is_selected) {
|
|
||||||
ImGui::SetItemDefaultFocus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Custom themes (if any)
|
|
||||||
bool has_custom = false;
|
|
||||||
for (const auto& skin : skins) {
|
|
||||||
if (!skin.bundled) { has_custom = true; break; }
|
|
||||||
}
|
|
||||||
if (has_custom) {
|
|
||||||
ImGui::Spacing();
|
|
||||||
ImGui::TextDisabled("%s", TR("settings_custom"));
|
|
||||||
ImGui::Separator();
|
|
||||||
for (size_t i = 0; i < skins.size(); i++) {
|
|
||||||
const auto& skin = skins[i];
|
|
||||||
if (skin.bundled) continue;
|
|
||||||
bool is_selected = (skin.id == skinMgr.activeSkinId());
|
|
||||||
|
|
||||||
if (!skin.valid) {
|
|
||||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.3f, 0.3f, 1.0f));
|
|
||||||
ImGui::BeginDisabled(true);
|
|
||||||
std::string label = skin.name + TR("swin_invalid_suffix");
|
|
||||||
ImGui::Selectable(label.c_str(), false);
|
|
||||||
ImGui::EndDisabled();
|
|
||||||
ImGui::PopStyleColor();
|
|
||||||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
|
|
||||||
material::Tooltip("%s", skin.validationError.c_str());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
std::string label = skin.name;
|
|
||||||
if (!skin.author.empty()) {
|
|
||||||
label += " (" + skin.author + ")";
|
|
||||||
}
|
|
||||||
if (ImGui::Selectable(label.c_str(), is_selected)) {
|
|
||||||
skinMgr.setActiveSkin(skin.id);
|
|
||||||
if (app->settings()) {
|
|
||||||
app->settings()->setSkinId(skin.id);
|
|
||||||
app->settings()->save();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (is_selected) {
|
|
||||||
ImGui::SetItemDefaultFocus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ImGui::EndCombo();
|
|
||||||
}
|
|
||||||
if (ImGui::IsItemHovered())
|
|
||||||
material::Tooltip("%s", TR("tt_theme_hotkey"));
|
|
||||||
|
|
||||||
// Show indicator if custom theme is active
|
|
||||||
if (active_is_custom) {
|
|
||||||
ImGui::SameLine();
|
|
||||||
ImGui::PushFont(material::Type().iconSmall());
|
|
||||||
ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), ICON_CUSTOM_THEME);
|
|
||||||
ImGui::PopFont();
|
|
||||||
if (ImGui::IsItemHovered()) {
|
|
||||||
material::Tooltip("%s", TR("tt_custom_theme"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::SameLine();
|
|
||||||
ImGui::PushFont(material::Type().iconSmall());
|
|
||||||
if (material::StyledButton(ICON_REFRESH_THEMES, ImVec2(0, 0))) {
|
|
||||||
skinMgr.refresh();
|
|
||||||
Notifications::instance().info(TR("swin_theme_list_refreshed"));
|
|
||||||
}
|
|
||||||
ImGui::PopFont();
|
|
||||||
if (ImGui::IsItemHovered()) {
|
|
||||||
material::Tooltip(TR("tt_scan_themes"),
|
|
||||||
schema::SkinManager::getUserSkinsDirectory().c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::Spacing();
|
|
||||||
|
|
||||||
// Language selection
|
|
||||||
ImGui::Text("%s", TR("language"));
|
|
||||||
ImGui::SameLine(lbl.position);
|
|
||||||
auto& i18n = util::I18n::instance();
|
|
||||||
const auto& languages = i18n.getAvailableLanguages();
|
|
||||||
|
|
||||||
// Build language display names array
|
|
||||||
std::vector<const char*> lang_names;
|
|
||||||
lang_names.reserve(languages.size());
|
|
||||||
for (const auto& lang : languages) {
|
|
||||||
lang_names.push_back(lang.second.c_str()); // Display name
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::SetNextItemWidth(cmb.width);
|
|
||||||
if (ImGui::Combo("##Language", &s_language_index, lang_names.data(), static_cast<int>(lang_names.size()))) {
|
|
||||||
// Get locale code from index
|
|
||||||
auto it = languages.begin();
|
|
||||||
std::advance(it, s_language_index);
|
|
||||||
i18n.loadLanguage(it->first);
|
|
||||||
}
|
|
||||||
ImGui::TextDisabled(" %s", TR("settings_language_note"));
|
|
||||||
|
|
||||||
ImGui::Spacing();
|
|
||||||
ImGui::Separator();
|
|
||||||
ImGui::Spacing();
|
|
||||||
|
|
||||||
// Acrylic Effects settings
|
|
||||||
ImGui::Text("%s", TR("settings_visual_effects"));
|
|
||||||
ImGui::Spacing();
|
|
||||||
|
|
||||||
ImGui::Text("%s", TR("settings_acrylic_level"));
|
|
||||||
ImGui::SameLine(lbl.position);
|
|
||||||
ImGui::SetNextItemWidth(cmb.width);
|
|
||||||
{
|
|
||||||
char blur_fmt[16];
|
|
||||||
if (s_blur_amount < 0.01f)
|
|
||||||
snprintf(blur_fmt, sizeof(blur_fmt), "Off");
|
|
||||||
else
|
|
||||||
snprintf(blur_fmt, sizeof(blur_fmt), "%.0f%%%%", s_blur_amount * 25.0f);
|
|
||||||
if (ImGui::SliderFloat("##AcrylicBlur", &s_blur_amount, 0.0f, 4.0f, blur_fmt,
|
|
||||||
ImGuiSliderFlags_AlwaysClamp)) {
|
|
||||||
if (s_blur_amount > 0.0f && s_blur_amount < 0.15f) s_blur_amount = 0.0f;
|
|
||||||
s_acrylic_enabled = (s_blur_amount > 0.001f);
|
|
||||||
effects::ImGuiAcrylic::ApplyBlurAmount(s_blur_amount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ImGui::TextDisabled(" %s", TR("tt_blur"));
|
|
||||||
|
|
||||||
ImGui::Spacing();
|
|
||||||
ImGui::Text("%s", TR("settings_noise_opacity"));
|
|
||||||
ImGui::SameLine(lbl.position);
|
|
||||||
ImGui::SetNextItemWidth(cmb.width);
|
|
||||||
{
|
|
||||||
char noise_fmt[16];
|
|
||||||
if (s_noise_opacity < 0.01f)
|
|
||||||
snprintf(noise_fmt, sizeof(noise_fmt), "Off");
|
|
||||||
else
|
|
||||||
snprintf(noise_fmt, sizeof(noise_fmt), "%.0f%%%%", s_noise_opacity * 100.0f);
|
|
||||||
if (ImGui::SliderFloat("##NoiseOpacity", &s_noise_opacity, 0.0f, 1.0f, noise_fmt,
|
|
||||||
ImGuiSliderFlags_AlwaysClamp)) {
|
|
||||||
effects::ImGuiAcrylic::SetNoiseOpacity(s_noise_opacity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ImGui::TextDisabled(" %s", TR("tt_noise"));
|
|
||||||
|
|
||||||
ImGui::Spacing();
|
|
||||||
|
|
||||||
// Accessibility: Reduced transparency
|
|
||||||
if (ImGui::Checkbox(TrId("settings_reduce_transparency", "reduce_trans").c_str(), &s_reduced_transparency)) {
|
|
||||||
effects::ImGuiAcrylic::SetReducedTransparency(s_reduced_transparency);
|
|
||||||
}
|
|
||||||
ImGui::TextDisabled(" %s", TR("settings_solid_colors_desc"));
|
|
||||||
|
|
||||||
ImGui::Spacing();
|
|
||||||
|
|
||||||
if (ImGui::Checkbox(TrId("simple_background", "simple_bg").c_str(), &s_gradient_background)) {
|
|
||||||
schema::SkinManager::instance().setGradientMode(s_gradient_background);
|
|
||||||
}
|
|
||||||
ImGui::TextDisabled(" %s", TR("settings_gradient_desc"));
|
|
||||||
|
|
||||||
ImGui::Spacing();
|
|
||||||
ImGui::Separator();
|
|
||||||
ImGui::Spacing();
|
|
||||||
|
|
||||||
// Privacy settings
|
|
||||||
ImGui::Text("%s", TR("settings_privacy"));
|
|
||||||
ImGui::Spacing();
|
|
||||||
|
|
||||||
ImGui::Checkbox(TrId("settings_save_shielded_local", "save_ztx_w").c_str(), &s_save_ztxs);
|
|
||||||
ImGui::TextDisabled(" %s", TR("settings_save_shielded_desc"));
|
|
||||||
|
|
||||||
ImGui::Spacing();
|
|
||||||
|
|
||||||
ImGui::Checkbox(TrId("settings_auto_shield_funds", "auto_shld_w").c_str(), &s_auto_shield);
|
|
||||||
ImGui::TextDisabled(" %s", TR("settings_auto_shield_desc"));
|
|
||||||
|
|
||||||
ImGui::Spacing();
|
|
||||||
|
|
||||||
{
|
|
||||||
// Not implemented: disabled and never shown as "on", even for a stored use_tor=true.
|
|
||||||
bool torOff = false;
|
|
||||||
ImGui::BeginDisabled();
|
|
||||||
ImGui::Checkbox(TrId("settings_use_tor_network", "tor_w").c_str(), &torOff);
|
|
||||||
ImGui::EndDisabled();
|
|
||||||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
|
|
||||||
const float wrapW = S.drawElement("components.settings-page", "tor-tooltip-wrap-width")
|
|
||||||
.sizeOr(420.0f) * Layout::dpiScale();
|
|
||||||
material::BeginTooltip();
|
|
||||||
ImGui::PushTextWrapPos(wrapW);
|
|
||||||
ImGui::TextUnformatted(TR(app && app->isLiteBuild() ? "tt_tor_unavailable_lite"
|
|
||||||
: "tt_tor_unavailable"));
|
|
||||||
ImGui::PopTextWrapPos();
|
|
||||||
material::EndTooltip();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::Spacing();
|
|
||||||
ImGui::Separator();
|
|
||||||
ImGui::Spacing();
|
|
||||||
|
|
||||||
// Other settings
|
|
||||||
ImGui::Text("%s", TR("settings_other"));
|
|
||||||
ImGui::Spacing();
|
|
||||||
|
|
||||||
ImGui::Checkbox(TrId("custom_fees", "fees_w").c_str(), &s_allow_custom_fees);
|
|
||||||
ImGui::Checkbox(TrId("fetch_prices", "prices_w").c_str(), &s_fetch_prices);
|
|
||||||
|
|
||||||
ImGui::EndTabItem();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connection settings tab
|
|
||||||
if (ImGui::BeginTabItem(TR("settings_connection"))) {
|
|
||||||
ImGui::Spacing();
|
|
||||||
|
|
||||||
ImGui::Text("%s", TR("settings_rpc_connection"));
|
|
||||||
ImGui::TextDisabled("%s", TR("settings_configure_rpc"));
|
|
||||||
ImGui::Spacing();
|
|
||||||
ImGui::Separator();
|
|
||||||
ImGui::Spacing();
|
|
||||||
|
|
||||||
material::LabeledInputRow(TR("rpc_host"), connLbl.position, "##RPCHost",
|
|
||||||
s_rpc_host, sizeof(s_rpc_host), cmb.width);
|
|
||||||
|
|
||||||
material::LabeledInputRow(TR("rpc_port"), connLbl.position, "##RPCPort",
|
|
||||||
s_rpc_port, sizeof(s_rpc_port), portInput.width);
|
|
||||||
|
|
||||||
ImGui::Spacing();
|
|
||||||
|
|
||||||
material::LabeledInputRow(TR("rpc_user"), connLbl.position, "##RPCUser",
|
|
||||||
s_rpc_user, sizeof(s_rpc_user), cmb.width);
|
|
||||||
|
|
||||||
material::LabeledInputRow(TR("rpc_pass"), connLbl.position, "##RPCPassword",
|
|
||||||
s_rpc_password, sizeof(s_rpc_password), cmb.width, nullptr,
|
|
||||||
ImGuiInputTextFlags_Password);
|
|
||||||
|
|
||||||
ImGui::Spacing();
|
|
||||||
ImGui::Separator();
|
|
||||||
ImGui::Spacing();
|
|
||||||
|
|
||||||
ImGui::TextDisabled("%s", TR("settings_rpc_note"));
|
|
||||||
|
|
||||||
ImGui::Spacing();
|
|
||||||
|
|
||||||
if (material::StyledButton(TR("test_connection"), ImVec2(0,0), S.resolveFont("button"))) {
|
|
||||||
if (app->rpc()) {
|
|
||||||
rpc::RPCClient::TraceScope trace("Settings / Test connection");
|
|
||||||
app->rpc()->getInfo([](const nlohmann::json& result, const std::string& error) {
|
|
||||||
if (error.empty()) {
|
|
||||||
std::string version = result.value("version", "unknown");
|
|
||||||
std::string msg = std::string(TR("swin_connection_successful")) + version;
|
|
||||||
Notifications::instance().success(msg);
|
|
||||||
} else {
|
|
||||||
Notifications::instance().error(std::string(TR("swin_connection_failed")) + error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
Notifications::instance().error(TR("swin_rpc_client_not_initialized"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::EndTabItem();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wallet tab
|
|
||||||
if (ImGui::BeginTabItem(TR("wallet"))) {
|
|
||||||
ImGui::Spacing();
|
|
||||||
|
|
||||||
ImGui::Text("%s", TR("settings_wallet_maintenance"));
|
|
||||||
ImGui::Spacing();
|
|
||||||
ImGui::Separator();
|
|
||||||
ImGui::Spacing();
|
|
||||||
|
|
||||||
if (material::StyledButton(TR("rescan"), ImVec2(walletBtn.width, 0), S.resolveFont(walletBtn.font))) {
|
|
||||||
if (app->rpc()) {
|
|
||||||
// Start rescan from block 0
|
|
||||||
app->rpc()->rescanBlockchain(0, [](const nlohmann::json& result, const std::string& error) {
|
|
||||||
if (error.empty()) {
|
|
||||||
int start = result.value("start_height", 0);
|
|
||||||
int end = result.value("stop_height", 0);
|
|
||||||
std::string msg = std::string(TR("swin_rescan_started_from_block")) + std::to_string(start) +
|
|
||||||
TR("swin_rescan_to") + std::to_string(end);
|
|
||||||
Notifications::instance().success(msg);
|
|
||||||
} else {
|
|
||||||
Notifications::instance().error(std::string(TR("swin_rescan_failed")) + error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
Notifications::instance().error(TR("swin_rpc_client_not_initialized"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ImGui::TextDisabled(" %s", TR("settings_rescan_desc"));
|
|
||||||
|
|
||||||
ImGui::Spacing();
|
|
||||||
|
|
||||||
static bool s_confirm_clear_ztx = false;
|
|
||||||
if (material::StyledButton(TR("settings_clear_ztx_long"), ImVec2(walletBtn.width, 0), S.resolveFont(walletBtn.font))) {
|
|
||||||
s_confirm_clear_ztx = true;
|
|
||||||
}
|
|
||||||
ImGui::TextDisabled(" %s", TR("settings_clear_ztx_desc"));
|
|
||||||
|
|
||||||
// Confirmation dialog
|
|
||||||
if (s_confirm_clear_ztx) {
|
|
||||||
// Distinct idSuffix: this confirm renders nested inside (and the same frame as) the parent
|
|
||||||
// settings dialog, so it must not share the default ##OverlayDialogContent key — otherwise it
|
|
||||||
// inherits the parent's OverlayCardState (incl. the sticky overflow flag) and collides on the
|
|
||||||
// child window id.
|
|
||||||
if (material::BeginOverlayDialog(TR("confirm_clear_ztx_title"), &s_confirm_clear_ztx, 480.0f,
|
|
||||||
0.94f, 0.85f, "settings_clearztx")) {
|
|
||||||
material::DialogWarningHeader(TR("warning"), ImVec4(1.0f, 0.6f, 0.0f, 1.0f));
|
|
||||||
|
|
||||||
ImGui::Spacing();
|
|
||||||
ImGui::TextWrapped("%s", TR("confirm_clear_ztx_warning1"));
|
|
||||||
ImGui::Spacing();
|
|
||||||
ImGui::TextWrapped("%s", TR("confirm_clear_ztx_warning2"));
|
|
||||||
ImGui::Spacing();
|
|
||||||
ImGui::Separator();
|
|
||||||
ImGui::Spacing();
|
|
||||||
|
|
||||||
bool doCancel = false, doConfirm = false;
|
|
||||||
material::DialogConfirmFooter(TR("cancel"), TrId("clear_anyway", "clear_ztx_w").c_str(),
|
|
||||||
true, doCancel, doConfirm);
|
|
||||||
if (doCancel) {
|
|
||||||
s_confirm_clear_ztx = false;
|
|
||||||
}
|
|
||||||
if (doConfirm) {
|
|
||||||
std::string ztx_file = util::Platform::getDragonXDataDir() + "ztx_history.json";
|
|
||||||
if (util::Platform::deleteFile(ztx_file)) {
|
|
||||||
Notifications::instance().success(TR("swin_ztx_history_cleared"));
|
|
||||||
} else {
|
|
||||||
Notifications::instance().info(TR("swin_no_history_file_found"));
|
|
||||||
}
|
|
||||||
s_confirm_clear_ztx = false;
|
|
||||||
}
|
|
||||||
material::EndOverlayDialog();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::Spacing();
|
|
||||||
ImGui::Separator();
|
|
||||||
ImGui::Spacing();
|
|
||||||
|
|
||||||
ImGui::Text("%s", TR("settings_wallet_info"));
|
|
||||||
ImGui::Spacing();
|
|
||||||
|
|
||||||
// Get actual wallet size
|
|
||||||
std::string wallet_path = util::Platform::getDragonXDataDir() + "wallet.dat";
|
|
||||||
uint64_t wallet_size = util::Platform::getFileSize(wallet_path);
|
|
||||||
if (wallet_size > 0) {
|
|
||||||
std::string size_str = util::Platform::formatFileSize(wallet_size);
|
|
||||||
ImGui::Text(TR("settings_wallet_file_size"), size_str.c_str());
|
|
||||||
} else {
|
|
||||||
ImGui::TextDisabled("%s", TR("settings_wallet_not_found"));
|
|
||||||
}
|
|
||||||
ImGui::Text(TR("settings_wallet_location"), wallet_path.c_str());
|
|
||||||
|
|
||||||
ImGui::EndTabItem();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Explorer tab
|
|
||||||
if (ImGui::BeginTabItem(TR("explorer"))) {
|
|
||||||
ImGui::Spacing();
|
|
||||||
|
|
||||||
ImGui::Text("%s", TR("settings_block_explorer_urls"));
|
|
||||||
ImGui::TextDisabled("%s", TR("settings_configure_explorer"));
|
|
||||||
ImGui::Spacing();
|
|
||||||
ImGui::Separator();
|
|
||||||
ImGui::Spacing();
|
|
||||||
|
|
||||||
ImGui::Text("%s", TR("transaction_url"));
|
|
||||||
ImGui::SetNextItemWidth(-1);
|
|
||||||
ImGui::InputText("##TxExplorer", s_tx_explorer, sizeof(s_tx_explorer));
|
|
||||||
|
|
||||||
ImGui::Text("%s", TR("address_url"));
|
|
||||||
ImGui::SetNextItemWidth(-1);
|
|
||||||
ImGui::InputText("##AddrExplorer", s_addr_explorer, sizeof(s_addr_explorer));
|
|
||||||
|
|
||||||
ImGui::Spacing();
|
|
||||||
ImGui::TextDisabled("%s", TR("settings_explorer_hint"));
|
|
||||||
|
|
||||||
ImGui::EndTabItem();
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::EndTabBar();
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::Spacing();
|
|
||||||
ImGui::Separator();
|
|
||||||
ImGui::Spacing();
|
|
||||||
|
|
||||||
// Save/Cancel buttons
|
|
||||||
if (material::StyledButton(TR("save"), ImVec2(saveBtn.width, 0), S.resolveFont(saveBtn.font))) {
|
|
||||||
saveSettingsFromUI(app->settings());
|
|
||||||
Notifications::instance().success(TR("swin_settings_saved"));
|
|
||||||
*p_open = false;
|
|
||||||
}
|
|
||||||
ImGui::SameLine();
|
|
||||||
if (material::StyledButton(TR("cancel"), ImVec2(cancelBtn.width, 0), S.resolveFont(cancelBtn.font))) {
|
|
||||||
// Reload settings to revert changes
|
|
||||||
loadSettingsToUI(app->settings());
|
|
||||||
// Revert skin to what was active when settings opened
|
|
||||||
if (!s_saved_skin_id.empty()) {
|
|
||||||
schema::SkinManager::instance().setActiveSkin(s_saved_skin_id);
|
|
||||||
if (app->settings()) {
|
|
||||||
app->settings()->setSkinId(s_saved_skin_id);
|
|
||||||
app->settings()->save();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*p_open = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
material::EndOverlayDialog();
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace ui
|
|
||||||
} // namespace dragonx
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
// DragonX Wallet - ImGui Edition
|
|
||||||
// Copyright 2024-2026 The Hush Developers
|
|
||||||
// Released under the GPLv3
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
namespace dragonx {
|
|
||||||
class App;
|
|
||||||
|
|
||||||
namespace ui {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Render the Settings window
|
|
||||||
* Modal dialog for application settings
|
|
||||||
*/
|
|
||||||
void RenderSettingsWindow(App* app, bool* p_open);
|
|
||||||
|
|
||||||
} // namespace ui
|
|
||||||
} // namespace dragonx
|
|
||||||
@@ -41,6 +41,8 @@ static bool s_advanced = false; // Advanced (fee + batch size)
|
|||||||
static bool s_confirm = false; // inline "confirm before moving funds" phase
|
static bool s_confirm = false; // inline "confirm before moving funds" phase
|
||||||
static bool s_operation_pending = false;
|
static bool s_operation_pending = false;
|
||||||
static bool s_op_terminal = false; // async op reached success/failed — freeze inputs
|
static bool s_op_terminal = false; // async op reached success/failed — freeze inputs
|
||||||
|
enum class OpOutcome { None, Success, Failed, Uncertain };
|
||||||
|
static OpOutcome s_op_outcome = OpOutcome::None; // tags the terminal status for colour (display only)
|
||||||
static std::string s_operation_id;
|
static std::string s_operation_id;
|
||||||
static std::string s_status_message;
|
static std::string s_status_message;
|
||||||
static double s_last_poll = 0.0; // live-progress self-poll timer (ImGui::GetTime seconds)
|
static double s_last_poll = 0.0; // live-progress self-poll timer (ImGui::GetTime seconds)
|
||||||
@@ -68,6 +70,7 @@ static void resetTransient()
|
|||||||
++s_poll_session;
|
++s_poll_session;
|
||||||
s_operation_pending = false;
|
s_operation_pending = false;
|
||||||
s_op_terminal = false;
|
s_op_terminal = false;
|
||||||
|
s_op_outcome = OpOutcome::None;
|
||||||
s_confirm = false;
|
s_confirm = false;
|
||||||
s_status_message.clear();
|
s_status_message.clear();
|
||||||
s_operation_id.clear();
|
s_operation_id.clear();
|
||||||
@@ -201,6 +204,25 @@ static double srcAmount() { return s_src == 0 ? s_t_amount : s_src == 1 ? s_z_am
|
|||||||
|
|
||||||
static std::string fmtAmt(double v) { char b[48]; std::snprintf(b, sizeof(b), "%.4f", v); return b; }
|
static std::string fmtAmt(double v) { char b[48]; std::snprintf(b, sizeof(b), "%.4f", v); return b; }
|
||||||
|
|
||||||
|
// Shield-coinbase source helpers. "*" = all spendable mining (transparent) addresses; else one address.
|
||||||
|
// Balances are DISPLAY totals (may include immature/non-coinbase), so the UI wording says "up to".
|
||||||
|
static double miningBalance(const WalletState& st, const char* from) {
|
||||||
|
if (std::strcmp(from, "*") == 0) {
|
||||||
|
double sum = 0.0;
|
||||||
|
for (const auto& a : st.t_addresses) if (a.isSpendable()) sum += a.balance;
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
const int i = addressIndexByString(st.t_addresses, from);
|
||||||
|
return i >= 0 ? st.t_addresses[i].balance : 0.0;
|
||||||
|
}
|
||||||
|
static int miningAddrCount(const WalletState& st, const char* from) {
|
||||||
|
if (std::strcmp(from, "*") == 0) {
|
||||||
|
int n = 0; for (const auto& a : st.t_addresses) if (a.isSpendable() && a.balance > 0.0) ++n; return n;
|
||||||
|
}
|
||||||
|
const int i = addressIndexByString(st.t_addresses, from);
|
||||||
|
return (i >= 0 && st.t_addresses[i].balance > 0.0) ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
// A caption-style line that wraps at the dialog edge (Type().textColored doesn't wrap).
|
// A caption-style line that wraps at the dialog edge (Type().textColored doesn't wrap).
|
||||||
static void wrappedCaption(ImU32 color, const char* text)
|
static void wrappedCaption(ImU32 color, const char* text)
|
||||||
{
|
{
|
||||||
@@ -232,6 +254,7 @@ static void submitOperation(App* app)
|
|||||||
if (!app->worker()) return; // before the pending state, so a missing worker can't wedge it
|
if (!app->worker()) return; // before the pending state, so a missing worker can't wedge it
|
||||||
s_operation_pending = true;
|
s_operation_pending = true;
|
||||||
s_op_terminal = false;
|
s_op_terminal = false;
|
||||||
|
s_op_outcome = OpOutcome::None;
|
||||||
s_status_message = TR("shield_submitting");
|
s_status_message = TR("shield_submitting");
|
||||||
s_last_poll = ImGui::GetTime();
|
s_last_poll = ImGui::GetTime();
|
||||||
|
|
||||||
@@ -253,8 +276,9 @@ static void submitOperation(App* app)
|
|||||||
app->trackOperation(s_operation_id);
|
app->trackOperation(s_operation_id);
|
||||||
} else {
|
} else {
|
||||||
s_operation_pending = false; s_op_terminal = true;
|
s_operation_pending = false; s_op_terminal = true;
|
||||||
s_status_message = std::string(TR("shield_error_prefix")) + error;
|
s_op_outcome = OpOutcome::Failed;
|
||||||
Notifications::instance().error(std::string(TR("shield_send_failed")) + error);
|
s_status_message = std::string(TR("shield_error_prefix")) + util::friendlyRpcError(error);
|
||||||
|
Notifications::instance().error(std::string(TR("shield_send_failed")) + util::friendlyRpcError(error));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -309,13 +333,15 @@ static void submitOperation(App* app)
|
|||||||
app->trackOperation(s_operation_id);
|
app->trackOperation(s_operation_id);
|
||||||
} else if (uncertain) {
|
} else if (uncertain) {
|
||||||
s_operation_pending = false; s_op_terminal = true;
|
s_operation_pending = false; s_op_terminal = true;
|
||||||
|
s_op_outcome = OpOutcome::Uncertain;
|
||||||
s_confirm = false; // don't leave Confirm armed for a merge that may still be running
|
s_confirm = false; // don't leave Confirm armed for a merge that may still be running
|
||||||
s_status_message = TR("merge_submit_uncertain");
|
s_status_message = TR("merge_submit_uncertain");
|
||||||
Notifications::instance().warning(TR("merge_submit_uncertain"));
|
Notifications::instance().warning(TR("merge_submit_uncertain"));
|
||||||
} else {
|
} else {
|
||||||
s_operation_pending = false; s_op_terminal = true;
|
s_operation_pending = false; s_op_terminal = true;
|
||||||
s_status_message = std::string(TR("shield_error_prefix")) + error;
|
s_op_outcome = OpOutcome::Failed;
|
||||||
Notifications::instance().error(std::string(TR("merge_send_failed")) + error);
|
s_status_message = std::string(TR("shield_error_prefix")) + util::friendlyRpcError(error);
|
||||||
|
Notifications::instance().error(std::string(TR("merge_send_failed")) + util::friendlyRpcError(error));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -340,6 +366,7 @@ static void submitPerAddress(App* app)
|
|||||||
s_confirm = false;
|
s_confirm = false;
|
||||||
s_status_message.clear(); // an earlier "Into one" error / uncertain text belongs to that op
|
s_status_message.clear(); // an earlier "Into one" error / uncertain text belongs to that op
|
||||||
s_op_terminal = false;
|
s_op_terminal = false;
|
||||||
|
s_op_outcome = OpOutcome::None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -384,6 +411,7 @@ static void pollOperation(App* app)
|
|||||||
// client popped it with z_getoperationresult). It registers an op before returning its id,
|
// client popped it with z_getoperationresult). It registers an op before returning its id,
|
||||||
// so this is no race. The outcome can't be confirmed; stop polling.
|
// so this is no race. The outcome can't be confirmed; stop polling.
|
||||||
s_operation_pending = false; s_op_terminal = true;
|
s_operation_pending = false; s_op_terminal = true;
|
||||||
|
s_op_outcome = OpOutcome::Uncertain;
|
||||||
s_status_message = TR("send_status_unconfirmed");
|
s_status_message = TR("send_status_unconfirmed");
|
||||||
Notifications::instance().warning(TR("send_status_unconfirmed"));
|
Notifications::instance().warning(TR("send_status_unconfirmed"));
|
||||||
return;
|
return;
|
||||||
@@ -393,6 +421,7 @@ static void pollOperation(App* app)
|
|||||||
? op["status"].get<std::string>() : std::string();
|
? op["status"].get<std::string>() : std::string();
|
||||||
if (status == "success") {
|
if (status == "success") {
|
||||||
s_operation_pending = false; s_op_terminal = true;
|
s_operation_pending = false; s_op_terminal = true;
|
||||||
|
s_op_outcome = OpOutcome::Success;
|
||||||
s_status_message = TR("shield_completed");
|
s_status_message = TR("shield_completed");
|
||||||
Notifications::instance().success(TR("shield_merge_done"));
|
Notifications::instance().success(TR("shield_merge_done"));
|
||||||
} else if (status == "failed" || status == "cancelled" || status == "canceled") {
|
} else if (status == "failed" || status == "cancelled" || status == "canceled") {
|
||||||
@@ -401,6 +430,7 @@ static void pollOperation(App* app)
|
|||||||
op["error"]["message"].is_string())
|
op["error"]["message"].is_string())
|
||||||
msg = op["error"]["message"].get<std::string>();
|
msg = op["error"]["message"].get<std::string>();
|
||||||
s_operation_pending = false; s_op_terminal = true;
|
s_operation_pending = false; s_op_terminal = true;
|
||||||
|
s_op_outcome = OpOutcome::Failed;
|
||||||
s_status_message = std::string(TR("shield_op_failed")) + msg;
|
s_status_message = std::string(TR("shield_op_failed")) + msg;
|
||||||
// No toast: the op is tracked (App::trackOperation) and the shared opid poller raises the
|
// No toast: the op is tracked (App::trackOperation) and the shared opid poller raises the
|
||||||
// one failure toast, whether this dialog is open or not.
|
// one failure toast, whether this dialog is open or not.
|
||||||
@@ -523,9 +553,12 @@ void ShieldDialog::render(App* app)
|
|||||||
if (s_scan.phase == services::ScopePhase::Loaded && listed == 0) {
|
if (s_scan.phase == services::ScopePhase::Loaded && listed == 0) {
|
||||||
wrappedCaption(material::Warning(), TR("merge_keep_each_none"));
|
wrappedCaption(material::Warning(), TR("merge_keep_each_none"));
|
||||||
} else if (listed > 0) {
|
} else if (listed > 0) {
|
||||||
|
const float eachPad = 10.0f * dp; // inner padding (the floating overlay leaves ambient 0)
|
||||||
const float listH = std::min(static_cast<float>(listed), static_cast<float>(maxRows)) * rowH +
|
const float listH = std::min(static_cast<float>(listed), static_cast<float>(maxRows)) * rowH +
|
||||||
ImGui::GetStyle().WindowPadding.y * 2.0f;
|
eachPad * 2.0f;
|
||||||
if (ImGui::BeginChild("##ConsolidateEach", ImVec2(-1, listH), ImGuiChildFlags_Borders)) {
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(eachPad, eachPad));
|
||||||
|
if (ImGui::BeginChild("##ConsolidateEach", ImVec2(-1, listH),
|
||||||
|
ImGuiChildFlags_Borders | ImGuiChildFlags_AlwaysUseWindowPadding)) {
|
||||||
for (const auto& g : s_groups) {
|
for (const auto& g : s_groups) {
|
||||||
if (g.notes < 2) continue;
|
if (g.notes < 2) continue;
|
||||||
const bool worth = services::worthConsolidating(g, s_fee);
|
const bool worth = services::worthConsolidating(g, s_fee);
|
||||||
@@ -549,6 +582,7 @@ void ShieldDialog::render(App* app)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ImGui::EndChild();
|
ImGui::EndChild();
|
||||||
|
ImGui::PopStyleVar(); // ##ConsolidateEach inner padding
|
||||||
}
|
}
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
}
|
}
|
||||||
@@ -556,8 +590,32 @@ void ShieldDialog::render(App* app)
|
|||||||
|
|
||||||
// ── Shield coinbase: from address ───────────────────────────────────────────────────────────
|
// ── Shield coinbase: from address ───────────────────────────────────────────────────────────
|
||||||
if (!isMerge && !opInFlight) {
|
if (!isMerge && !opInFlight) {
|
||||||
material::LabeledInput(TR("shield_from_address"), "##FromAddr", s_from_address, sizeof(s_from_address));
|
// Pick the mining (transparent) address to shield from a dropdown with balances, instead of a
|
||||||
ImGui::TextDisabled("%s", TR("shield_wildcard_hint"));
|
// free-text box with a "*" wildcard. "*" shields all of them.
|
||||||
|
ImGui::TextUnformatted(TR("shield_from_address"));
|
||||||
|
const bool allFrom = std::strcmp(s_from_address, "*") == 0;
|
||||||
|
const std::string fromLbl = allFrom ? std::string(TR("shield_all_mining_addrs"))
|
||||||
|
: (app->getAddressLabel(s_from_address).empty() ? shortAddr(s_from_address)
|
||||||
|
: app->getAddressLabel(s_from_address));
|
||||||
|
ImGui::SetNextItemWidth(-1);
|
||||||
|
if (ImGui::BeginCombo("##FromAddr", fromLbl.c_str())) {
|
||||||
|
if (ImGui::Selectable(TR("shield_all_mining_addrs"), allFrom))
|
||||||
|
strncpy(s_from_address, "*", sizeof(s_from_address));
|
||||||
|
for (const auto& a : state.t_addresses) {
|
||||||
|
if (!a.isSpendable()) continue;
|
||||||
|
const std::string lbl = app->getAddressLabel(a.address);
|
||||||
|
char item[192];
|
||||||
|
std::snprintf(item, sizeof(item), "%s (%s DRGX)",
|
||||||
|
lbl.empty() ? shortAddr(a.address).c_str() : lbl.c_str(),
|
||||||
|
fmtAmt(a.balance).c_str());
|
||||||
|
const bool sel = std::strcmp(s_from_address, a.address.c_str()) == 0;
|
||||||
|
if (ImGui::Selectable(item, sel)) {
|
||||||
|
strncpy(s_from_address, a.address.c_str(), sizeof(s_from_address) - 1);
|
||||||
|
s_from_address[sizeof(s_from_address) - 1] = '\0';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::EndCombo();
|
||||||
|
}
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -666,16 +724,29 @@ void ShieldDialog::render(App* app)
|
|||||||
std::max(1, p.batchesEstimated), p.notesMerged);
|
std::max(1, p.batchesEstimated), p.notesMerged);
|
||||||
wrappedCaption(material::OnSurfaceMedium(), cap);
|
wrappedCaption(material::OnSurfaceMedium(), cap);
|
||||||
if (!p.stopping) {
|
if (!p.stopping) {
|
||||||
if (const char* key = services::runWaitKey(run.lastWait())) wrappedCaption(material::OnSurfaceMedium(), TR(key));
|
// The pause notice (waiting for a batch gap / sync / a send) toggles on and off every batch.
|
||||||
|
// Reserve a fixed two-line slot for it so its coming and going doesn't change the content
|
||||||
|
// height and make the auto-height, vertically-centred modal jump around during the run.
|
||||||
|
const float waitTop = ImGui::GetCursorPosY();
|
||||||
|
if (const char* key = services::runWaitKey(run.lastWait()))
|
||||||
|
wrappedCaption(material::OnSurfaceMedium(), TR(key));
|
||||||
|
const float waitSlotH = ImGui::GetTextLineHeightWithSpacing() * 2.0f;
|
||||||
|
if (ImGui::GetCursorPosY() < waitTop + waitSlotH) ImGui::SetCursorPosY(waitTop + waitSlotH);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ImGui::ProgressBar(static_cast<float>(p.batchesDone) / static_cast<float>(std::max(1, p.batchesEstimated)),
|
ImGui::ProgressBar(static_cast<float>(p.batchesDone) / static_cast<float>(std::max(1, p.batchesEstimated)),
|
||||||
ImVec2(-1, 0));
|
ImVec2(-1, 0));
|
||||||
const auto& addrs = run.addresses();
|
const auto& addrs = run.addresses();
|
||||||
|
// Inner padding for the bordered list: this floating overlay pops its content padding, so the
|
||||||
|
// ambient WindowPadding is 0 and a bordered child's text would otherwise touch the border. Push a
|
||||||
|
// small padding (and reserve it in the height) so the rows sit inset.
|
||||||
|
const float listPad = 10.0f * dp;
|
||||||
const float runListH = std::min(static_cast<float>(addrs.size()), static_cast<float>(maxRows)) * rowH +
|
const float runListH = std::min(static_cast<float>(addrs.size()), static_cast<float>(maxRows)) * rowH +
|
||||||
ImGui::GetStyle().WindowPadding.y * 2.0f;
|
listPad * 2.0f;
|
||||||
bool leftovers = false;
|
bool leftovers = false;
|
||||||
if (ImGui::BeginChild("##ConsolidateRuns", ImVec2(-1, runListH), ImGuiChildFlags_Borders)) {
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(listPad, listPad));
|
||||||
|
if (ImGui::BeginChild("##ConsolidateRuns", ImVec2(-1, runListH),
|
||||||
|
ImGuiChildFlags_Borders | ImGuiChildFlags_AlwaysUseWindowPadding)) {
|
||||||
for (const auto& a : addrs) {
|
for (const auto& a : addrs) {
|
||||||
const std::string label = app->getAddressLabel(a.group.address);
|
const std::string label = app->getAddressLabel(a.group.address);
|
||||||
const std::string name = label.empty() ? shortAddr(a.group.address) : label;
|
const std::string name = label.empty() ? shortAddr(a.group.address) : label;
|
||||||
@@ -711,10 +782,22 @@ void ShieldDialog::render(App* app)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ImGui::EndChild();
|
ImGui::EndChild();
|
||||||
|
ImGui::PopStyleVar(); // ##ConsolidateRuns inner padding
|
||||||
|
{
|
||||||
|
// The "run again" hint appears partway through a run (once an address has leftover notes). While
|
||||||
|
// the run is live, reserve its measured height up front so it appearing doesn't shift the modal.
|
||||||
|
const float loTop = ImGui::GetCursorPosY();
|
||||||
if (leftovers) {
|
if (leftovers) {
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
wrappedCaption(material::OnSurfaceMedium(), TR("merge_keep_each_run_again"));
|
wrappedCaption(material::OnSurfaceMedium(), TR("merge_keep_each_run_again"));
|
||||||
}
|
}
|
||||||
|
if (live) {
|
||||||
|
const float loH = ImGui::GetStyle().ItemSpacing.y +
|
||||||
|
ImGui::CalcTextSize(TR("merge_keep_each_run_again"), nullptr, false,
|
||||||
|
ImGui::GetContentRegionAvail().x).y;
|
||||||
|
if (ImGui::GetCursorPosY() < loTop + loH) ImGui::SetCursorPosY(loTop + loH);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!live) { // below the rows: merge_keep_each_some_failed says "see the list above"
|
if (!live) { // below the rows: merge_keep_each_some_failed says "see the list above"
|
||||||
std::string endLine;
|
std::string endLine;
|
||||||
ImU32 endCol = material::OnSurfaceMedium();
|
ImU32 endCol = material::OnSurfaceMedium();
|
||||||
@@ -746,10 +829,18 @@ void ShieldDialog::render(App* app)
|
|||||||
|
|
||||||
// ── Live progress / status ──────────────────────────────────────────────────────────────────
|
// ── Live progress / status ──────────────────────────────────────────────────────────────────
|
||||||
if (!s_status_message.empty() && !(isMerge && run.hasRun())) { // the single-op path's message, not the run's
|
if (!s_status_message.empty() && !(isMerge && run.hasRun())) { // the single-op path's message, not the run's
|
||||||
if (s_operation_pending && !s_op_terminal)
|
const bool inFlight = s_operation_pending && !s_op_terminal;
|
||||||
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), "%s", s_status_message.c_str());
|
const ImU32 stCol = inFlight ? material::Warning()
|
||||||
|
: s_op_outcome == OpOutcome::Success ? material::Success()
|
||||||
|
: s_op_outcome == OpOutcome::Failed ? material::Error()
|
||||||
|
: s_op_outcome == OpOutcome::Uncertain ? material::Warning()
|
||||||
|
: material::OnSurfaceMedium();
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(stCol));
|
||||||
|
if (inFlight)
|
||||||
|
ImGui::Text("%s%s", s_status_message.c_str(), material::LoadingDots()); // spinner while scanning
|
||||||
else
|
else
|
||||||
ImGui::TextWrapped("%s", s_status_message.c_str());
|
ImGui::TextWrapped("%s", s_status_message.c_str());
|
||||||
|
ImGui::PopStyleColor();
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -814,9 +905,24 @@ void ShieldDialog::render(App* app)
|
|||||||
fmtAmt(srcAmount()).c_str(), srcCount(), shortAddr(s_to_address).c_str());
|
fmtAmt(srcAmount()).c_str(), srcCount(), shortAddr(s_to_address).c_str());
|
||||||
ImGui::TextWrapped("%s", cb);
|
ImGui::TextWrapped("%s", cb);
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
} else if (s_confirm && !isMerge) {
|
||||||
|
// Shield-coinbase preview: amount / address count / fee / destination (no amount/count was shown before).
|
||||||
|
const bool allFrom = std::strcmp(s_from_address, "*") == 0;
|
||||||
|
const std::string toLbl = app->getAddressLabel(s_to_address);
|
||||||
|
char cb[360];
|
||||||
|
std::snprintf(cb, sizeof(cb), TR("shield_confirm_fmt"),
|
||||||
|
fmtAmt(miningBalance(state, s_from_address)).c_str(),
|
||||||
|
allFrom ? miningAddrCount(state, s_from_address) : 1,
|
||||||
|
s_utxo_limit,
|
||||||
|
toLbl.empty() ? shortAddr(s_to_address).c_str() : toLbl.c_str(),
|
||||||
|
fmtAmt(s_fee).c_str());
|
||||||
|
ImGui::TextWrapped("%s", cb);
|
||||||
|
ImGui::Spacing();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool can_submit = haveDest && connected && !syncing;
|
bool can_submit = haveDest && connected && !syncing;
|
||||||
|
// Block a shield from an empty source or one with no funds (the free-text box allowed both).
|
||||||
|
if (!isMerge && (s_from_address[0] == '\0' || miningBalance(state, s_from_address) <= 0.0)) can_submit = false;
|
||||||
const bool scopeLoaded = s_scan.phase == services::ScopePhase::Loaded;
|
const bool scopeLoaded = s_scan.phase == services::ScopePhase::Loaded;
|
||||||
if (isMerge && scopeLoaded && srcCount() == 0) can_submit = false;
|
if (isMerge && scopeLoaded && srcCount() == 0) can_submit = false;
|
||||||
if (perAddress) can_submit = connected && !syncing && scopeLoaded && !selectedGroups().empty();
|
if (perAddress) can_submit = connected && !syncing && scopeLoaded && !selectedGroups().empty();
|
||||||
|
|||||||
@@ -58,16 +58,16 @@ void TransactionDetailsDialog::render(App* app)
|
|||||||
ImVec4 type_color;
|
ImVec4 type_color;
|
||||||
std::string type_display;
|
std::string type_display;
|
||||||
if (tx.type == "receive") {
|
if (tx.type == "receive") {
|
||||||
type_color = ImVec4(0.3f, 0.8f, 0.3f, 1.0f);
|
type_color = material::SuccessVec4();
|
||||||
type_display = TR("tx_received");
|
type_display = TR("tx_received");
|
||||||
} else if (tx.type == "send") {
|
} else if (tx.type == "send") {
|
||||||
type_color = ImVec4(0.8f, 0.3f, 0.3f, 1.0f);
|
type_color = material::ErrorVec4();
|
||||||
type_display = TR("tx_sent");
|
type_display = TR("tx_sent");
|
||||||
} else if (tx.type == "generate" || tx.type == "mined") {
|
} else if (tx.type == "generate" || tx.type == "mined") {
|
||||||
type_color = ImVec4(0.3f, 0.6f, 0.9f, 1.0f);
|
type_color = ImVec4(0.3f, 0.6f, 0.9f, 1.0f);
|
||||||
type_display = TR("tx_mined");
|
type_display = TR("tx_mined");
|
||||||
} else if (tx.type == "immature") {
|
} else if (tx.type == "immature") {
|
||||||
type_color = ImVec4(0.8f, 0.8f, 0.3f, 1.0f);
|
type_color = material::WarningVec4();
|
||||||
type_display = TR("tx_immature");
|
type_display = TR("tx_immature");
|
||||||
} else {
|
} else {
|
||||||
type_color = ImVec4(0.7f, 0.7f, 0.7f, 1.0f);
|
type_color = ImVec4(0.7f, 0.7f, 0.7f, 1.0f);
|
||||||
@@ -79,11 +79,11 @@ void TransactionDetailsDialog::render(App* app)
|
|||||||
|
|
||||||
// Confirmations badge
|
// Confirmations badge
|
||||||
if (tx.confirmations == 0) {
|
if (tx.confirmations == 0) {
|
||||||
ImGui::TextColored(ImVec4(0.8f, 0.6f, 0.0f, 1.0f), "%s", TR("pending"));
|
ImGui::TextColored(material::WarningVec4(), "%s", TR("pending"));
|
||||||
} else if (tx.confirmations < 10) {
|
} else if (tx.confirmations < 10) {
|
||||||
ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.3f, 1.0f), TR("tx_confirmations"), tx.confirmations);
|
ImGui::TextColored(material::WarningVec4(), TR("tx_confirmations"), tx.confirmations);
|
||||||
} else {
|
} else {
|
||||||
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), TR("tx_confirmations"), tx.confirmations);
|
ImGui::TextColored(material::SuccessVec4(), TR("tx_confirmations"), tx.confirmations);
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
@@ -94,9 +94,9 @@ void TransactionDetailsDialog::render(App* app)
|
|||||||
ImGui::Text("%s", TR("amount_label"));
|
ImGui::Text("%s", TR("amount_label"));
|
||||||
ImGui::SameLine(lbl.position);
|
ImGui::SameLine(lbl.position);
|
||||||
if (tx.amount >= 0) {
|
if (tx.amount >= 0) {
|
||||||
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "+%.8f DRGX", tx.amount);
|
ImGui::TextColored(material::SuccessVec4(), "+%.8f DRGX", tx.amount);
|
||||||
} else {
|
} else {
|
||||||
ImGui::TextColored(ImVec4(0.8f, 0.3f, 0.3f, 1.0f), "%.8f DRGX", tx.amount);
|
ImGui::TextColored(material::ErrorVec4(), "%.8f DRGX", tx.amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
// USD equivalent if price available
|
// USD equivalent if price available
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
// Released under the GPLv3
|
// Released under the GPLv3
|
||||||
|
|
||||||
#include "transactions_tab.h"
|
#include "transactions_tab.h"
|
||||||
|
#include "../notifications.h"
|
||||||
#include "transaction_details_dialog.h"
|
#include "transaction_details_dialog.h"
|
||||||
#include "export_transactions_dialog.h"
|
#include "export_transactions_dialog.h"
|
||||||
#include "transactions_tab_helpers.h"
|
#include "transactions_tab_helpers.h"
|
||||||
@@ -36,6 +37,11 @@ namespace ui {
|
|||||||
|
|
||||||
using namespace material;
|
using namespace material;
|
||||||
|
|
||||||
|
// Latest History-tab filter snapshot, published from the filtered-list rebuild below. Used by the
|
||||||
|
// transactions CSV export to offer "export only the current filter".
|
||||||
|
static TxFilterSnapshot s_filter_snapshot;
|
||||||
|
const TxFilterSnapshot& GetTransactionsFilterSnapshot() { return s_filter_snapshot; }
|
||||||
|
|
||||||
static std::string TrId(const char* key, const char* id) {
|
static std::string TrId(const char* key, const char* id) {
|
||||||
return std::string(TR(key)) + "##" + id;
|
return std::string(TR(key)) + "##" + id;
|
||||||
}
|
}
|
||||||
@@ -213,10 +219,11 @@ void RenderTransactionsTab(App* app)
|
|||||||
// txid grouping shared by the summary pass and the display pass. Indices only, rebuilt once per tx_revision
|
// txid grouping shared by the summary pass and the display pass. Indices only, rebuilt once per tx_revision
|
||||||
// (a sort-combo change re-sorts without regrouping).
|
// (a sort-combo change re-sorts without regrouping).
|
||||||
static history::TxidGroups s_groups;
|
static history::TxidGroups s_groups;
|
||||||
static std::uint64_t s_groups_key = 0; // 0 never matches: tx_revision starts at 1 and only grows
|
static std::uint64_t s_groups_key = 0; // 0 never matches: tx_structure_revision starts at 1 and only grows
|
||||||
if (s_groups_key != contentKey) {
|
const std::uint64_t structKey = state.tx_structure_revision; // ignores pure confirmation-count bumps
|
||||||
|
if (s_groups_key != structKey) {
|
||||||
s_groups = history::groupByTxid(state.transactions);
|
s_groups = history::groupByTxid(state.transactions);
|
||||||
s_groups_key = contentKey;
|
s_groups_key = structKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Summary-card totals: Received / Sent / Mined (counts + amounts). These are three static
|
// Summary-card totals: Received / Sent / Mined (counts + amounts). These are three static
|
||||||
@@ -409,13 +416,14 @@ void RenderTransactionsTab(App* app)
|
|||||||
// case while the user reads/scrolls) the cache is reused. The result is already sorted newest-first by
|
// case while the user reads/scrolls) the cache is reused. The result is already sorted newest-first by
|
||||||
// the refresh service, but we re-sort here to apply the "pending first" ordering.
|
// the refresh service, but we re-sort here to apply the "pending first" ordering.
|
||||||
static std::vector<DisplayTx> s_display_cache;
|
static std::vector<DisplayTx> s_display_cache;
|
||||||
static std::uint64_t s_display_cache_key = 0; // tx_revision the cache was built from
|
static std::uint64_t s_display_cache_key = 0; // tx_structure_revision the cache was built from
|
||||||
|
static std::uint64_t s_display_conf_rev = 0; // tx_revision the cached confirmations were last synced to
|
||||||
static int s_display_cache_sort = 0; // sort mode the cache was built with
|
static int s_display_cache_sort = 0; // sort mode the cache was built with
|
||||||
static bool s_display_cache_valid = false;
|
static bool s_display_cache_valid = false;
|
||||||
static std::uint64_t s_display_gen = 0; // bumped on every rebuild; keys the filtered-index cache below
|
static std::uint64_t s_display_gen = 0; // bumped on every rebuild; keys the filtered-index cache below
|
||||||
|
|
||||||
// Two separate fields, not a mixed hash: small revision/sort values would collide under xor-mixing.
|
// Two separate fields, not a mixed hash: small revision/sort values would collide under xor-mixing.
|
||||||
if (!s_display_cache_valid || contentKey != s_display_cache_key || s_sort_mode != s_display_cache_sort) {
|
if (!s_display_cache_valid || structKey != s_display_cache_key || s_sort_mode != s_display_cache_sort) {
|
||||||
++s_display_gen;
|
++s_display_gen;
|
||||||
s_display_cache.clear();
|
s_display_cache.clear();
|
||||||
std::vector<DisplayTx>& display_txns = s_display_cache;
|
std::vector<DisplayTx>& display_txns = s_display_cache;
|
||||||
@@ -518,9 +526,27 @@ void RenderTransactionsTab(App* app)
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
s_display_cache_key = contentKey;
|
s_display_cache_key = structKey;
|
||||||
|
s_display_conf_rev = contentKey; // freshly built from the current list
|
||||||
s_display_cache_sort = s_sort_mode;
|
s_display_cache_sort = s_sort_mode;
|
||||||
s_display_cache_valid = true;
|
s_display_cache_valid = true;
|
||||||
|
} else if (s_display_conf_rev != contentKey) {
|
||||||
|
// Confirmation-count-only delta (setTransactionsIfChanged kept tx_structure_revision): the merged rows, their
|
||||||
|
// order and the indices into state.transactions are unchanged, so refresh just the count (same rule as the
|
||||||
|
// build above: a shield shows the larger of its two legs). Sort order cannot change: mode 0 only reads
|
||||||
|
// conf==0, which the layout check holds constant, and the other modes ignore confirmations.
|
||||||
|
for (DisplayTx& dtx : s_display_cache) {
|
||||||
|
if (dtx.is_shield) {
|
||||||
|
dtx.confirmations = std::max(state.transactions[dtx.send_idx].confirmations,
|
||||||
|
state.transactions[dtx.recv_idx].confirmations);
|
||||||
|
} else {
|
||||||
|
dtx.confirmations = state.transactions[dtx.orig_idx].confirmations;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The filtered-index cache keys on s_display_gen (unchanged, correct: filters ignore confirmations) and
|
||||||
|
// the snapshot's indices remain valid; only re-stamp it so 'export current filter' stays usable.
|
||||||
|
if (s_filter_snapshot.valid) s_filter_snapshot.txRevision = contentKey;
|
||||||
|
s_display_conf_rev = contentKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The merged + sorted list (rebuilt above only on change). Pagination below runs per-frame (it only
|
// The merged + sorted list (rebuilt above only on change). Pagination below runs per-frame (it only
|
||||||
@@ -565,6 +591,35 @@ void RenderTransactionsTab(App* app)
|
|||||||
s_filtered_search = search_str;
|
s_filtered_search = search_str;
|
||||||
s_filtered_chat_rev = chatRev;
|
s_filtered_chat_rev = chatRev;
|
||||||
s_filtered_valid = true;
|
s_filtered_valid = true;
|
||||||
|
|
||||||
|
// Publish a snapshot of the visible set for "export current filter". A merged autoshield row
|
||||||
|
// contributes BOTH original legs, so the CSV stays a per-leg record like an unfiltered export.
|
||||||
|
s_filter_snapshot.indices.clear();
|
||||||
|
s_filter_snapshot.indices.reserve(s_filtered_cache.size() * 2);
|
||||||
|
for (size_t fi : s_filtered_cache) {
|
||||||
|
const auto& dtx = display_txns[fi];
|
||||||
|
if (dtx.is_shield) {
|
||||||
|
if (dtx.send_idx >= 0) s_filter_snapshot.indices.push_back(static_cast<size_t>(dtx.send_idx));
|
||||||
|
if (dtx.recv_idx >= 0) s_filter_snapshot.indices.push_back(static_cast<size_t>(dtx.recv_idx));
|
||||||
|
} else if (dtx.orig_idx >= 0) {
|
||||||
|
s_filter_snapshot.indices.push_back(static_cast<size_t>(dtx.orig_idx));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::sort(s_filter_snapshot.indices.begin(), s_filter_snapshot.indices.end());
|
||||||
|
s_filter_snapshot.active = (type_filter != 0 || !search_str.empty());
|
||||||
|
s_filter_snapshot.txRevision = contentKey;
|
||||||
|
s_filter_snapshot.valid = true;
|
||||||
|
{
|
||||||
|
const char* typeNames[] = { "", TR("sent_filter"), TR("received_filter"),
|
||||||
|
TR("mined_filter"), TR("chat_filter") };
|
||||||
|
std::string desc = (type_filter >= 1 && type_filter <= 4) ? typeNames[type_filter] : "";
|
||||||
|
if (!search_str.empty()) {
|
||||||
|
if (!desc.empty()) desc += ", ";
|
||||||
|
desc += "\"" + search_str + "\"";
|
||||||
|
}
|
||||||
|
if (desc.empty()) desc = TR("all_filter");
|
||||||
|
s_filter_snapshot.description = desc;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const std::vector<size_t>& filtered_indices = s_filtered_cache;
|
const std::vector<size_t>& filtered_indices = s_filtered_cache;
|
||||||
filtered_count = static_cast<int>(filtered_indices.size());
|
filtered_count = static_cast<int>(filtered_indices.size());
|
||||||
@@ -800,7 +855,12 @@ void RenderTransactionsTab(App* app)
|
|||||||
{
|
{
|
||||||
const char* statusStr;
|
const char* statusStr;
|
||||||
ImU32 statusCol;
|
ImU32 statusCol;
|
||||||
if (tx.confirmations == 0) {
|
const int64_t nowSec = static_cast<int64_t>(std::time(nullptr));
|
||||||
|
if (tx.confirmations < 0) { // gettransaction < 0: conflicted / double-spent / reorged out
|
||||||
|
statusStr = TR("tx_dropped"); statusCol = Error();
|
||||||
|
} else if (tx.confirmations == 0 && tx.timestamp > 0 && nowSec - tx.timestamp > 3600) {
|
||||||
|
statusStr = TR("tx_stale_pending"); statusCol = Warning();
|
||||||
|
} else if (tx.confirmations == 0) {
|
||||||
statusStr = TR("pending"); statusCol = Warning();
|
statusStr = TR("pending"); statusCol = Warning();
|
||||||
} else if (tx.confirmations < 10) {
|
} else if (tx.confirmations < 10) {
|
||||||
snprintf(buf, sizeof(buf), TR("conf_count"), tx.confirmations);
|
snprintf(buf, sizeof(buf), TR("conf_count"), tx.confirmations);
|
||||||
@@ -871,9 +931,11 @@ void RenderTransactionsTab(App* app)
|
|||||||
if (effects::ImGuiAcrylic::BeginAcrylicContextItem("TxContext", 0, acrylicTheme.menu)) {
|
if (effects::ImGuiAcrylic::BeginAcrylicContextItem("TxContext", 0, acrylicTheme.menu)) {
|
||||||
if (ImGui::MenuItem(TR("copy_address")) && !tx.address.empty()) {
|
if (ImGui::MenuItem(TR("copy_address")) && !tx.address.empty()) {
|
||||||
ImGui::SetClipboardText(tx.address.c_str());
|
ImGui::SetClipboardText(tx.address.c_str());
|
||||||
|
Notifications::instance().info(TR("copied_to_clipboard"));
|
||||||
}
|
}
|
||||||
if (ImGui::MenuItem(TR("copy_txid"))) {
|
if (ImGui::MenuItem(TR("copy_txid"))) {
|
||||||
ImGui::SetClipboardText(tx.txid.c_str());
|
ImGui::SetClipboardText(tx.txid.c_str());
|
||||||
|
Notifications::instance().info(TR("copied_to_clipboard"));
|
||||||
}
|
}
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
if (ImGui::MenuItem(TR("view_on_explorer"))) {
|
if (ImGui::MenuItem(TR("view_on_explorer"))) {
|
||||||
@@ -916,7 +978,10 @@ void RenderTransactionsTab(App* app)
|
|||||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("transaction_id"));
|
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("transaction_id"));
|
||||||
ImGui::TextWrapped("%s", tx.txid.c_str());
|
ImGui::TextWrapped("%s", tx.txid.c_str());
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
if (ImGui::IsItemHovered()) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
if (ImGui::IsItemClicked()) ImGui::SetClipboardText(tx.txid.c_str());
|
if (ImGui::IsItemClicked()) {
|
||||||
|
ImGui::SetClipboardText(tx.txid.c_str());
|
||||||
|
Notifications::instance().info(TR("copied_to_clipboard"));
|
||||||
|
}
|
||||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||||
|
|
||||||
// Memo
|
// Memo
|
||||||
@@ -938,10 +1003,12 @@ void RenderTransactionsTab(App* app)
|
|||||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, schema::UI().drawElement("tabs.transactions", "detail-btn-rounding").size);
|
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, schema::UI().drawElement("tabs.transactions", "detail-btn-rounding").size);
|
||||||
if (TactileSmallButton(TrId("copy_txid", "detail").c_str(), S.resolveFont("button"))) {
|
if (TactileSmallButton(TrId("copy_txid", "detail").c_str(), S.resolveFont("button"))) {
|
||||||
ImGui::SetClipboardText(tx.txid.c_str());
|
ImGui::SetClipboardText(tx.txid.c_str());
|
||||||
|
Notifications::instance().info(TR("copied_to_clipboard"));
|
||||||
}
|
}
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
if (!tx.address.empty() && TactileSmallButton(TrId("copy_address", "detail").c_str(), S.resolveFont("button"))) {
|
if (!tx.address.empty() && TactileSmallButton(TrId("copy_address", "detail").c_str(), S.resolveFont("button"))) {
|
||||||
ImGui::SetClipboardText(tx.address.c_str());
|
ImGui::SetClipboardText(tx.address.c_str());
|
||||||
|
Notifications::instance().info(TR("copied_to_clipboard"));
|
||||||
}
|
}
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
if (TactileSmallButton(TrId("explorer", "detail").c_str(), S.resolveFont("button"))) {
|
if (TactileSmallButton(TrId("explorer", "detail").c_str(), S.resolveFont("button"))) {
|
||||||
|
|||||||
@@ -5,12 +5,34 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstddef>
|
||||||
|
|
||||||
namespace dragonx {
|
namespace dragonx {
|
||||||
class App;
|
class App;
|
||||||
|
|
||||||
namespace ui {
|
namespace ui {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Snapshot of what the History tab currently shows, for "export current filter".
|
||||||
|
*
|
||||||
|
* `indices` are positions into WalletState::transactions of every leg currently visible (a merged
|
||||||
|
* autoshield row contributes both its send and receive legs), sorted ascending. Published only when the
|
||||||
|
* filtered list is (re)built, so it reflects the last state the History tab rendered. `txRevision` lets a
|
||||||
|
* consumer reject a snapshot that predates the current tx list.
|
||||||
|
*/
|
||||||
|
struct TxFilterSnapshot {
|
||||||
|
bool valid = false;
|
||||||
|
bool active = false; // a type filter and/or search text is applied
|
||||||
|
std::uint64_t txRevision = 0;
|
||||||
|
std::string description; // human label of the active filter
|
||||||
|
std::vector<std::size_t> indices;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The latest History-tab filter snapshot (see TxFilterSnapshot).
|
||||||
|
const TxFilterSnapshot& GetTransactionsFilterSnapshot();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Render the Transactions tab
|
* @brief Render the Transactions tab
|
||||||
* Shows transaction history
|
* Shows transaction history
|
||||||
|
|||||||
@@ -91,7 +91,8 @@ void ValidateAddressDialog::render(App* app)
|
|||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
|
||||||
// Validate button
|
// Validate button
|
||||||
bool can_validate = strlen(s_address_input) > 0 && !s_validating && app->isConnected();
|
// Classification + checksum are LOCAL, so allow validating offline (the is-mine check needs the node).
|
||||||
|
bool can_validate = strlen(s_address_input) > 0 && !s_validating;
|
||||||
|
|
||||||
if (!can_validate) {
|
if (!can_validate) {
|
||||||
ImGui::BeginDisabled();
|
ImGui::BeginDisabled();
|
||||||
@@ -123,8 +124,9 @@ void ValidateAddressDialog::render(App* app)
|
|||||||
s_address_type.clear();
|
s_address_type.clear();
|
||||||
s_validated = true;
|
s_validated = true;
|
||||||
s_validating = false;
|
s_validating = false;
|
||||||
} else if (app->isLiteBuild()) {
|
} else if (app->isLiteBuild() || !app->isConnected()) {
|
||||||
// Lite has no node to ask: the checksum verdict is the answer, ownership is unknown (hidden).
|
// No node to ask (lite, or full-node offline): the checksum verdict is the answer; ownership
|
||||||
|
// is unknown. The result block shows why.
|
||||||
s_is_valid = true;
|
s_is_valid = true;
|
||||||
s_is_mine = false;
|
s_is_mine = false;
|
||||||
s_ownership_known = false;
|
s_ownership_known = false;
|
||||||
@@ -181,7 +183,7 @@ void ValidateAddressDialog::render(App* app)
|
|||||||
|
|
||||||
if (s_validating) {
|
if (s_validating) {
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f), "%s", TR("validating"));
|
ImGui::TextColored(material::WarningVec4(), "%s", TR("validating"));
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
@@ -192,15 +194,15 @@ void ValidateAddressDialog::render(App* app)
|
|||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
|
||||||
if (!s_error_message.empty()) {
|
if (!s_error_message.empty()) {
|
||||||
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), TR("error_format"), s_error_message.c_str());
|
ImGui::TextColored(material::ErrorVec4(), TR("error_format"), s_error_message.c_str());
|
||||||
} else {
|
} else {
|
||||||
// Valid/Invalid indicator
|
// Valid/Invalid indicator
|
||||||
ImGui::Text("%s", TR("validate_status"));
|
ImGui::Text("%s", TR("validate_status"));
|
||||||
ImGui::SameLine(lbl.position);
|
ImGui::SameLine(lbl.position);
|
||||||
if (s_is_valid) {
|
if (s_is_valid) {
|
||||||
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", TR("validate_valid"));
|
ImGui::TextColored(material::SuccessVec4(), "%s", TR("validate_valid"));
|
||||||
} else {
|
} else {
|
||||||
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "%s", TR("validate_invalid"));
|
ImGui::TextColored(material::ErrorVec4(), "%s", TR("validate_invalid"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (s_is_valid) {
|
if (s_is_valid) {
|
||||||
@@ -209,16 +211,18 @@ void ValidateAddressDialog::render(App* app)
|
|||||||
ImGui::SameLine(lbl.position);
|
ImGui::SameLine(lbl.position);
|
||||||
ImGui::Text("%s", s_address_type.c_str());
|
ImGui::Text("%s", s_address_type.c_str());
|
||||||
|
|
||||||
// Is mine? (not shown when no node was asked - lite)
|
// Is mine? When no node was asked (lite / offline) say why ownership is unknown rather
|
||||||
if (s_ownership_known) {
|
// than silently omitting it.
|
||||||
ImGui::Text("%s", TR("validate_ownership"));
|
ImGui::Text("%s", TR("validate_ownership"));
|
||||||
ImGui::SameLine(lbl.position);
|
ImGui::SameLine(lbl.position);
|
||||||
if (s_is_mine) {
|
if (!s_ownership_known) {
|
||||||
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", TR("validate_is_mine"));
|
ImGui::TextDisabled("%s", TR(app->isLiteBuild() ? "validate_ownership_unknown_lite"
|
||||||
|
: "validate_ownership_unknown_offline"));
|
||||||
|
} else if (s_is_mine) {
|
||||||
|
ImGui::TextColored(material::SuccessVec4(), "%s", TR("validate_is_mine"));
|
||||||
} else {
|
} else {
|
||||||
ImGui::TextDisabled("%s", TR("validate_not_mine"));
|
ImGui::TextDisabled("%s", TR("validate_not_mine"));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (s_is_mining) { // valid, but not for payments
|
if (s_is_mining) { // valid, but not for payments
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(material::Warning()));
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(material::Warning()));
|
||||||
@@ -227,8 +231,6 @@ void ValidateAddressDialog::render(App* app)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (!app->isConnected()) {
|
|
||||||
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), "%s", TR("not_connected"));
|
|
||||||
} else {
|
} else {
|
||||||
// Pre-interaction placeholder: keeps the results area from reading as a large
|
// Pre-interaction placeholder: keeps the results area from reading as a large
|
||||||
// dead gap between the Validate/Paste row and the Close button until a result exists.
|
// dead gap between the Validate/Paste row and the Close button until a result exists.
|
||||||
|
|||||||
@@ -39,6 +39,7 @@
|
|||||||
#include "../layout.h"
|
#include "../layout.h"
|
||||||
#include "../material/colors.h"
|
#include "../material/colors.h"
|
||||||
#include "../material/draw_helpers.h"
|
#include "../material/draw_helpers.h"
|
||||||
|
#include "../material/tooltip_style.h" // material::Tooltip (width-clamped)
|
||||||
#include "../material/type.h"
|
#include "../material/type.h"
|
||||||
#include "folder_picker.h"
|
#include "folder_picker.h"
|
||||||
|
|
||||||
@@ -557,7 +558,7 @@ public:
|
|||||||
ImGui::AlignTextToFramePadding();
|
ImGui::AlignTextToFramePadding();
|
||||||
const float textW = listW - bh2 - 2.0f * Layout::spacingSm();
|
const float textW = listW - bh2 - 2.0f * Layout::spacingSm();
|
||||||
Type().textColored(TypeStyle::Caption, OnSurface(), elideFront(folder, textW).c_str());
|
Type().textColored(TypeStyle::Caption, OnSurface(), elideFront(folder, textW).c_str());
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", folder.c_str());
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", folder.c_str()); // wrapped: long paths
|
||||||
ImGui::PopID();
|
ImGui::PopID();
|
||||||
}
|
}
|
||||||
if (!toRemove.empty()) {
|
if (!toRemove.empty()) {
|
||||||
|
|||||||
@@ -1,11 +1,32 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
namespace dragonx {
|
namespace dragonx {
|
||||||
namespace util {
|
namespace util {
|
||||||
|
|
||||||
std::string formatAmountFixed(double amount, int decimals = 8);
|
std::string formatAmountFixed(double amount, int decimals = 8);
|
||||||
|
|
||||||
|
// DRGX amounts are 8-decimal. Converting to int64 satoshis and comparing there avoids the IEEE-double
|
||||||
|
// rounding that could block a valid exact-max send (amount+fee computed in doubles landed 1 ulp over).
|
||||||
|
inline std::int64_t toSats(double coins) { return static_cast<std::int64_t>(std::llround(coins * 1e8)); }
|
||||||
|
|
||||||
|
// True when amount + fee fits within available, compared in integer satoshis (not doubles).
|
||||||
|
inline bool canAfford(double amount, double fee, double available) {
|
||||||
|
return toSats(amount) + toSats(fee) <= toSats(available);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert a coin amount to zatoshis for a send, rejecting non-finite / non-positive / rounds-to-zero /
|
||||||
|
// over-supply values. Returns false (and leaves out untouched) on any of those.
|
||||||
|
inline bool coinsToZatoshis(double coins, std::uint64_t& out) {
|
||||||
|
if (!std::isfinite(coins) || coins <= 0.0) return false;
|
||||||
|
const std::int64_t z = toSats(coins);
|
||||||
|
if (z <= 0 || z > static_cast<std::int64_t>(21000000) * 100000000LL) return false;
|
||||||
|
out = static_cast<std::uint64_t>(z);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace util
|
} // namespace util
|
||||||
} // namespace dragonx
|
} // namespace dragonx
|
||||||
|
|||||||
+7
-30
@@ -1125,23 +1125,17 @@ bool Bootstrap::verifyChecksums(const std::string& zipPath, const std::string& b
|
|||||||
std::string expectedSha = parseChecksumFile(sha256Content, 64);
|
std::string expectedSha = parseChecksumFile(sha256Content, 64);
|
||||||
const bool haveSHA256 = !expectedSha.empty();
|
const bool haveSHA256 = !expectedSha.empty();
|
||||||
|
|
||||||
std::string expectedMd5;
|
// SHA-256 is now required: the collision-prone MD5 fallback was removed. (A same-origin checksum
|
||||||
bool haveMD5 = false;
|
// only protects against transport corruption, not a malicious server — true authenticity needs a
|
||||||
if (!haveSHA256) { // MD5 only as a fallback when no SHA-256 is published
|
// detached ed25519 signature like the daemon/xmrig updaters; that is a separate release-coordinated
|
||||||
std::string md5Content = httpGetString(baseUrl + "/" + kZipName + ".md5", "[bootstrap]", opt);
|
// change, since it requires publishing per-archive .sig files and pinning a key.)
|
||||||
if (cancel_requested_) return false;
|
if (!haveSHA256) {
|
||||||
expectedMd5 = parseChecksumFile(md5Content, 32);
|
DEBUG_LOGF("[Bootstrap] No SHA-256 published - refusing to install an unverified archive\n");
|
||||||
haveMD5 = !expectedMd5.empty();
|
setProgress(State::Failed, "Verification failed: no SHA-256 checksum was published for the bootstrap. "
|
||||||
}
|
|
||||||
|
|
||||||
if (!haveSHA256 && !haveMD5) {
|
|
||||||
DEBUG_LOGF("[Bootstrap] No checksum published - refusing to install an unverified archive\n");
|
|
||||||
setProgress(State::Failed, "Verification failed: no checksum was published for the bootstrap. "
|
|
||||||
"Refusing to install an unverified archive.");
|
"Refusing to install an unverified archive.");
|
||||||
return false; // fail CLOSED; archive is KEPT
|
return false; // fail CLOSED; archive is KEPT
|
||||||
}
|
}
|
||||||
|
|
||||||
if (haveSHA256) {
|
|
||||||
setProgress(State::Verifying, "Verifying SHA-256...", 0, 1);
|
setProgress(State::Verifying, "Verifying SHA-256...", 0, 1);
|
||||||
std::string actual = computeSHA256(zipPath, 0.0f, 100.0f);
|
std::string actual = computeSHA256(zipPath, 0.0f, 100.0f);
|
||||||
if (cancel_requested_) return false;
|
if (cancel_requested_) return false;
|
||||||
@@ -1158,23 +1152,6 @@ bool Bootstrap::verifyChecksums(const std::string& zipPath, const std::string& b
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
DEBUG_LOGF("[Bootstrap] SHA-256 verified: %s\n", actual.c_str());
|
DEBUG_LOGF("[Bootstrap] SHA-256 verified: %s\n", actual.c_str());
|
||||||
} else { // haveMD5
|
|
||||||
setProgress(State::Verifying, "Verifying MD5...", 0, 1);
|
|
||||||
std::string actual = computeMD5(zipPath, 0.0f, 100.0f);
|
|
||||||
if (cancel_requested_) return false;
|
|
||||||
if (actual.empty()) { setProgress(State::Failed, "MD5 verification error"); return false; }
|
|
||||||
if (expectedMd5 != actual) {
|
|
||||||
last_verify_mismatch_ = true;
|
|
||||||
DEBUG_LOGF("[Bootstrap] MD5 MISMATCH!\n expected: %s\n actual: %s\n",
|
|
||||||
expectedMd5.c_str(), actual.c_str());
|
|
||||||
setProgress(State::Failed,
|
|
||||||
"MD5 mismatch \xe2\x80\x94 the server's checksum file may be out of date.\n"
|
|
||||||
"Expected: " + expectedMd5 + "\nGot: " + actual + "\n"
|
|
||||||
"Try again or use a fresh download.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
DEBUG_LOGF("[Bootstrap] MD5 verified: %s\n", actual.c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
setProgress(State::Verifying, "Checksums verified \xe2\x9c\x93", 100.0, 100.0);
|
setProgress(State::Verifying, "Checksums verified \xe2\x9c\x93", 100.0, 100.0);
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
// DragonX Wallet - ImGui Edition
|
||||||
|
// Copyright 2024-2026 The Hush Developers
|
||||||
|
// Released under the GPLv3
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
|
||||||
|
namespace dragonx {
|
||||||
|
namespace util {
|
||||||
|
|
||||||
|
// Make a user-typed filename safe to write into a trusted config/export directory: basename only
|
||||||
|
// (no path traversal), illegal / control chars -> '_', no leading dots or trailing dots-or-spaces,
|
||||||
|
// Windows reserved device stems prefixed with '_', length-capped, and always ending in requiredExt
|
||||||
|
// (e.g. ".csv"). Returns `fallback` if nothing usable remains. Header-only so every export dialog
|
||||||
|
// shares one implementation.
|
||||||
|
inline std::string sanitizeExportFilename(std::string name, const char* requiredExt, const char* fallback) {
|
||||||
|
const std::size_t slash = name.find_last_of("/\\");
|
||||||
|
if (slash != std::string::npos) name = name.substr(slash + 1);
|
||||||
|
auto notSpace = [](unsigned char c) { return !std::isspace(c); };
|
||||||
|
name.erase(name.begin(), std::find_if(name.begin(), name.end(), notSpace));
|
||||||
|
name.erase(std::find_if(name.rbegin(), name.rend(), notSpace).base(), name.end());
|
||||||
|
for (char& c : name) {
|
||||||
|
const unsigned char u = static_cast<unsigned char>(c);
|
||||||
|
if (u < 0x20 || c == '<' || c == '>' || c == ':' || c == '"' || c == '|' ||
|
||||||
|
c == '?' || c == '*' || c == '/' || c == '\\')
|
||||||
|
c = '_';
|
||||||
|
}
|
||||||
|
const std::size_t firstGood = name.find_first_not_of('.');
|
||||||
|
if (firstGood == std::string::npos) name.clear(); else name.erase(0, firstGood);
|
||||||
|
while (!name.empty() && (name.back() == '.' || name.back() == ' ')) name.pop_back();
|
||||||
|
if (name.size() > 120) name.resize(120);
|
||||||
|
// Windows reserved device names (case-insensitive, checked on the stem before the first dot).
|
||||||
|
{
|
||||||
|
std::string stem = name;
|
||||||
|
const std::size_t dot = stem.find('.');
|
||||||
|
if (dot != std::string::npos) stem = stem.substr(0, dot);
|
||||||
|
std::transform(stem.begin(), stem.end(), stem.begin(),
|
||||||
|
[](unsigned char c) { return static_cast<char>(std::toupper(c)); });
|
||||||
|
static const char* const kReserved[] = {
|
||||||
|
"CON", "PRN", "AUX", "NUL",
|
||||||
|
"COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
|
||||||
|
"LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
|
||||||
|
};
|
||||||
|
for (const char* r : kReserved) if (stem == r) { name = "_" + name; break; }
|
||||||
|
}
|
||||||
|
std::string lower = name;
|
||||||
|
std::transform(lower.begin(), lower.end(), lower.begin(),
|
||||||
|
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||||
|
std::string lext = requiredExt ? requiredExt : "";
|
||||||
|
std::transform(lext.begin(), lext.end(), lext.begin(),
|
||||||
|
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||||
|
if (!lext.empty() &&
|
||||||
|
(lower.size() < lext.size() || lower.compare(lower.size() - lext.size(), lext.size(), lext) != 0))
|
||||||
|
name += requiredExt;
|
||||||
|
if (name.empty() || (requiredExt && name == requiredExt)) name = fallback ? fallback : "export";
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace util
|
||||||
|
} // namespace dragonx
|
||||||
@@ -60,6 +60,20 @@ int cancelXferCb(void* clientp, curl_off_t, curl_off_t, curl_off_t, curl_off_t)
|
|||||||
return static_cast<const std::atomic<bool>*>(clientp)->load(std::memory_order_relaxed) ? 1 : 0;
|
return static_cast<const std::atomic<bool>*>(clientp)->load(std::memory_order_relaxed) ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Restrict the transfer AND any redirect to HTTPS so a redirect can't silently downgrade to http://,
|
||||||
|
// file://, ftp:// or an unexpected scheme. Every caller of these shared helpers (daemon/xmrig updaters,
|
||||||
|
// pool stats) uses https URLs. (Verify-peer/host stay on at the call sites.)
|
||||||
|
void restrictToHttps(CURL* curl)
|
||||||
|
{
|
||||||
|
#if LIBCURL_VERSION_NUM >= 0x075500 // 7.85.0: the string-form protocol setopts
|
||||||
|
curl_easy_setopt(curl, CURLOPT_PROTOCOLS_STR, "https");
|
||||||
|
curl_easy_setopt(curl, CURLOPT_REDIR_PROTOCOLS_STR, "https");
|
||||||
|
#else
|
||||||
|
curl_easy_setopt(curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTPS);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTPS);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
HttpGetResult httpGet(const std::string& url, const char* logTag, const HttpGetOptions& opt)
|
HttpGetResult httpGet(const std::string& url, const char* logTag, const HttpGetOptions& opt)
|
||||||
@@ -78,6 +92,7 @@ HttpGetResult httpGet(const std::string& url, const char* logTag, const HttpGetO
|
|||||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &sink);
|
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &sink);
|
||||||
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
|
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
|
||||||
curl_easy_setopt(curl, CURLOPT_MAXREDIRS, opt.maxRedirects);
|
curl_easy_setopt(curl, CURLOPT_MAXREDIRS, opt.maxRedirects);
|
||||||
|
restrictToHttps(curl);
|
||||||
curl_easy_setopt(curl, CURLOPT_USERAGENT, "ObsidianDragon/1.0");
|
curl_easy_setopt(curl, CURLOPT_USERAGENT, "ObsidianDragon/1.0");
|
||||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT, opt.timeoutSec);
|
curl_easy_setopt(curl, CURLOPT_TIMEOUT, opt.timeoutSec);
|
||||||
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, opt.connectTimeoutSec);
|
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, opt.connectTimeoutSec);
|
||||||
@@ -131,6 +146,8 @@ bool httpDownloadToFile(const std::string& url, const std::string& destPath,
|
|||||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeFileCb);
|
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeFileCb);
|
||||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp);
|
curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp);
|
||||||
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
|
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 5L); // bound redirects (this path set none before)
|
||||||
|
restrictToHttps(curl);
|
||||||
curl_easy_setopt(curl, CURLOPT_USERAGENT, "ObsidianDragon/1.0");
|
curl_easy_setopt(curl, CURLOPT_USERAGENT, "ObsidianDragon/1.0");
|
||||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 0L);
|
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 0L);
|
||||||
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 30L);
|
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 30L);
|
||||||
|
|||||||
+234
-5
@@ -114,6 +114,11 @@ void I18n::overlayTranslations(const nlohmann::json& translations)
|
|||||||
|
|
||||||
bool I18n::loadLanguage(const std::string& locale)
|
bool I18n::loadLanguage(const std::string& locale)
|
||||||
{
|
{
|
||||||
|
// Exclusive for the whole load: strings_ is cleared/rebuilt below, and translateCopy() readers on
|
||||||
|
// other threads (daemon monitor) must never observe it mid-rewrite. loadBuiltinEnglish() and
|
||||||
|
// overlayTranslations() are called only from here and do not lock (no recursion).
|
||||||
|
std::unique_lock<std::shared_mutex> lock(strings_mutex_);
|
||||||
|
|
||||||
// Try to load from file: CWD-relative first, then exe-relative
|
// Try to load from file: CWD-relative first, then exe-relative
|
||||||
std::string rel_path = "res/lang/" + locale + ".json";
|
std::string rel_path = "res/lang/" + locale + ".json";
|
||||||
const std::string exe_dir = Platform::getExecutableDirectory();
|
const std::string exe_dir = Platform::getExecutableDirectory();
|
||||||
@@ -232,6 +237,12 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["chat_empty_hint"] = "No conversations yet. Messages you receive will appear here.";
|
strings_["chat_empty_hint"] = "No conversations yet. Messages you receive will appear here.";
|
||||||
strings_["chat_you"] = "You";
|
strings_["chat_you"] = "You";
|
||||||
strings_["chat_contact_request"] = "contact request";
|
strings_["chat_contact_request"] = "contact request";
|
||||||
|
strings_["chat_request_pill"] = "Request";
|
||||||
|
strings_["chat_request_title"] = "New contact request";
|
||||||
|
strings_["chat_request_body"] = "This person isn't in your contacts. Accept to add them, or block to delete this conversation and ignore them.";
|
||||||
|
strings_["chat_request_accept"] = "Accept";
|
||||||
|
strings_["chat_request_block"] = "Block";
|
||||||
|
strings_["chat_request_accepted"] = "Contact added.";
|
||||||
strings_["chat_send_failed"] = "not sent";
|
strings_["chat_send_failed"] = "not sent";
|
||||||
strings_["chat_status_sending"] = "Sending… waiting for your wallet to broadcast it";
|
strings_["chat_status_sending"] = "Sending… waiting for your wallet to broadcast it";
|
||||||
strings_["chat_status_sent"] = "Sent to the network — it arrives after it confirms (about a minute)";
|
strings_["chat_status_sent"] = "Sent to the network — it arrives after it confirms (about a minute)";
|
||||||
@@ -254,6 +265,7 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["chat_toast_lite_busy"] = "A send is already in progress, or no wallet is open.";
|
strings_["chat_toast_lite_busy"] = "A send is already in progress, or no wallet is open.";
|
||||||
strings_["chat_toast_waiting_reply"] = "Waiting for the contact to reply before you can message them.";
|
strings_["chat_toast_waiting_reply"] = "Waiting for the contact to reply before you can message them.";
|
||||||
strings_["chat_toast_compose_failed"] = "Could not compose the message (too long?).";
|
strings_["chat_toast_compose_failed"] = "Could not compose the message (too long?).";
|
||||||
|
strings_["chat_toast_too_long"] = "Message too long — shorten it and send again.";
|
||||||
strings_["chat_toast_request_compose_failed"] = "Could not compose the contact request (invalid address / text?).";
|
strings_["chat_toast_request_compose_failed"] = "Could not compose the contact request (invalid address / text?).";
|
||||||
strings_["chat_toast_request_queued"] = "Contact request queued.";
|
strings_["chat_toast_request_queued"] = "Contact request queued.";
|
||||||
strings_["chat_toast_need_funds"] = "Need a small shielded balance to send chat (to cover the fee).";
|
strings_["chat_toast_need_funds"] = "Need a small shielded balance to send chat (to cover the fee).";
|
||||||
@@ -262,14 +274,30 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["chat_retry"] = "Retry";
|
strings_["chat_retry"] = "Retry";
|
||||||
strings_["chat_jump_latest"] = "Latest";
|
strings_["chat_jump_latest"] = "Latest";
|
||||||
strings_["chat_empty_title"] = "No conversations yet";
|
strings_["chat_empty_title"] = "No conversations yet";
|
||||||
|
strings_["chat_intro_title"] = "How chat works";
|
||||||
|
strings_["chat_intro_body"] = "Messages are end-to-end encrypted and sent as shielded transactions, so only you and your contact can read them. Your chat identity is tied to your wallet seed, so restoring your seed restores your chat address.";
|
||||||
|
strings_["chat_intro_fee"] = "Each message costs a small network fee. Share your chat address to let people start a conversation with you.";
|
||||||
|
strings_["chat_intro_dismiss"] = "Got it";
|
||||||
strings_["chat_empty_start"] = "Start one with \"New conversation\".";
|
strings_["chat_empty_start"] = "Start one with \"New conversation\".";
|
||||||
|
strings_["chat_copy_my_address"] = "Copy my chat address";
|
||||||
|
strings_["chat_my_address_tip"] = "Share this address so others can message you";
|
||||||
|
strings_["chat_no_my_address"] = "No chat address yet";
|
||||||
strings_["chat_search"] = "Search conversations";
|
strings_["chat_search"] = "Search conversations";
|
||||||
strings_["chat_no_matches"] = "No conversations match your search.";
|
strings_["chat_no_matches"] = "No conversations match your search.";
|
||||||
strings_["chat_export"] = "Export chat\xE2\x80\xA6";
|
strings_["chat_export"] = "Export chat\xE2\x80\xA6";
|
||||||
strings_["chat_export_warn"] = "Saves the decrypted messages as plain text. Store the file securely.";
|
strings_["chat_export_warn"] = "Saves the decrypted messages as plain text. Store the file securely.";
|
||||||
strings_["chat_export_done"] = "Conversation exported";
|
strings_["chat_export_done"] = "Conversation exported";
|
||||||
strings_["chat_export_failed"] = "Could not write the export file.";
|
strings_["chat_export_failed"] = "Could not write the export file.";
|
||||||
|
strings_["chat_export_confirm_title"] = "Export chat as plain text?";
|
||||||
|
strings_["chat_export_confirm_body"] = "This saves the decrypted messages as an UNENCRYPTED plain-text file. Anyone who can open the file can read the whole conversation. Store it securely and delete it when you no longer need it.";
|
||||||
|
strings_["chat_export_confirm_path"] = "Will be saved to:";
|
||||||
|
strings_["chat_export_confirm_btn"] = "Export";
|
||||||
strings_["chat_len_over"] = "Message too long";
|
strings_["chat_len_over"] = "Message too long";
|
||||||
|
strings_["chat_parts_label"] = "parts";
|
||||||
|
strings_["chat_txns_label"] = "txns";
|
||||||
|
strings_["chat_parts_explain"] = "This long message is sent as one transaction split into these memo parts — it arrives as quickly as a normal message.";
|
||||||
|
strings_["chat_parts_explain_multi"] = "This message is too big for one transaction, so it is split across several. The recipient sees it once every part has arrived, so it takes longer to deliver and costs a network fee per transaction.";
|
||||||
|
strings_["chat_receiving_large"] = "Receiving a large message…";
|
||||||
strings_["chat_mute"] = "Mute";
|
strings_["chat_mute"] = "Mute";
|
||||||
strings_["chat_unmute"] = "Unmute";
|
strings_["chat_unmute"] = "Unmute";
|
||||||
strings_["chat_hide"] = "Hide";
|
strings_["chat_hide"] = "Hide";
|
||||||
@@ -290,15 +318,65 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["chat_deleted_toast"] = "Conversation deleted";
|
strings_["chat_deleted_toast"] = "Conversation deleted";
|
||||||
strings_["chat_delete_failed"] = "Couldn't delete the conversation — nothing was changed.";
|
strings_["chat_delete_failed"] = "Couldn't delete the conversation — nothing was changed.";
|
||||||
strings_["chat_blocked_toast"] = "Conversation deleted & blocked";
|
strings_["chat_blocked_toast"] = "Conversation deleted & blocked";
|
||||||
|
strings_["chat_delete_everywhere"] = "Delete everywhere";
|
||||||
|
strings_["chat_delete_everywhere_note"] = "Also tells your other devices - and a future restore from your seed phrase - to hide this conversation. It sends a tiny on-chain signal (costs the usual chat fee). You can bring it back later from Restore.";
|
||||||
|
strings_["chat_delete_everywhere_toast"] = "Deleting everywhere - signal sent";
|
||||||
|
strings_["chat_delete_needs_sync"] = "Wait until the wallet is fully synced to delete everywhere.";
|
||||||
strings_["chat_blocked_manage"] = "Blocked";
|
strings_["chat_blocked_manage"] = "Blocked";
|
||||||
strings_["chat_blocked_title"] = "Blocked conversations";
|
strings_["chat_blocked_title"] = "Blocked conversations";
|
||||||
strings_["chat_blocked_desc"] = "Blocked conversations are removed and their messages are dropped — old and new — until you unblock. Unblocking re-imports the conversation from the chain.";
|
strings_["chat_blocked_desc"] = "Blocked conversations are removed and their messages are dropped — old and new — until you unblock. Unblocking re-imports the conversation from the chain.";
|
||||||
strings_["chat_unblock"] = "Unblock";
|
strings_["chat_unblock"] = "Unblock";
|
||||||
strings_["chat_unblocked_toast"] = "Conversation unblocked";
|
strings_["chat_unblocked_toast"] = "Conversation unblocked";
|
||||||
|
strings_["chat_restore_manage"] = "Restore";
|
||||||
|
strings_["chat_restore_title"] = "Restore conversations";
|
||||||
|
strings_["chat_restore_desc"] = "Conversations you deleted everywhere can be brought back here - on this device or after importing your seed phrase. Restoring re-imports the messages from the chain and tells your other devices to show it again.";
|
||||||
|
strings_["chat_restore_none"] = "No deleted conversations found yet. If you deleted one on another device, try Quick scan or paste its chat address below.";
|
||||||
|
strings_["chat_restore_btn"] = "Restore";
|
||||||
|
strings_["chat_restore_scan"] = "Quick scan";
|
||||||
|
strings_["chat_restore_scanning"] = "Scanning the chain for deleted conversations...";
|
||||||
|
strings_["chat_restore_toast"] = "Restoring conversation - it reappears after the next scan";
|
||||||
|
strings_["chat_restore_paste_label"] = "Restore by chat address";
|
||||||
|
strings_["chat_restore_paste_hint"] = "Paste the conversation's chat (z) address";
|
||||||
|
strings_["chat_sensitive_mark_tip"] = "Make sensitive";
|
||||||
|
strings_["chat_sensitive_unmark_tip"] = "Sensitive - tap to change";
|
||||||
|
strings_["chat_sensitive_mark_title"] = "Make this conversation sensitive?";
|
||||||
|
strings_["chat_sensitive_unmark_title"] = "Make this conversation normal?";
|
||||||
|
strings_["chat_sensitive_unlock_title"] = "Unlock sensitive conversations";
|
||||||
|
strings_["chat_sensitive_warn_1"] = "Its messages will be encrypted on this device under a separate passphrase - not your seed phrase. Anyone who later restores your wallet from your seed alone won't see this conversation.";
|
||||||
|
strings_["chat_sensitive_warn_lose"] = "If you lose this passphrase, these messages are gone forever - they can't be recovered from your seed.";
|
||||||
|
strings_["chat_sensitive_warn_dir"] = "Your sent messages here are protected completely. Received messages are still on the blockchain and could, in theory, be recovered by someone who has your seed and other software - DragonX itself will never show them without your passphrase.";
|
||||||
|
strings_["chat_sensitive_warn_hist"] = "Messages already in this conversation stay recoverable from the chain; this fully protects messages from here on.";
|
||||||
|
strings_["chat_sensitive_unmark_body"] = "This conversation will no longer be encrypted under your sensitive-chat passphrase. New messages will be stored like any other conversation.";
|
||||||
|
strings_["chat_sensitive_unlock_body"] = "Enter your sensitive-chat passphrase to show your sensitive conversations again.";
|
||||||
|
strings_["chat_sensitive_setup_label"] = "Create a passphrase for sensitive conversations";
|
||||||
|
strings_["chat_sensitive_unlock_label"] = "Enter your sensitive-chat passphrase";
|
||||||
|
strings_["chat_sensitive_pass_hint"] = "Passphrase";
|
||||||
|
strings_["chat_sensitive_pass2_hint"] = "Confirm passphrase";
|
||||||
|
strings_["chat_sensitive_btn_setup"] = "Set up & make sensitive";
|
||||||
|
strings_["chat_sensitive_btn_mark"] = "Make sensitive";
|
||||||
|
strings_["chat_sensitive_btn_unmark"] = "Make normal";
|
||||||
|
strings_["chat_sensitive_btn_unlock"] = "Unlock";
|
||||||
|
strings_["chat_sensitive_btn_unlock_mark"] = "Unlock & make sensitive";
|
||||||
|
strings_["chat_sensitive_btn_unlock_unmark"] = "Unlock & make normal";
|
||||||
|
strings_["chat_sensitive_lock_btn"] = "Lock sensitive";
|
||||||
|
strings_["chat_sensitive_unlock_btn"] = "Unlock sensitive";
|
||||||
|
strings_["chat_sensitive_toast_marked"] = "Conversation is now sensitive";
|
||||||
|
strings_["chat_sensitive_toast_unmarked"] = "Conversation is no longer sensitive";
|
||||||
|
strings_["chat_sensitive_toast_unlocked"] = "Sensitive conversations unlocked";
|
||||||
|
strings_["chat_sensitive_err_pass"] = "Passphrases don't match, or are empty.";
|
||||||
|
strings_["chat_sensitive_err_unlock"] = "Wrong passphrase.";
|
||||||
|
strings_["chat_sensitive_err_create"] = "Couldn't set up the sensitive-chat vault.";
|
||||||
strings_["chat_pick_contact"] = "Choose from contacts\xE2\x80\xA6";
|
strings_["chat_pick_contact"] = "Choose from contacts\xE2\x80\xA6";
|
||||||
strings_["chat_no_z_contacts"] = "No shielded-address contacts yet";
|
strings_["chat_no_z_contacts"] = "No shielded-address contacts yet";
|
||||||
|
strings_["chat_contact_needs_z"] = "Needs a shielded (z) address to chat";
|
||||||
|
strings_["contacts_message"] = "Message";
|
||||||
|
strings_["contacts_message_needs_z"] = "Chat needs a shielded (z) address — this contact only has a transparent one";
|
||||||
strings_["chat_copy_address_tip"] = "Click to copy address";
|
strings_["chat_copy_address_tip"] = "Click to copy address";
|
||||||
strings_["chat_verify_key"] = "Identity key \xE2\x80\x94 compare to verify";
|
strings_["chat_verify_key"] = "Identity key \xE2\x80\x94 compare to verify";
|
||||||
|
strings_["chat_verify_tip"] = "End-to-end encrypted to this contact's identity key. Click to view the key fingerprint and compare it with your contact over a separate channel (call or in person) to be sure no one is impersonating them.";
|
||||||
|
strings_["chat_verify_title"] = "Verify contact key";
|
||||||
|
strings_["chat_verify_body"] = "Messages in this chat are encrypted to the identity key below, which was learned from this contact's first message. Ask your contact to read you their key fingerprint over a separate channel (a call or in person). If it matches exactly, you are talking to the right person; if not, do not trust this conversation.";
|
||||||
|
strings_["chat_verify_copy"] = "Copy key";
|
||||||
strings_["chat_awaiting_key"] = "Waiting for reply";
|
strings_["chat_awaiting_key"] = "Waiting for reply";
|
||||||
strings_["chat_rename"] = "Rename contact";
|
strings_["chat_rename"] = "Rename contact";
|
||||||
strings_["chat_rename_hint"] = "Contact name";
|
strings_["chat_rename_hint"] = "Contact name";
|
||||||
@@ -458,6 +536,23 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["mig_receive_addr"] = "New wallet receive address:";
|
strings_["mig_receive_addr"] = "New wallet receive address:";
|
||||||
strings_["mig_copy_seed"] = "Copy seed";
|
strings_["mig_copy_seed"] = "Copy seed";
|
||||||
strings_["mig_backed_up"] = "I've written down my seed phrase";
|
strings_["mig_backed_up"] = "I've written down my seed phrase";
|
||||||
|
strings_["err_not_connected_daemon"] = "Not connected to the daemon.";
|
||||||
|
strings_["sweep_err_need_spending_key"] = "Enter a spending key (a viewing key can't move funds).";
|
||||||
|
strings_["sweep_status_importing"] = "Importing key & rescanning…";
|
||||||
|
strings_["sweep_err_unknown"] = "Sweep failed (unknown error)";
|
||||||
|
strings_["sweep_err_nofunds"] = "This key holds no funds to sweep.";
|
||||||
|
strings_["sweep_err_unconfirmed"] = "This key's funds are still unconfirmed — try again shortly.";
|
||||||
|
strings_["sweep_err_already"] = "This key is already in your wallet — use Send to move its funds.";
|
||||||
|
strings_["sweep_err_dust"] = "This key's balance is too small to cover the network fee.";
|
||||||
|
strings_["sweep_status_sweeping"] = "Sweeping funds to your address…";
|
||||||
|
strings_["migrate_status_checking_sweep"] = "Checking on the previous sweep…";
|
||||||
|
strings_["migrate_err_no_dest"] = "No destination address for the sweep.";
|
||||||
|
strings_["migrate_status_building"] = "Building the sweep transaction…";
|
||||||
|
strings_["migrate_status_waiting_accept"] = "Waiting for the sweep transaction to be accepted…";
|
||||||
|
strings_["migrate_status_restarting"] = "Restarting with your new wallet — this rescan can take a while…";
|
||||||
|
strings_["rescan_status_from_block"] = "Rescanning from block %d...";
|
||||||
|
strings_["mig_understood"] = "I understand that once I continue, my funds will be moved into this new wallet and can only be recovered with this seed phrase";
|
||||||
|
strings_["mig_ack_required"] = "Confirm both statements above to continue";
|
||||||
strings_["mig_step1_done"] = "Step 1 of 2 complete — your funds have NOT moved yet. Sweeping them into this new wallet is the next step.";
|
strings_["mig_step1_done"] = "Step 1 of 2 complete — your funds have NOT moved yet. Sweeping them into this new wallet is the next step.";
|
||||||
strings_["mig_continue_sweep"] = "Continue to sweep";
|
strings_["mig_continue_sweep"] = "Continue to sweep";
|
||||||
strings_["mig_discard"] = "Discard migration";
|
strings_["mig_discard"] = "Discard migration";
|
||||||
@@ -466,6 +561,8 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["mig_to"] = "To: ";
|
strings_["mig_to"] = "To: ";
|
||||||
strings_["mig_unlock_first"] = "Unlock your wallet first to spend from it.";
|
strings_["mig_unlock_first"] = "Unlock your wallet first to spend from it.";
|
||||||
strings_["mig_sweep_all"] = "Sweep all funds";
|
strings_["mig_sweep_all"] = "Sweep all funds";
|
||||||
|
strings_["mig_unlock_prompt"] = "Your wallet is locked. Enter your passphrase to unlock it, then sweep.";
|
||||||
|
strings_["mig_unlock_btn"] = "Unlock";
|
||||||
strings_["mig_later"] = "Later";
|
strings_["mig_later"] = "Later";
|
||||||
strings_["mig_sweeping"] = "Sweeping your funds into the new wallet…";
|
strings_["mig_sweeping"] = "Sweeping your funds into the new wallet…";
|
||||||
strings_["mig_confirming"] = "Waiting for the sweep to confirm on-chain before switching wallets — this guarantees your funds have actually moved.";
|
strings_["mig_confirming"] = "Waiting for the sweep to confirm on-chain before switching wallets — this guarantees your funds have actually moved.";
|
||||||
@@ -475,7 +572,13 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["mig_adopt"] = "Make this my wallet";
|
strings_["mig_adopt"] = "Make this my wallet";
|
||||||
strings_["mig_remainder"] = "Some funds are still in your old wallet (too many inputs for one transaction) — sweep the remainder before switching.";
|
strings_["mig_remainder"] = "Some funds are still in your old wallet (too many inputs for one transaction) — sweep the remainder before switching.";
|
||||||
strings_["mig_sweep_remaining"] = "Sweep remaining";
|
strings_["mig_sweep_remaining"] = "Sweep remaining";
|
||||||
|
strings_["mig_discard_confirm"] = "Discard this migration? The new seed wallet created on this device will be deleted. Make sure you have not sent anything to it. Your current wallet is not changed.";
|
||||||
|
strings_["mig_discard_yes"] = "Yes, discard";
|
||||||
|
strings_["mig_sweep_confirm"] = "Send %.8f DRGX from your current wallet to the new seed wallet in a single transaction? This moves all of your funds.";
|
||||||
|
strings_["mig_sweep_confirm_yes"] = "Yes, sweep funds";
|
||||||
strings_["mig_waiting_mine"] = "Waiting for the sweep transaction to be mined…";
|
strings_["mig_waiting_mine"] = "Waiting for the sweep transaction to be mined…";
|
||||||
|
strings_["mig_waiting_depth"] = "Sweep mined. Waiting for %d confirmations before switching wallets, so a short chain reorganisation cannot undo it…";
|
||||||
|
strings_["mig_adopt_reverify_failed"] = "Could not confirm it is safe to switch wallets yet (the node may still be syncing, the sweep is not deep enough, or funds remain). Nothing was changed - wait and try again.";
|
||||||
strings_["mig_adopting"] = "Installing your new wallet and rescanning…";
|
strings_["mig_adopting"] = "Installing your new wallet and rescanning…";
|
||||||
strings_["mig_done"] = "Migration complete. Your wallet is now backed by your seed phrase.";
|
strings_["mig_done"] = "Migration complete. Your wallet is now backed by your seed phrase.";
|
||||||
strings_["mig_done_detail"] = "The daemon is rescanning to show your funds — this can take a few minutes. Your previous wallet was saved as a .bak in the data folder.";
|
strings_["mig_done_detail"] = "The daemon is rescanning to show your funds — this can take a few minutes. Your previous wallet was saved as a .bak in the data folder.";
|
||||||
@@ -540,6 +643,7 @@ void I18n::loadBuiltinEnglish()
|
|||||||
// Wallet settings
|
// Wallet settings
|
||||||
strings_["save_z_transactions"] = "Save shielded tx history";
|
strings_["save_z_transactions"] = "Save shielded tx history";
|
||||||
strings_["auto_shield"] = "Auto-shield";
|
strings_["auto_shield"] = "Auto-shield";
|
||||||
|
strings_["auto_consolidate"] = "Auto-consolidate notes";
|
||||||
strings_["use_tor"] = "Use Tor";
|
strings_["use_tor"] = "Use Tor";
|
||||||
strings_["keep_daemon"] = "Keep daemon running";
|
strings_["keep_daemon"] = "Keep daemon running";
|
||||||
strings_["stop_external"] = "Stop external daemon";
|
strings_["stop_external"] = "Stop external daemon";
|
||||||
@@ -576,6 +680,15 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["settings_merge_to_address"] = "Merge to Address…";
|
strings_["settings_merge_to_address"] = "Merge to Address…";
|
||||||
strings_["settings_clear_ztx"] = "Clear Z-Tx History";
|
strings_["settings_clear_ztx"] = "Clear Z-Tx History";
|
||||||
strings_["settings_import_key"] = "Import Private Key…";
|
strings_["settings_import_key"] = "Import Private Key…";
|
||||||
|
strings_["settings_import_wallet_file"] = "Import From Backup File…";
|
||||||
|
strings_["import_wallet_file_title"] = "Import keys from a backup file";
|
||||||
|
strings_["import_wallet_file_note"] = "Imports every private key from an old wallet's export file (its z_exportwallet / dumpwallet output, or SilentDragon's \"Export all private keys\") and rescans once — the fastest way to recover a legacy wallet.dat this client can't open.";
|
||||||
|
strings_["import_wallet_file_path_label"] = "Path to the key dump file:";
|
||||||
|
strings_["import_wallet_file_import_btn"] = "Import and rescan";
|
||||||
|
strings_["import_wallet_file_importing"] = "Importing keys and rescanning — this can take a while";
|
||||||
|
strings_["import_wallet_file_ok"] = "Keys imported. Once the rescan finishes, shield any mined (coinbase) transparent funds from the Shield screen to make them spendable.";
|
||||||
|
strings_["import_wallet_file_need_path"] = "Enter the path to the key dump file.";
|
||||||
|
strings_["import_wallet_file_not_found"] = "Couldn't open that file — check the path is correct and the file is readable.";
|
||||||
strings_["settings_import_viewkey"] = "Import Viewing Key…";
|
strings_["settings_import_viewkey"] = "Import Viewing Key…";
|
||||||
strings_["settings_export_key"] = "Export Key…";
|
strings_["settings_export_key"] = "Export Key…";
|
||||||
strings_["settings_export_all"] = "Export All…";
|
strings_["settings_export_all"] = "Export All…";
|
||||||
@@ -682,6 +795,7 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["wiz_bootstrap_desc"] = "Download a blockchain bootstrap to dramatically speed up initial sync.\n\nYour existing wallet.dat will NOT be modified or replaced.";
|
strings_["wiz_bootstrap_desc"] = "Download a blockchain bootstrap to dramatically speed up initial sync.\n\nYour existing wallet.dat will NOT be modified or replaced.";
|
||||||
strings_["wiz_skip"] = "Skip";
|
strings_["wiz_skip"] = "Skip";
|
||||||
strings_["wiz_bootstrap_failed"] = "Bootstrap failed";
|
strings_["wiz_bootstrap_failed"] = "Bootstrap failed";
|
||||||
|
strings_["bootstrap_try_mirror"] = "Try mirror";
|
||||||
strings_["wiz_download_failed"] = "Download Failed";
|
strings_["wiz_download_failed"] = "Download Failed";
|
||||||
strings_["wiz_ext_daemon_running"] = "External daemon running";
|
strings_["wiz_ext_daemon_running"] = "External daemon running";
|
||||||
strings_["wiz_ext_daemon_warning"] = "It must be stopped before downloading a bootstrap, otherwise chain data could be corrupted.";
|
strings_["wiz_ext_daemon_warning"] = "It must be stopped before downloading a bootstrap, otherwise chain data could be corrupted.";
|
||||||
@@ -750,6 +864,8 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["wiz_pass_spaces"] = "Passphrase has leading/trailing spaces \xe2\x80\x94 remove them";
|
strings_["wiz_pass_spaces"] = "Passphrase has leading/trailing spaces \xe2\x80\x94 remove them";
|
||||||
strings_["wiz_encrypt_continue"] = "Encrypt & Continue";
|
strings_["wiz_encrypt_continue"] = "Encrypt & Continue";
|
||||||
strings_["wiz_encrypt_bg"] = "Encryption will complete in the background";
|
strings_["wiz_encrypt_bg"] = "Encryption will complete in the background";
|
||||||
|
strings_["wiz_handoff_starting"] = "Setting up your wallet. The node is starting and will keep syncing in the background. You can start exploring now, but balances may take a while to appear.";
|
||||||
|
strings_["wiz_handoff_encrypting"] = "Setting up your wallet. The node is starting, and encryption will finish in the background once it connects. Balances may take a while to appear.";
|
||||||
strings_["wiz_skip_confirm"] = "Continue WITHOUT encryption? Keys will be stored unencrypted \xe2\x80\x94 click Skip again to confirm.";
|
strings_["wiz_skip_confirm"] = "Continue WITHOUT encryption? Keys will be stored unencrypted \xe2\x80\x94 click Skip again to confirm.";
|
||||||
strings_["wiz_pin_optional"] = "Quick-Unlock PIN (optional)";
|
strings_["wiz_pin_optional"] = "Quick-Unlock PIN (optional)";
|
||||||
strings_["wiz_pin_label"] = "PIN (4-8 digits):";
|
strings_["wiz_pin_label"] = "PIN (4-8 digits):";
|
||||||
@@ -758,9 +874,22 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["wiz_pin_mismatch"] = "PINs do not match";
|
strings_["wiz_pin_mismatch"] = "PINs do not match";
|
||||||
strings_["settings_data_dir"] = "Data Dir";
|
strings_["settings_data_dir"] = "Data Dir";
|
||||||
strings_["settings_wallet_size_label"] = "Wallet Size";
|
strings_["settings_wallet_size_label"] = "Wallet Size";
|
||||||
strings_["wallet_size_warn"] = "This wallet file is large. Consolidating your notes can curb further growth.";
|
strings_["wallet_size_warn"] = "Your wallet has built up a lot of notes. Each payment you receive is stored as a separate \"note\"; as they pile up they make the wallet slower to sync and keep it growing. Consolidating merges them into fewer notes so it scans faster and stops growing \xE2\x80\x94 your balance stays exactly the same.";
|
||||||
strings_["tt_wallet_size_warn"] = "Shielded wallets grow with each note's witness data — merging many notes into one address reduces it. Back up first.";
|
strings_["wallet_size_auto_info"] = "Your wallet has built up a lot of notes \xE2\x80\x94 separate pieces of received funds that slow syncing and keep the file growing. Auto-consolidation is quietly merging them in the background to keep the wallet fast and stop it growing, so there's nothing you need to do.";
|
||||||
|
strings_["tt_wallet_size_warn"] = "Each payment arrives as a separate shielded note, and every note carries witness data that slows scans and keeps the wallet file growing. Merging many notes into one per address keeps it fast and stops the growth \xE2\x80\x94 your funds and balance are unchanged. The file reclaims its freed space only when you Compact it (Berkeley DB can't shrink in place). Back up your seed first. You can also turn on Auto-consolidate in Settings.";
|
||||||
strings_["wallet_size_consolidate"] = "Consolidate notes\xE2\x80\xA6";
|
strings_["wallet_size_consolidate"] = "Consolidate notes\xE2\x80\xA6";
|
||||||
|
strings_["wallet_size_compact"] = "Compact wallet\xE2\x80\xA6";
|
||||||
|
strings_["tt_wallet_size_compact"] = "Reclaim the disk space freed by consolidating. Berkeley DB can't shrink the file in place, so this rewrites wallet.dat into a smaller file and restarts the node with a full rescan (can take a while). A backup of the current wallet is kept.";
|
||||||
|
strings_["compact_started"] = "Compacting wallet \xE2\x80\x94 the node is restarting and will re-scan the chain.";
|
||||||
|
strings_["compact_done"] = "Wallet compacted \xE2\x80\x94 the file was rewritten and the freed space reclaimed. The node is re-scanning to rebuild your transaction view.";
|
||||||
|
strings_["confirm_compact_wallet_title"] = "Compact wallet";
|
||||||
|
strings_["confirm_compact_wallet_msg"] = "Consolidating merged your notes, but the wallet file can't shrink on its own \xE2\x80\x94 the node has to rewrite it to reclaim the freed space. This restarts the node and re-scans the blockchain, which can take a while on a large wallet.";
|
||||||
|
strings_["confirm_compact_wallet_safe"] = "Your funds and balance are safe \xE2\x80\x94 a timestamped backup of the current wallet is kept.";
|
||||||
|
strings_["compact_wallet_confirm"] = "Compact";
|
||||||
|
strings_["compact_needs_embedded"] = "Compacting rewrites wallet.dat, which the wallet can only do with its built-in node. You're connected to a dragonxd you started yourself \xE2\x80\x94 turn on \"Use embedded dragonxd\" in Settings, or restart your dragonxd with -salvagewallet to reclaim the space.";
|
||||||
|
strings_["confirm_compact_switch_msg"] = "You're connected to a dragonxd you started yourself. To compact, the wallet will stop that node, switch to its own built-in node, and rewrite wallet.dat on it \xE2\x80\x94 restarting and re-scanning the blockchain, which can take a while. Note: the built-in node is the dragonxd in the wallet's own folder, so this changes which node you run.";
|
||||||
|
strings_["compact_switching"] = "Stopping your node and switching to the built-in one\xE2\x80\xA6";
|
||||||
|
strings_["compact_switch_stop_failed"] = "Couldn't stop your external node \xE2\x80\x94 it's still running, so nothing was changed.";
|
||||||
strings_["settings_debug_changed"] = "Debug categories changed \xe2\x80\x94 restart daemon to apply";
|
strings_["settings_debug_changed"] = "Debug categories changed \xe2\x80\x94 restart daemon to apply";
|
||||||
strings_["settings_auto_detected"] = "Auto-detected from DRAGONX.conf";
|
strings_["settings_auto_detected"] = "Auto-detected from DRAGONX.conf";
|
||||||
strings_["settings_visual_effects"] = "Visual Effects";
|
strings_["settings_visual_effects"] = "Visual Effects";
|
||||||
@@ -819,6 +948,7 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["tt_clear_ztx"] = "Delete locally cached z-transaction history";
|
strings_["tt_clear_ztx"] = "Delete locally cached z-transaction history";
|
||||||
strings_["tt_save_ztx"] = "Store z-address transaction history locally for faster loading";
|
strings_["tt_save_ztx"] = "Store z-address transaction history locally for faster loading";
|
||||||
strings_["tt_auto_shield"] = "Automatically move transparent balance to shielded addresses for privacy";
|
strings_["tt_auto_shield"] = "Automatically move transparent balance to shielded addresses for privacy";
|
||||||
|
strings_["tt_auto_consolidate"] = "Quietly merge small notes in the background while the wallet is idle, in tiny batches. Slower than a manual consolidation but barely noticeable, and each address's funds stay separate. Good for mining/pool wallets that accumulate thousands of notes.";
|
||||||
strings_["tt_tor"] = "Route daemon connections through the Tor network for anonymity";
|
strings_["tt_tor"] = "Route daemon connections through the Tor network for anonymity";
|
||||||
strings_["tt_tor_unavailable"] = "Not available yet: this wallet does not route any traffic through Tor. For the node only, you can add proxy=127.0.0.1:9050 to DRAGONX.conf and restart the node. Tor must be running, or the node finds no peers. Price, update, bootstrap and pool requests still connect directly; use a system-wide VPN/Tor for those.";
|
strings_["tt_tor_unavailable"] = "Not available yet: this wallet does not route any traffic through Tor. For the node only, you can add proxy=127.0.0.1:9050 to DRAGONX.conf and restart the node. Tor must be running, or the node finds no peers. Price, update, bootstrap and pool requests still connect directly; use a system-wide VPN/Tor for those.";
|
||||||
strings_["tt_tor_unavailable_lite"] = "Not available yet: this wallet does not route any traffic through Tor. Use a system-wide VPN/Tor.";
|
strings_["tt_tor_unavailable_lite"] = "Not available yet: this wallet does not route any traffic through Tor. Use a system-wide VPN/Tor.";
|
||||||
@@ -886,6 +1016,9 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["bootstrap_trust_warning"] = "Only use bootstrap.dragonx.is or bootstrap2.dragonx.is. Using files from untrusted sources could compromise your node.";
|
strings_["bootstrap_trust_warning"] = "Only use bootstrap.dragonx.is or bootstrap2.dragonx.is. Using files from untrusted sources could compromise your node.";
|
||||||
strings_["bootstrap_mirror"] = "Mirror";
|
strings_["bootstrap_mirror"] = "Mirror";
|
||||||
strings_["bootstrap_mirror_tooltip"] = "Download from mirror (bootstrap2.dragonx.is).\nUse this if the main download is slow or failing.";
|
strings_["bootstrap_mirror_tooltip"] = "Download from mirror (bootstrap2.dragonx.is).\nUse this if the main download is slow or failing.";
|
||||||
|
strings_["bootstrap_use_mirror"] = "Use mirror";
|
||||||
|
strings_["bootstrap_use_mirror_tooltip"] = "Download the same bootstrap from the backup server (bootstrap2.dragonx.is) instead of the main one (bootstrap.dragonx.is).\nChoose this if the main download is slow or failing.";
|
||||||
|
strings_["wiz_bootstrap_size_hint"] = "This is a large download. The exact size and a time estimate appear once it starts, and it can be resumed if interrupted.";
|
||||||
strings_["bootstrap_downloading"] = "Downloading bootstrap...";
|
strings_["bootstrap_downloading"] = "Downloading bootstrap...";
|
||||||
strings_["bootstrap_verifying"] = "Verifying checksums...";
|
strings_["bootstrap_verifying"] = "Verifying checksums...";
|
||||||
strings_["bootstrap_extracting"] = "Extracting blockchain data...";
|
strings_["bootstrap_extracting"] = "Extracting blockchain data...";
|
||||||
@@ -896,6 +1029,8 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["bootstrap_success"] = "Bootstrap Complete";
|
strings_["bootstrap_success"] = "Bootstrap Complete";
|
||||||
strings_["bootstrap_success_desc"] = "Blockchain data has been extracted successfully. Start the daemon to begin syncing from the bootstrap point.";
|
strings_["bootstrap_success_desc"] = "Blockchain data has been extracted successfully. Start the daemon to begin syncing from the bootstrap point.";
|
||||||
strings_["bootstrap_restart_daemon"] = "Restart Daemon";
|
strings_["bootstrap_restart_daemon"] = "Restart Daemon";
|
||||||
|
strings_["bootstrap_start_daemon"] = "Start Daemon";
|
||||||
|
strings_["bootstrap_success_desc_start"] = "Blockchain data was extracted successfully. Click Start Daemon to begin syncing from the bootstrap point.";
|
||||||
strings_["bootstrap_failed"] = "Bootstrap Failed";
|
strings_["bootstrap_failed"] = "Bootstrap Failed";
|
||||||
strings_["tt_open_dir"] = "Click to open in file explorer";
|
strings_["tt_open_dir"] = "Click to open in file explorer";
|
||||||
strings_["settings_open_data_dir"] = "Open data folder";
|
strings_["settings_open_data_dir"] = "Open data folder";
|
||||||
@@ -1030,10 +1165,14 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["tt_reset_settings"] = "Revert this page to the last saved settings (undo unsaved changes)";
|
strings_["tt_reset_settings"] = "Revert this page to the last saved settings (undo unsaved changes)";
|
||||||
strings_["tt_debug_collapse"] = "Collapse debug logging options";
|
strings_["tt_debug_collapse"] = "Collapse debug logging options";
|
||||||
strings_["tt_debug_expand"] = "Expand debug logging options";
|
strings_["tt_debug_expand"] = "Expand debug logging options";
|
||||||
|
strings_["debug_options_caption_gated"] = "Diagnostics and node tuning for troubleshooting. Locked: opening requires confirmation (and your PIN or passphrase if set).";
|
||||||
|
strings_["debug_options_caption_unlocked"] = "Diagnostics and node tuning for troubleshooting. Unlocked for this session.";
|
||||||
|
strings_["tt_debug_expand_gated"] = "Locked: confirm (and enter your PIN or passphrase if set) to show debug options";
|
||||||
strings_["tt_restart_daemon"] = "Restart the daemon to apply debug logging changes";
|
strings_["tt_restart_daemon"] = "Restart the daemon to apply debug logging changes";
|
||||||
|
|
||||||
// Settings: additional tooltips (keys/data row)
|
// Settings: additional tooltips (keys/data row)
|
||||||
strings_["tt_import_key"] = "Import a private key (zkey or tkey) into this wallet";
|
strings_["tt_import_key"] = "Import a private key (zkey or tkey) into this wallet";
|
||||||
|
strings_["tt_import_wallet_file"] = "Import all keys from an old wallet's exported dump file (one rescan)";
|
||||||
strings_["tt_import_viewkey"] = "Import a shielded viewing key to watch an address (read-only)";
|
strings_["tt_import_viewkey"] = "Import a shielded viewing key to watch an address (read-only)";
|
||||||
strings_["tt_export_key"] = "Export the private key for the selected address";
|
strings_["tt_export_key"] = "Export the private key for the selected address";
|
||||||
strings_["tt_export_all"] = "Export all private keys to a file";
|
strings_["tt_export_all"] = "Export all private keys to a file";
|
||||||
@@ -1112,6 +1251,8 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["confirm_transfer"] = "Confirm Transfer";
|
strings_["confirm_transfer"] = "Confirm Transfer";
|
||||||
strings_["transfer_sent"] = "Transfer Sent";
|
strings_["transfer_sent"] = "Transfer Sent";
|
||||||
strings_["transfer_sent_desc"] = "Your transfer has been submitted to the network.";
|
strings_["transfer_sent_desc"] = "Your transfer has been submitted to the network.";
|
||||||
|
strings_["transfer_txid_label"] = "Transaction ID: %s";
|
||||||
|
strings_["transfer_tooltip_locked"] = "Wallet is locked — unlock it to transfer";
|
||||||
strings_["transfer_failed"] = "Transfer Failed";
|
strings_["transfer_failed"] = "Transfer Failed";
|
||||||
|
|
||||||
// Misc dialog strings
|
// Misc dialog strings
|
||||||
@@ -1131,6 +1272,8 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["recent_sends"] = "RECENT SENDS";
|
strings_["recent_sends"] = "RECENT SENDS";
|
||||||
strings_["recipient"] = "RECIPIENT";
|
strings_["recipient"] = "RECIPIENT";
|
||||||
strings_["memo_optional"] = "MEMO (OPTIONAL)";
|
strings_["memo_optional"] = "MEMO (OPTIONAL)";
|
||||||
|
strings_["send_memo_requires_shielded"] = "Memos require a shielded (z) recipient address.";
|
||||||
|
strings_["send_memo_will_not_be_sent"] = "This memo will not be sent: memos only work with shielded (z) recipients.";
|
||||||
strings_["no_recent_sends"] = "No recent sends";
|
strings_["no_recent_sends"] = "No recent sends";
|
||||||
strings_["network_fee"] = "NETWORK FEE";
|
strings_["network_fee"] = "NETWORK FEE";
|
||||||
strings_["amount_details"] = "AMOUNT DETAILS";
|
strings_["amount_details"] = "AMOUNT DETAILS";
|
||||||
@@ -1217,6 +1360,7 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["shielded_self_type"] = "Shielded";
|
strings_["shielded_self_type"] = "Shielded";
|
||||||
strings_["immature_type"] = "Immature";
|
strings_["immature_type"] = "Immature";
|
||||||
strings_["mined_type"] = "Mined";
|
strings_["mined_type"] = "Mined";
|
||||||
|
strings_["tx_type_generic"] = "Transaction";
|
||||||
strings_["mature"] = "Mature";
|
strings_["mature"] = "Mature";
|
||||||
strings_["copy_txid"] = "Copy TxID";
|
strings_["copy_txid"] = "Copy TxID";
|
||||||
strings_["view_details"] = "View Details";
|
strings_["view_details"] = "View Details";
|
||||||
@@ -1291,6 +1435,7 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["lite_unlock_btn"] = "Unlock";
|
strings_["lite_unlock_btn"] = "Unlock";
|
||||||
strings_["lite_unlock_ok"] = "Wallet unlocked — you can send now";
|
strings_["lite_unlock_ok"] = "Wallet unlocked — you can send now";
|
||||||
strings_["lite_unlock_failed"] = "Unlock failed — wrong passphrase?";
|
strings_["lite_unlock_failed"] = "Unlock failed — wrong passphrase?";
|
||||||
|
strings_["lite_encrypt_failed_unencrypted"] = "Wallet encryption failed — your wallet may still be UNENCRYPTED. Open Settings > Security and set the passphrase again.";
|
||||||
|
|
||||||
// Send Tab
|
// Send Tab
|
||||||
strings_["pay_from"] = "Pay From";
|
strings_["pay_from"] = "Pay From";
|
||||||
@@ -1442,6 +1587,10 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["confirmations"] = "Confirmations";
|
strings_["confirmations"] = "Confirmations";
|
||||||
strings_["confirmed"] = "Confirmed";
|
strings_["confirmed"] = "Confirmed";
|
||||||
strings_["pending"] = "Pending";
|
strings_["pending"] = "Pending";
|
||||||
|
strings_["tx_dropped"] = "Dropped";
|
||||||
|
strings_["tx_stale_pending"] = "Expired?";
|
||||||
|
strings_["baltab_pending_chip"] = "Pending +%.4f";
|
||||||
|
strings_["baltab_pending_tip"] = "Awaiting confirmations — not spendable yet; it moves to your available balance once confirmed.";
|
||||||
strings_["sent"] = "sent";
|
strings_["sent"] = "sent";
|
||||||
strings_["received"] = "received";
|
strings_["received"] = "received";
|
||||||
strings_["mined"] = "mined";
|
strings_["mined"] = "mined";
|
||||||
@@ -1532,6 +1681,7 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["sb_mining_hs"] = "%.1f H/s";
|
strings_["sb_mining_hs"] = "%.1f H/s";
|
||||||
strings_["sb_syncing_eta"] = "Syncing %.1f%% (%d left, %.0f blk/s, ~%s)";
|
strings_["sb_syncing_eta"] = "Syncing %.1f%% (%d left, %.0f blk/s, ~%s)";
|
||||||
strings_["sb_syncing_basic"] = "Syncing %.1f%% (%d left)";
|
strings_["sb_syncing_basic"] = "Syncing %.1f%% (%d left)";
|
||||||
|
strings_["sb_lite_sync_error"] = "Sync error: %.80s";
|
||||||
strings_["sb_rescanning_pct"] = "Rescanning %.0f%%";
|
strings_["sb_rescanning_pct"] = "Rescanning %.0f%%";
|
||||||
strings_["sb_rescanning"] = "Rescanning";
|
strings_["sb_rescanning"] = "Rescanning";
|
||||||
strings_["sb_building_witnesses_pct"] = "Rebuilding witnesses %.0f%%";
|
strings_["sb_building_witnesses_pct"] = "Rebuilding witnesses %.0f%%";
|
||||||
@@ -1544,7 +1694,7 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["sb_no_conf"] = "No DRAGONX.conf found";
|
strings_["sb_no_conf"] = "No DRAGONX.conf found";
|
||||||
strings_["sb_starting_daemon"] = "Starting dragonxd...";
|
strings_["sb_starting_daemon"] = "Starting dragonxd...";
|
||||||
strings_["sb_connecting_daemon"] = "Connecting to dragonxd...";
|
strings_["sb_connecting_daemon"] = "Connecting to dragonxd...";
|
||||||
strings_["sb_auth_failed"] = "Auth failed — check rpcuser/rpcpassword";
|
strings_["sb_auth_failed"] = "Couldn't sign in to the node";
|
||||||
strings_["sb_waiting_daemon"] = "Waiting for dragonxd to start...";
|
strings_["sb_waiting_daemon"] = "Waiting for dragonxd to start...";
|
||||||
strings_["sb_waiting_daemon_err"] = "Waiting for dragonxd — %s";
|
strings_["sb_waiting_daemon_err"] = "Waiting for dragonxd — %s";
|
||||||
strings_["sb_connecting_external"] = "Connecting to external daemon...";
|
strings_["sb_connecting_external"] = "Connecting to external daemon...";
|
||||||
@@ -1562,6 +1712,12 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["node_banner_lite_open_failed"] = "Couldn't open your wallet";
|
strings_["node_banner_lite_open_failed"] = "Couldn't open your wallet";
|
||||||
strings_["node_banner_reconnect"] = "Reconnect";
|
strings_["node_banner_reconnect"] = "Reconnect";
|
||||||
strings_["node_banner_restart"] = "Restart node";
|
strings_["node_banner_restart"] = "Restart node";
|
||||||
|
strings_["node_banner_open_config"] = "Open config folder";
|
||||||
|
strings_["node_banner_auth_title"] = "Wallet can't sign in to the node";
|
||||||
|
strings_["node_banner_auth_detail"] = "The node rejected the wallet's saved login. Restart the node, or open the config folder to fix the login in DRAGONX.conf.";
|
||||||
|
strings_["node_banner_legacy_title"] = "Your transparent funds can't be moved yet";
|
||||||
|
strings_["node_banner_legacy_detail"] = "This wallet has no usable seed, so it can't create a shielded address to move its transparent funds. Recover them into a fresh seed-backed wallet.";
|
||||||
|
strings_["node_banner_recover"] = "Recover funds";
|
||||||
// Refresh-staleness badge (W6-2) on the Total Balance card.
|
// Refresh-staleness badge (W6-2) on the Total Balance card.
|
||||||
strings_["data_stale_prefix"] = "Updated";
|
strings_["data_stale_prefix"] = "Updated";
|
||||||
strings_["data_stale_tooltip"] =
|
strings_["data_stale_tooltip"] =
|
||||||
@@ -1938,15 +2094,24 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["toast_finish_send_first"] = "Finish or cancel the pending send before switching wallets.";
|
strings_["toast_finish_send_first"] = "Finish or cancel the pending send before switching wallets.";
|
||||||
strings_["toast_rescan_complete"] = "Blockchain rescan complete.";
|
strings_["toast_rescan_complete"] = "Blockchain rescan complete.";
|
||||||
strings_["toast_rescan_from_fmt"] = "Rescanning the blockchain from block %d — this can take a while.";
|
strings_["toast_rescan_from_fmt"] = "Rescanning the blockchain from block %d — this can take a while.";
|
||||||
|
strings_["warmup_block_fmt"] = "(Block %d)";
|
||||||
strings_["toast_solo_unavailable_lite"] = "Solo mining isn't available in the lite wallet.";
|
strings_["toast_solo_unavailable_lite"] = "Solo mining isn't available in the lite wallet.";
|
||||||
strings_["toast_pool_unavailable"] = "Pool mining isn't available in this build.";
|
strings_["toast_pool_unavailable"] = "Pool mining isn't available in this build.";
|
||||||
strings_["toast_no_address_generate_z"] = "No wallet address yet — generate a shielded (Z) address in the Receive tab.";
|
strings_["toast_no_address_generate_z"] = "No wallet address yet — generate a shielded (Z) address in the Receive tab.";
|
||||||
strings_["toast_pool_connecting"] = "Starting pool miner — connecting to the pool…";
|
strings_["toast_pool_connecting"] = "Starting pool miner — connecting to the pool…";
|
||||||
strings_["toast_not_connected_daemon"] = "Not connected to the node.";
|
strings_["toast_not_connected_daemon"] = "Not connected to the node.";
|
||||||
strings_["toast_mining_failed"] = "Couldn't start mining: ";
|
strings_["toast_mining_failed"] = "Couldn't start mining: ";
|
||||||
|
strings_["toast_pool_payout_invalid"] = "Pool payout address is not a valid DragonX address — mining not started.";
|
||||||
|
strings_["toast_av_blocked_xmrig"] = "Windows Defender blocked the miner. Add an exclusion for %APPDATA%\\ObsidianDragon.";
|
||||||
|
strings_["mining_error_retry_hint"] = "Press Mine to try again.";
|
||||||
|
strings_["mining_error_av_help"] = "How to fix";
|
||||||
|
strings_["mining_error_dismiss"] = "Dismiss";
|
||||||
strings_["toast_stop_mining_failed"] = "Couldn't stop mining: ";
|
strings_["toast_stop_mining_failed"] = "Couldn't stop mining: ";
|
||||||
strings_["toast_pool_start_failed"] = "Couldn't start the pool miner: ";
|
strings_["toast_pool_start_failed"] = "Couldn't start the pool miner: ";
|
||||||
strings_["toast_ban_failed"] = "Couldn't ban the peer: ";
|
strings_["toast_ban_failed"] = "Couldn't ban the peer: ";
|
||||||
|
strings_["toast_peer_banned"] = "Peer banned.";
|
||||||
|
strings_["toast_peer_unbanned"] = "Peer unbanned.";
|
||||||
|
strings_["toast_bans_cleared"] = "All bans cleared.";
|
||||||
strings_["toast_unban_failed"] = "Couldn't unban the peer: ";
|
strings_["toast_unban_failed"] = "Couldn't unban the peer: ";
|
||||||
strings_["toast_clear_bans_failed"] = "Couldn't clear bans: ";
|
strings_["toast_clear_bans_failed"] = "Couldn't clear bans: ";
|
||||||
strings_["toast_key_export_failed"] = "Key export failed: ";
|
strings_["toast_key_export_failed"] = "Key export failed: ";
|
||||||
@@ -1956,6 +2121,11 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["console_clear"] = "Clear";
|
strings_["console_clear"] = "Clear";
|
||||||
strings_["console_clear_console"] = "Clear Console";
|
strings_["console_clear_console"] = "Clear Console";
|
||||||
strings_["console_cleared"] = "Console cleared";
|
strings_["console_cleared"] = "Console cleared";
|
||||||
|
strings_["console_secret_result_hidden"] = "[result hidden — contains private key material. Use \"Copy secret\" to copy it, or it clears automatically.]";
|
||||||
|
strings_["console_secret_result_expired"] = "[hidden secret result cleared]";
|
||||||
|
strings_["console_copy_secret"] = "Copy secret";
|
||||||
|
strings_["console_copy_secret_tip"] = "Copy the hidden key/seed result to the clipboard (auto-clears after ~45s).";
|
||||||
|
strings_["console_hide_secret"] = "Hide";
|
||||||
strings_["console_click_commands"] = "Click commands above to insert them";
|
strings_["console_click_commands"] = "Click commands above to insert them";
|
||||||
strings_["console_click_insert"] = "Click to insert";
|
strings_["console_click_insert"] = "Click to insert";
|
||||||
strings_["console_click_insert_params"] = "Click to insert with parameters";
|
strings_["console_click_insert_params"] = "Click to insert with parameters";
|
||||||
@@ -2070,6 +2240,17 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["export_tx_file_fail"] = "Failed to create CSV file";
|
strings_["export_tx_file_fail"] = "Failed to create CSV file";
|
||||||
strings_["export_tx_none"] = "No transactions to export";
|
strings_["export_tx_none"] = "No transactions to export";
|
||||||
strings_["export_tx_success"] = "Transactions exported successfully";
|
strings_["export_tx_success"] = "Transactions exported successfully";
|
||||||
|
strings_["export_tx_open_folder"] = "Open folder";
|
||||||
|
strings_["export_tx_scope"] = "Export scope";
|
||||||
|
strings_["export_tx_scope_all"] = "All transactions";
|
||||||
|
strings_["export_tx_scope_filter"] = "Current History filter";
|
||||||
|
strings_["export_tx_scope_range"] = "Date range";
|
||||||
|
strings_["export_tx_scope_filter_none"] = "No filter is active on the History tab";
|
||||||
|
strings_["export_tx_scope_stale"] = "The History filter changed while loading. Close and reopen this dialog.";
|
||||||
|
strings_["export_tx_range_from"] = "From (YYYY-MM-DD)";
|
||||||
|
strings_["export_tx_range_to"] = "To (YYYY-MM-DD)";
|
||||||
|
strings_["export_tx_range_invalid"] = "Enter valid dates as YYYY-MM-DD, with From on or before To";
|
||||||
|
strings_["export_tx_range_pending_note"] = "Pending transactions have no date and are not included in a date range.";
|
||||||
strings_["export_tx_title"] = "Export Transactions to CSV";
|
strings_["export_tx_title"] = "Export Transactions to CSV";
|
||||||
|
|
||||||
// --- Import Key Dialog ---
|
// --- Import Key Dialog ---
|
||||||
@@ -2121,6 +2302,8 @@ void I18n::loadBuiltinEnglish()
|
|||||||
|
|
||||||
// --- Key Export Dialog ---
|
// --- Key Export Dialog ---
|
||||||
strings_["key_export_fetching"] = "Fetching key from wallet...";
|
strings_["key_export_fetching"] = "Fetching key from wallet...";
|
||||||
|
strings_["key_export_fetching_base"] = "Fetching key from wallet";
|
||||||
|
strings_["key_export_unlock_first"] = "Unlock your wallet first, then reveal the key.";
|
||||||
strings_["key_export_failed"] = "Couldn't export the key \xE2\x80\x94 unlock the wallet (if encrypted) and try again.";
|
strings_["key_export_failed"] = "Couldn't export the key \xE2\x80\x94 unlock the wallet (if encrypted) and try again.";
|
||||||
strings_["key_export_private_key"] = "Private Key:";
|
strings_["key_export_private_key"] = "Private Key:";
|
||||||
strings_["key_export_private_warning"] = "Keep this key SECRET! Anyone with this key can spend your funds. Never share it online or with untrusted parties.";
|
strings_["key_export_private_warning"] = "Keep this key SECRET! Anyone with this key can spend your funds. Never share it online or with untrusted parties.";
|
||||||
@@ -2222,6 +2405,10 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["market_24h_change"] = "24h";
|
strings_["market_24h_change"] = "24h";
|
||||||
strings_["market_price_loading"] = "Loading price data...";
|
strings_["market_price_loading"] = "Loading price data...";
|
||||||
strings_["market_price_unavailable"] = "Price data unavailable";
|
strings_["market_price_unavailable"] = "Price data unavailable";
|
||||||
|
strings_["market_price_fetch_failed"] = "Price fetch failed";
|
||||||
|
strings_["market_price_stale_min"] = "as of %d min ago";
|
||||||
|
strings_["market_price_stale_hour"] = "as of %dh ago";
|
||||||
|
strings_["market_price_stale_tip"] = "Showing the last known price. The latest refresh failed: %s";
|
||||||
strings_["market_refresh_price"] = "Refresh price data";
|
strings_["market_refresh_price"] = "Refresh price data";
|
||||||
strings_["market_trade_on"] = "Trade on %s";
|
strings_["market_trade_on"] = "Trade on %s";
|
||||||
|
|
||||||
@@ -2241,6 +2428,11 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["mining_click_copy_difficulty"] = "Click to copy difficulty";
|
strings_["mining_click_copy_difficulty"] = "Click to copy difficulty";
|
||||||
strings_["mining_connected"] = "Connected";
|
strings_["mining_connected"] = "Connected";
|
||||||
strings_["mining_connecting"] = "Connecting...";
|
strings_["mining_connecting"] = "Connecting...";
|
||||||
|
strings_["mining_status_label"] = "Status";
|
||||||
|
strings_["mining_status_connected"] = "Connected";
|
||||||
|
strings_["mining_status_connecting"] = "Connecting…";
|
||||||
|
strings_["mining_status_stopped"] = "Stopped";
|
||||||
|
strings_["mining_pool_unreachable"] = "Can't reach pool — check the URL";
|
||||||
strings_["mining_difficulty_copied"] = "Difficulty copied";
|
strings_["mining_difficulty_copied"] = "Difficulty copied";
|
||||||
strings_["mining_est_block"] = "Est. Block";
|
strings_["mining_est_block"] = "Est. Block";
|
||||||
strings_["mining_est_daily"] = "Est. Daily";
|
strings_["mining_est_daily"] = "Est. Daily";
|
||||||
@@ -2276,6 +2468,8 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["send_mining_addr_short"] = "Mining address — can't be paid";
|
strings_["send_mining_addr_short"] = "Mining address — can't be paid";
|
||||||
strings_["send_mining_addr_blocked"] = "This is a mining (transparent) address. On DragonX these can only receive mining rewards — the network rejects payments to them. Ask the recipient for a shielded address (it starts with zs).";
|
strings_["send_mining_addr_blocked"] = "This is a mining (transparent) address. On DragonX these can only receive mining rewards — the network rejects payments to them. Ask the recipient for a shielded address (it starts with zs).";
|
||||||
strings_["mining_payout_needs_shielded"] = "Pools pay out with ordinary transactions, which can't reach a mining (transparent, R…) address on DragonX. Use a shielded address (it starts with zs) as your payout address.";
|
strings_["mining_payout_needs_shielded"] = "Pools pay out with ordinary transactions, which can't reach a mining (transparent, R…) address on DragonX. Use a shielded address (it starts with zs) as your payout address.";
|
||||||
|
strings_["mining_payout_invalid_short"] = "Not a valid DragonX address";
|
||||||
|
strings_["mining_payout_needs_shielded_short"] = "Use a shielded (zs…) address, not a mining address";
|
||||||
strings_["new_mining_address"] = "New mining address";
|
strings_["new_mining_address"] = "New mining address";
|
||||||
strings_["tt_new_mining_address"] = "Creates a transparent (R…) address for solo-mining rewards. It can't receive payments — share a shielded address for those.";
|
strings_["tt_new_mining_address"] = "Creates a transparent (R…) address for solo-mining rewards. It can't receive payments — share a shielded address for those.";
|
||||||
strings_["mining_address"] = "Mining address";
|
strings_["mining_address"] = "Mining address";
|
||||||
@@ -2287,10 +2481,13 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["balance_mining_fmt"] = "Mining rewards: %.8f";
|
strings_["balance_mining_fmt"] = "Mining rewards: %.8f";
|
||||||
strings_["mining_card_tooltip"] = "Solo-mining rewards arrive at your mining (transparent, R…) addresses. Those addresses can't receive payments. Rewards are shielded automatically once they mature.";
|
strings_["mining_card_tooltip"] = "Solo-mining rewards arrive at your mining (transparent, R…) addresses. Those addresses can't receive payments. Rewards are shielded automatically once they mature.";
|
||||||
strings_["mining_card_click_to_shield"] = "Click to shield matured rewards now.";
|
strings_["mining_card_click_to_shield"] = "Click to shield matured rewards now.";
|
||||||
|
strings_["shielded_card_tooltip"] = "Shielded (private) balance: funds held in z-addresses. Amounts, senders and recipients are hidden on the blockchain. Click to open Receive.";
|
||||||
strings_["contact_chip_mining"] = "Mining only";
|
strings_["contact_chip_mining"] = "Mining only";
|
||||||
strings_["failed_create_mining_address"] = "Failed to create mining address";
|
strings_["failed_create_mining_address"] = "Failed to create mining address";
|
||||||
strings_["new_mining_address_created"] = "New mining address created";
|
strings_["new_mining_address_created"] = "New mining address created";
|
||||||
strings_["mining_addr_explainer"] = "Receives solo-mining rewards only — the network rejects payments to it. Rewards are shielded automatically once they mature.";
|
strings_["mining_addr_explainer"] = "Receives solo-mining rewards only — the network rejects payments to it. Rewards are shielded automatically once they mature.";
|
||||||
|
strings_["mining_addr_none"] = "None yet";
|
||||||
|
strings_["mining_addr_none_tip"] = "No mining address yet. Create one in the Receive tab (New mining address). Solo-mining rewards are paid to a transparent (R...) mining address, then shielded automatically once they mature.";
|
||||||
strings_["receive_use_shielded"] = "Use a shielded address";
|
strings_["receive_use_shielded"] = "Use a shielded address";
|
||||||
strings_["receive_not_for_payments"] = "Not for payments";
|
strings_["receive_not_for_payments"] = "Not for payments";
|
||||||
strings_["validate_mining_type"] = "Mining address (transparent)";
|
strings_["validate_mining_type"] = "Mining address (transparent)";
|
||||||
@@ -2344,9 +2541,13 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["mining_stopping"] = "Stopping...";
|
strings_["mining_stopping"] = "Stopping...";
|
||||||
strings_["mining_stopping_tooltip"] = "Miner is stopping...";
|
strings_["mining_stopping_tooltip"] = "Miner is stopping...";
|
||||||
strings_["mining_syncing_tooltip"] = "Blockchain is syncing...";
|
strings_["mining_syncing_tooltip"] = "Blockchain is syncing...";
|
||||||
|
strings_["mining_not_connected_tooltip"] = "Not connected to the node. Wait for the daemon to connect before mining.";
|
||||||
strings_["mining_to_save"] = "to save";
|
strings_["mining_to_save"] = "to save";
|
||||||
strings_["mining_today"] = "Today";
|
strings_["mining_today"] = "Today";
|
||||||
strings_["mining_uptime"] = "Uptime";
|
strings_["mining_uptime"] = "Uptime";
|
||||||
|
strings_["mining_txn_count"] = "(%d txn)";
|
||||||
|
strings_["mining_uptime_ms"] = "%lldm %llds";
|
||||||
|
strings_["mining_uptime_hm"] = "%lldh %lldm";
|
||||||
strings_["mining_yesterday"] = "Yesterday";
|
strings_["mining_yesterday"] = "Yesterday";
|
||||||
|
|
||||||
// --- Miner (xmrig) updater ---
|
// --- Miner (xmrig) updater ---
|
||||||
@@ -2420,6 +2621,10 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["daemon_update_installed_ok"] = "Node installed";
|
strings_["daemon_update_installed_ok"] = "Node installed";
|
||||||
strings_["daemon_update_restart_note"] = "Restart the daemon to start running the new version.";
|
strings_["daemon_update_restart_note"] = "Restart the daemon to start running the new version.";
|
||||||
strings_["daemon_update_restart_now"] = "Restart daemon now";
|
strings_["daemon_update_restart_now"] = "Restart daemon now";
|
||||||
|
strings_["daemon_update_restart_needed"] = "Restart required to use the new version";
|
||||||
|
strings_["daemon_update_later_hint"] = "A reminder stays in Settings until you restart the daemon.";
|
||||||
|
strings_["daemon_restart_pending"] = "A new daemon version is installed but not running yet. Restart the daemon to use it.";
|
||||||
|
strings_["tt_daemon_restart_pending"] = "Restarts the embedded daemon so the newly installed version takes effect.";
|
||||||
strings_["daemon_update_later"] = "Later";
|
strings_["daemon_update_later"] = "Later";
|
||||||
strings_["daemon_update_failed"] = "Update failed";
|
strings_["daemon_update_failed"] = "Update failed";
|
||||||
strings_["daemon_update_unknown_error"] = "Unknown error.";
|
strings_["daemon_update_unknown_error"] = "Unknown error.";
|
||||||
@@ -2476,6 +2681,8 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["peers_blocks"] = "Blocks";
|
strings_["peers_blocks"] = "Blocks";
|
||||||
strings_["peers_blocks_left"] = "%d blocks left";
|
strings_["peers_blocks_left"] = "%d blocks left";
|
||||||
strings_["peers_clear_all_bans"] = "Clear All Bans";
|
strings_["peers_clear_all_bans"] = "Clear All Bans";
|
||||||
|
strings_["peers_clear_bans_confirm"] = "Clear all %d bans?";
|
||||||
|
strings_["peers_clear_bans_confirm_yes"] = "Clear all";
|
||||||
strings_["peers_click_copy"] = "Click to copy";
|
strings_["peers_click_copy"] = "Click to copy";
|
||||||
strings_["peers_connected"] = "Connected";
|
strings_["peers_connected"] = "Connected";
|
||||||
strings_["peers_connected_count"] = "Connected: %d";
|
strings_["peers_connected_count"] = "Connected: %d";
|
||||||
@@ -2532,6 +2739,8 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["request_payment_uri"] = "Payment URI:";
|
strings_["request_payment_uri"] = "Payment URI:";
|
||||||
strings_["request_receive_address"] = "Receive Address:";
|
strings_["request_receive_address"] = "Receive Address:";
|
||||||
strings_["request_select_address"] = "Select address...";
|
strings_["request_select_address"] = "Select address...";
|
||||||
|
strings_["request_no_shielded"] = "You don't have a shielded address yet. Create one to receive payments.";
|
||||||
|
strings_["request_create_shielded"] = "Create a shielded address";
|
||||||
strings_["request_shielded_addrs"] = "-- Shielded Addresses --";
|
strings_["request_shielded_addrs"] = "-- Shielded Addresses --";
|
||||||
strings_["request_title"] = "Request Payment";
|
strings_["request_title"] = "Request Payment";
|
||||||
strings_["request_uri_copied"] = "Payment URI copied to clipboard";
|
strings_["request_uri_copied"] = "Payment URI copied to clipboard";
|
||||||
@@ -2541,6 +2750,8 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["send_amount_details"] = "AMOUNT DETAILS";
|
strings_["send_amount_details"] = "AMOUNT DETAILS";
|
||||||
strings_["send_amount_upper"] = "AMOUNT";
|
strings_["send_amount_upper"] = "AMOUNT";
|
||||||
strings_["send_clear_fields"] = "Clear all form fields?";
|
strings_["send_clear_fields"] = "Clear all form fields?";
|
||||||
|
strings_["send_clear_form"] = "Clear form";
|
||||||
|
strings_["send_clear_form_tip"] = "Clear the recipient, amount and memo. You can undo for a few seconds.";
|
||||||
strings_["send_copy_error"] = "Copy Error";
|
strings_["send_copy_error"] = "Copy Error";
|
||||||
strings_["send_dismiss"] = "Dismiss";
|
strings_["send_dismiss"] = "Dismiss";
|
||||||
strings_["send_error_copied"] = "Error copied to clipboard";
|
strings_["send_error_copied"] = "Error copied to clipboard";
|
||||||
@@ -2557,6 +2768,7 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["send_network_fee_label"] = "Network fee:";
|
strings_["send_network_fee_label"] = "Network fee:";
|
||||||
strings_["send_total_label"] = "Total:";
|
strings_["send_total_label"] = "Total:";
|
||||||
strings_["send_no_balance"] = "No balance";
|
strings_["send_no_balance"] = "No balance";
|
||||||
|
strings_["send_transparent_source_warn"] = "Sending from a transparent address is not private: the sender, amount and recipient are visible on-chain. Use a shielded (Z) address for privacy.";
|
||||||
strings_["send_no_recent"] = "No recent sends";
|
strings_["send_no_recent"] = "No recent sends";
|
||||||
strings_["send_recent_sends"] = "RECENT SENDS";
|
strings_["send_recent_sends"] = "RECENT SENDS";
|
||||||
strings_["send_recipient"] = "RECIPIENT";
|
strings_["send_recipient"] = "RECIPIENT";
|
||||||
@@ -2572,9 +2784,12 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["send_tooltip_not_connected"] = "Not connected to daemon";
|
strings_["send_tooltip_not_connected"] = "Not connected to daemon";
|
||||||
strings_["send_tooltip_select_source"] = "Select a source address first";
|
strings_["send_tooltip_select_source"] = "Select a source address first";
|
||||||
strings_["send_tooltip_syncing"] = "Wait for blockchain to sync";
|
strings_["send_tooltip_syncing"] = "Wait for blockchain to sync";
|
||||||
|
strings_["send_tooltip_no_tip"] = "Waiting for peers to report the network height. Sending is disabled until the wallet knows it is caught up.";
|
||||||
|
strings_["send_available_when_synced"] = "You can fill in the details now. Sending is available once the wallet is fully synced.";
|
||||||
strings_["send_tooltip_view_only"] = "View-only address — no spending key, cannot send";
|
strings_["send_tooltip_view_only"] = "View-only address — no spending key, cannot send";
|
||||||
strings_["send_total"] = "Total";
|
strings_["send_total"] = "Total";
|
||||||
strings_["send_tx_failed"] = "Transaction failed";
|
strings_["send_tx_failed"] = "Transaction failed";
|
||||||
|
strings_["send_maybe_sent"] = "This transaction may already have been broadcast (the reply was lost). Do NOT send again yet - check History first.";
|
||||||
strings_["send_tx_sent"] = "Transaction sent!";
|
strings_["send_tx_sent"] = "Transaction sent!";
|
||||||
strings_["send_cannot_send_now"] = "Cannot send now — check connection, sync, and available balance.";
|
strings_["send_cannot_send_now"] = "Cannot send now — check connection, sync, and available balance.";
|
||||||
strings_["send_tx_success"] = "Transaction sent successfully!";
|
strings_["send_tx_success"] = "Transaction sent successfully!";
|
||||||
@@ -2590,7 +2805,9 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["shield_check_status"] = "Check Status";
|
strings_["shield_check_status"] = "Check Status";
|
||||||
strings_["shield_completed"] = "Operation completed successfully!";
|
strings_["shield_completed"] = "Operation completed successfully!";
|
||||||
strings_["shield_description"] = "Shield your mining rewards by sending coinbase outputs from transparent addresses to a shielded address. This improves privacy by hiding your mining income.";
|
strings_["shield_description"] = "Shield your mining rewards by sending coinbase outputs from transparent addresses to a shielded address. This improves privacy by hiding your mining income.";
|
||||||
strings_["shield_from_address"] = "From Address:";
|
strings_["shield_from_address"] = "From mining address:";
|
||||||
|
strings_["shield_all_mining_addrs"] = "All mining addresses";
|
||||||
|
strings_["shield_confirm_fmt"] = "Shield up to %s DRGX from %d mining address(es) (at most %d rewards per run) to %s? Network fee: %s DRGX.";
|
||||||
strings_["shield_funds"] = "Shield Funds";
|
strings_["shield_funds"] = "Shield Funds";
|
||||||
strings_["shield_in_progress"] = "Operation in progress...";
|
strings_["shield_in_progress"] = "Operation in progress...";
|
||||||
strings_["shield_max_utxos"] = "Max UTXOs per operation";
|
strings_["shield_max_utxos"] = "Max UTXOs per operation";
|
||||||
@@ -2618,7 +2835,7 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["merge_title"] = "Merge to Address";
|
strings_["merge_title"] = "Merge to Address";
|
||||||
// Consolidate-funds flow (rich merge modal + wallet-bloat preset).
|
// Consolidate-funds flow (rich merge modal + wallet-bloat preset).
|
||||||
strings_["consolidate_title"] = "Consolidate funds";
|
strings_["consolidate_title"] = "Consolidate funds";
|
||||||
strings_["consolidate_desc"] = "Combine many small inputs into a single shielded note. Fewer notes means a smaller wallet file and better privacy.";
|
strings_["consolidate_desc"] = "Each payment you receive is stored as a separate \"note\" \xE2\x80\x94 like an individual bill in a wallet. They pile up over time, and every note adds witness data that makes the wallet slower to sync and keeps it growing. Consolidating combines many small notes into one per address: your balance doesn't change, but the wallet scans faster, stops growing, and reveals less about your funds.";
|
||||||
strings_["consolidate_funds_btn"] = "Consolidate";
|
strings_["consolidate_funds_btn"] = "Consolidate";
|
||||||
strings_["merge_scope_loading"] = "Checking your inputs\xE2\x80\xA6";
|
strings_["merge_scope_loading"] = "Checking your inputs\xE2\x80\xA6";
|
||||||
strings_["merge_scope_fmt"] = "%d transparent + %d shielded inputs \xC2\xB7 ~%s DRGX spendable";
|
strings_["merge_scope_fmt"] = "%d transparent + %d shielded inputs \xC2\xB7 ~%s DRGX spendable";
|
||||||
@@ -2695,6 +2912,8 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["validate_invalid"] = "INVALID";
|
strings_["validate_invalid"] = "INVALID";
|
||||||
strings_["validate_is_mine"] = "This wallet owns this address";
|
strings_["validate_is_mine"] = "This wallet owns this address";
|
||||||
strings_["validate_not_mine"] = "Not owned by this wallet";
|
strings_["validate_not_mine"] = "Not owned by this wallet";
|
||||||
|
strings_["validate_ownership_unknown_offline"] = "Unknown — node offline";
|
||||||
|
strings_["validate_ownership_unknown_lite"] = "Unknown — not checkable in the lite wallet";
|
||||||
strings_["validate_ownership"] = "Ownership:";
|
strings_["validate_ownership"] = "Ownership:";
|
||||||
strings_["validate_results"] = "Results:";
|
strings_["validate_results"] = "Results:";
|
||||||
strings_["validate_results_placeholder"] = "Results will appear here";
|
strings_["validate_results_placeholder"] = "Results will appear here";
|
||||||
@@ -2756,6 +2975,7 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["explorer_invalid_query"] = "Enter a block height or 64-character hash";
|
strings_["explorer_invalid_query"] = "Enter a block height or 64-character hash";
|
||||||
strings_["explorer_invalid_response"] = "Invalid response from daemon";
|
strings_["explorer_invalid_response"] = "Invalid response from daemon";
|
||||||
strings_["explorer_hash_not_found"] = "No block or transaction found for this hash";
|
strings_["explorer_hash_not_found"] = "No block or transaction found for this hash";
|
||||||
|
strings_["explorer_lookup_failed"] = "Lookup failed";
|
||||||
strings_["explorer_not_connected"] = "Not connected to daemon — cannot look up a block or transaction hash";
|
strings_["explorer_not_connected"] = "Not connected to daemon — cannot look up a block or transaction hash";
|
||||||
strings_["explorer_no_results"] = "No matching cached blocks";
|
strings_["explorer_no_results"] = "No matching cached blocks";
|
||||||
strings_["explorer_beyond_tip"] = "Block %d is beyond the chain tip (%d)";
|
strings_["explorer_beyond_tip"] = "Block %d is beyond the chain tip (%d)";
|
||||||
@@ -2983,6 +3203,15 @@ const char* I18n::translate(const char* key) const
|
|||||||
return key;
|
return key;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string I18n::translateCopy(const char* key) const
|
||||||
|
{
|
||||||
|
if (!key) return std::string();
|
||||||
|
std::shared_lock<std::shared_mutex> lock(strings_mutex_);
|
||||||
|
auto it = strings_.find(key);
|
||||||
|
if (it != strings_.end()) return it->second;
|
||||||
|
return std::string(key);
|
||||||
|
}
|
||||||
|
|
||||||
void I18n::registerLanguage(const std::string& code, const std::string& name)
|
void I18n::registerLanguage(const std::string& code, const std::string& name)
|
||||||
{
|
{
|
||||||
// Check if already registered
|
// Check if already registered
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <mutex>
|
||||||
|
#include <shared_mutex>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@@ -39,6 +41,15 @@ public:
|
|||||||
*/
|
*/
|
||||||
const char* translate(const char* key) const;
|
const char* translate(const char* key) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Thread-safe translate: returns a COPY under a read lock.
|
||||||
|
*
|
||||||
|
* translate() hands back a pointer into the string table, which loadLanguage() rewrites on the
|
||||||
|
* main thread. Any caller off the main thread (e.g. the EmbeddedDaemon monitor thread) MUST use
|
||||||
|
* this instead; main-thread UI code keeps using translate()/TR().
|
||||||
|
*/
|
||||||
|
std::string translateCopy(const char* key) const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Get current locale
|
* @brief Get current locale
|
||||||
*/
|
*/
|
||||||
@@ -66,6 +77,9 @@ private:
|
|||||||
void overlayTranslations(const nlohmann::json& translations);
|
void overlayTranslations(const nlohmann::json& translations);
|
||||||
|
|
||||||
std::string current_locale_ = "en";
|
std::string current_locale_ = "en";
|
||||||
|
// Guards strings_/current_locale_ against loadLanguage() (main thread, writer) racing
|
||||||
|
// translateCopy() (off-thread readers). The lock-free translate() is main-thread only.
|
||||||
|
mutable std::shared_mutex strings_mutex_;
|
||||||
std::unordered_map<std::string, std::string> strings_;
|
std::unordered_map<std::string, std::string> strings_;
|
||||||
std::vector<std::pair<std::string, std::string>> available_languages_;
|
std::vector<std::pair<std::string, std::string>> available_languages_;
|
||||||
};
|
};
|
||||||
@@ -79,6 +93,13 @@ inline const char* tr(const char* key) {
|
|||||||
return I18n::instance().translate(key);
|
return I18n::instance().translate(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Thread-safe translation returning an owned copy. Use from any non-main thread.
|
||||||
|
*/
|
||||||
|
inline std::string trCopy(const char* key) {
|
||||||
|
return I18n::instance().translateCopy(key);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Map a raw daemon / JSON-RPC error string to a short, friendly, TRANSLATED explanation.
|
* @brief Map a raw daemon / JSON-RPC error string to a short, friendly, TRANSLATED explanation.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -10,6 +10,29 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// Strictly parse a decimal DRGX amount from an (attacker-influenceable) payment URI / QR: digits with at
|
||||||
|
// most one '.', at most 8 fractional digits, in [0, 21000000]. Rejects empty, NaN/inf, signs, whitespace
|
||||||
|
// and trailing garbage that std::stod would otherwise accept — so a crafted URI can't inject a bogus
|
||||||
|
// amount into the Send form.
|
||||||
|
bool parseStrictAmount(const std::string& s, double& out) {
|
||||||
|
if (s.empty() || s.size() > 32) return false;
|
||||||
|
int dots = 0, digits = 0, frac = 0; bool afterDot = false;
|
||||||
|
for (char c : s) {
|
||||||
|
if (c == '.') { if (++dots > 1) return false; afterDot = true; }
|
||||||
|
else if (c >= '0' && c <= '9') { ++digits; if (afterDot && ++frac > 8) return false; }
|
||||||
|
else return false;
|
||||||
|
}
|
||||||
|
if (digits == 0) return false;
|
||||||
|
double v = 0.0;
|
||||||
|
try { v = std::stod(s); } catch (...) { return false; }
|
||||||
|
if (!std::isfinite(v) || v < 0.0 || v > 21000000.0) return false;
|
||||||
|
out = v;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
namespace dragonx {
|
namespace dragonx {
|
||||||
namespace util {
|
namespace util {
|
||||||
@@ -141,11 +164,13 @@ PaymentURI parsePaymentURI(const std::string& uri)
|
|||||||
std::transform(key.begin(), key.end(), key.begin(), ::tolower);
|
std::transform(key.begin(), key.end(), key.begin(), ::tolower);
|
||||||
|
|
||||||
if (key == "amount" || key == "amt") {
|
if (key == "amount" || key == "amt") {
|
||||||
try {
|
double a = 0.0;
|
||||||
result.amount = std::stod(value);
|
if (!parseStrictAmount(value, a)) {
|
||||||
} catch (...) {
|
result.error = "Invalid amount";
|
||||||
// Invalid amount, keep as 0
|
result.valid = false;
|
||||||
|
return result; // reject the whole URI rather than silently treating it as 0
|
||||||
}
|
}
|
||||||
|
result.amount = a;
|
||||||
} else if (key == "label") {
|
} else if (key == "label") {
|
||||||
result.label = value;
|
result.label = value;
|
||||||
} else if (key == "memo") {
|
} else if (key == "memo") {
|
||||||
@@ -174,7 +199,7 @@ PaymentURI parsePaymentURI(const std::string& uri)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate amount if present
|
// Validate amount if present
|
||||||
if (result.amount < 0) {
|
if (!std::isfinite(result.amount) || result.amount < 0 || result.amount > 21000000.0) {
|
||||||
result.error = "Invalid negative amount";
|
result.error = "Invalid negative amount";
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
+55
-8
@@ -25,6 +25,7 @@
|
|||||||
#include <shlobj.h>
|
#include <shlobj.h>
|
||||||
#include <psapi.h>
|
#include <psapi.h>
|
||||||
#include <tlhelp32.h>
|
#include <tlhelp32.h>
|
||||||
|
#include <aclapi.h> // SetEntriesInAcl / EXPLICIT_ACCESS — owner-only DACL for restricted writes
|
||||||
#else
|
#else
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
@@ -186,11 +187,15 @@ bool Platform::openFolder(const std::string& path, bool createIfMissing)
|
|||||||
|
|
||||||
uint64_t Platform::getFileSize(const std::string& path)
|
uint64_t Platform::getFileSize(const std::string& path)
|
||||||
{
|
{
|
||||||
struct stat st;
|
// Use std::filesystem (64-bit uintmax_t) rather than stat(): on mingw/Windows struct stat's
|
||||||
if (stat(path.c_str(), &st) == 0) {
|
// st_size is a 32-bit signed _off_t, so a wallet.dat over 2 GiB overflows negative and casts to a
|
||||||
return static_cast<uint64_t>(st.st_size);
|
// garbage ~2^64 value (the "17179869182 GB" bug). The error_code overload returns on failure
|
||||||
}
|
// (missing / unreadable / Windows sharing violation while the daemon holds the file) instead of
|
||||||
return 0;
|
// throwing, and we report 0 so callers fall back to "unknown".
|
||||||
|
std::error_code ec;
|
||||||
|
const auto sz = std::filesystem::file_size(path, ec);
|
||||||
|
if (ec) return 0;
|
||||||
|
return static_cast<uint64_t>(sz);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string Platform::formatFileSize(uint64_t bytes)
|
std::string Platform::formatFileSize(uint64_t bytes)
|
||||||
@@ -361,15 +366,54 @@ bool Platform::writeFileAtomically(const std::string& path,
|
|||||||
const std::string tmp = path + ".tmp";
|
const std::string tmp = path + ".tmp";
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
|
// When restrictPermissions, build a PROTECTED owner-only DACL so the secret file (exported keys,
|
||||||
|
// seed backup, vault, chat export) is readable only by the current user — not whatever the parent
|
||||||
|
// dir's inherited ACL grants. Best-effort: on any failure fall back to default ACLs (the per-user
|
||||||
|
// profile dir still protects it). The ACL is set on the temp file; MoveFileEx is a same-volume
|
||||||
|
// rename, so the explicit (non-inherited) DACL carries to the final path.
|
||||||
|
PACL dacl = nullptr;
|
||||||
|
PTOKEN_USER tokenUser = nullptr;
|
||||||
|
SECURITY_DESCRIPTOR sd;
|
||||||
|
SECURITY_ATTRIBUTES saOwner{};
|
||||||
|
SECURITY_ATTRIBUTES* psa = nullptr;
|
||||||
|
if (restrictPermissions) {
|
||||||
|
HANDLE tok = nullptr;
|
||||||
|
if (OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &tok)) {
|
||||||
|
DWORD need = 0;
|
||||||
|
GetTokenInformation(tok, TokenUser, nullptr, 0, &need);
|
||||||
|
if (need > 0) {
|
||||||
|
tokenUser = static_cast<PTOKEN_USER>(LocalAlloc(LPTR, need));
|
||||||
|
if (tokenUser && GetTokenInformation(tok, TokenUser, tokenUser, need, &need)) {
|
||||||
|
EXPLICIT_ACCESSA ea{};
|
||||||
|
ea.grfAccessPermissions = GENERIC_ALL;
|
||||||
|
ea.grfAccessMode = SET_ACCESS;
|
||||||
|
ea.grfInheritance = NO_INHERITANCE;
|
||||||
|
ea.Trustee.TrusteeForm = TRUSTEE_IS_SID;
|
||||||
|
ea.Trustee.TrusteeType = TRUSTEE_IS_USER;
|
||||||
|
ea.Trustee.ptstrName = static_cast<LPSTR>(tokenUser->User.Sid);
|
||||||
|
if (SetEntriesInAclA(1, &ea, nullptr, &dacl) == ERROR_SUCCESS &&
|
||||||
|
InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION) &&
|
||||||
|
SetSecurityDescriptorControl(&sd, SE_DACL_PROTECTED, SE_DACL_PROTECTED) &&
|
||||||
|
SetSecurityDescriptorDacl(&sd, TRUE, dacl, FALSE)) {
|
||||||
|
saOwner.nLength = sizeof(saOwner);
|
||||||
|
saOwner.lpSecurityDescriptor = &sd;
|
||||||
|
saOwner.bInheritHandle = FALSE;
|
||||||
|
psa = &saOwner;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CloseHandle(tok);
|
||||||
|
}
|
||||||
|
}
|
||||||
{
|
{
|
||||||
// One handle for write + flush. CreateFileA with no SECURITY_ATTRIBUTES is NOT inheritable: the old
|
// One handle for write + flush. CreateFileA with no SECURITY_ATTRIBUTES is NOT inheritable: the old
|
||||||
// std::ofstream (CRT) handle was, so a child spawned with bInheritHandles=TRUE at that moment (xmrig,
|
// std::ofstream (CRT) handle was, so a child spawned with bInheritHandles=TRUE at that moment (xmrig,
|
||||||
// runHiddenCapture) kept "<path>.tmp" open without FILE_SHARE_DELETE and every later MoveFileEx
|
// runHiddenCapture) kept "<path>.tmp" open without FILE_SHARE_DELETE and every later MoveFileEx
|
||||||
// failed while it ran. Also no second share-mode-0 open, which silently skipped the flush whenever
|
// failed while it ran. Also no second share-mode-0 open, which silently skipped the flush whenever
|
||||||
// AV / the indexer had the new file open.
|
// AV / the indexer had the new file open.
|
||||||
HANDLE h = CreateFileA(tmp.c_str(), GENERIC_WRITE, 0, nullptr,
|
HANDLE h = CreateFileA(tmp.c_str(), GENERIC_WRITE, 0, psa,
|
||||||
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
|
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||||
if (h == INVALID_HANDLE_VALUE) return false;
|
if (h == INVALID_HANDLE_VALUE) { if (dacl) LocalFree(dacl); if (tokenUser) LocalFree(tokenUser); return false; }
|
||||||
bool ok = true;
|
bool ok = true;
|
||||||
size_t written = 0;
|
size_t written = 0;
|
||||||
while (written < content.size()) {
|
while (written < content.size()) {
|
||||||
@@ -382,9 +426,12 @@ bool Platform::writeFileAtomically(const std::string& path,
|
|||||||
// Flush the temp file's contents to stable storage before the rename.
|
// Flush the temp file's contents to stable storage before the rename.
|
||||||
if (ok && !FlushFileBuffers(h)) ok = false;
|
if (ok && !FlushFileBuffers(h)) ok = false;
|
||||||
CloseHandle(h);
|
CloseHandle(h);
|
||||||
|
// The DACL is baked into the temp file's security descriptor at CreateFileA time, so release the
|
||||||
|
// builders now; every path below (including the early returns) is past this point.
|
||||||
|
if (dacl) LocalFree(dacl);
|
||||||
|
if (tokenUser) LocalFree(tokenUser);
|
||||||
if (!ok) { DeleteFileA(tmp.c_str()); return false; }
|
if (!ok) { DeleteFileA(tmp.c_str()); return false; }
|
||||||
}
|
}
|
||||||
(void)restrictPermissions; // owner-only ACLs rely on the per-user profile dir on Windows
|
|
||||||
// MoveFileEx with REPLACE_EXISTING is atomic on NTFS; WRITE_THROUGH makes it durable. AV / the search
|
// MoveFileEx with REPLACE_EXISTING is atomic on NTFS; WRITE_THROUGH makes it durable. AV / the search
|
||||||
// indexer can hold the destination briefly (ACCESS_DENIED / SHARING_VIOLATION / LOCK_VIOLATION): retry
|
// indexer can hold the destination briefly (ACCESS_DENIED / SHARING_VIOLATION / LOCK_VIOLATION): retry
|
||||||
// a few times (20+40+80+160 ms worst case, only on a write that used to just fail) instead of dropping it.
|
// a few times (20+40+80+160 ms worst case, only on a write that used to just fail) instead of dropping it.
|
||||||
|
|||||||
@@ -48,7 +48,11 @@ void SecureVault::setWalletScope(const std::string& walletFile) {
|
|||||||
// any other wallet gets a per-wallet tag. A readable sanitized prefix PLUS an 8-hex FNV-1a of the
|
// any other wallet gets a per-wallet tag. A readable sanitized prefix PLUS an 8-hex FNV-1a of the
|
||||||
// RAW filename — the hash disambiguates so two distinct files can never share a vault even if their
|
// RAW filename — the hash disambiguates so two distinct files can never share a vault even if their
|
||||||
// sanitized forms collide (e.g. "wallet foo.dat" vs "wallet_foo.dat").
|
// sanitized forms collide (e.g. "wallet foo.dat" vs "wallet_foo.dat").
|
||||||
if (walletFile.empty() || walletFile == "wallet.dat") { scope_.clear(); return; }
|
if (walletFile.empty() || walletFile == "wallet.dat") {
|
||||||
|
std::lock_guard<std::mutex> lk(scopeMutex_);
|
||||||
|
scope_.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
std::string s;
|
std::string s;
|
||||||
s.reserve(walletFile.size());
|
s.reserve(walletFile.size());
|
||||||
for (unsigned char c : walletFile)
|
for (unsigned char c : walletFile)
|
||||||
@@ -58,11 +62,18 @@ void SecureVault::setWalletScope(const std::string& walletFile) {
|
|||||||
char suffix[9];
|
char suffix[9];
|
||||||
std::snprintf(suffix, sizeof(suffix), "%08x", static_cast<unsigned>(h & 0xffffffffu));
|
std::snprintf(suffix, sizeof(suffix), "%08x", static_cast<unsigned>(h & 0xffffffffu));
|
||||||
if (s.size() > 48) s.resize(48);
|
if (s.size() > 48) s.resize(48);
|
||||||
scope_ = s + "-" + suffix;
|
std::string newScope = s + "-" + suffix;
|
||||||
|
std::lock_guard<std::mutex> lk(scopeMutex_);
|
||||||
|
scope_ = std::move(newScope);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string SecureVault::getVaultPath() const {
|
std::string SecureVault::getVaultPath() const {
|
||||||
const std::string name = scope_.empty() ? std::string("vault.dat") : ("vault-" + scope_ + ".dat");
|
std::string scope;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lk(scopeMutex_);
|
||||||
|
scope = scope_;
|
||||||
|
}
|
||||||
|
const std::string name = scope.empty() ? std::string("vault.dat") : ("vault-" + scope + ".dat");
|
||||||
return (fs::path(Platform::getConfigDir()) / name).string();
|
return (fs::path(Platform::getConfigDir()) / name).string();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user