feat(wallet): consolidate notes per address, keeping each address's funds in place
Users who keep funds apart under different labelled addresses can now shed notes (smaller wallet, faster scans/witness rebuilds) without the funds being merged into a single address. - Merge/Consolidate dialog: "Where should the funds go?" - Into one address (unchanged) or Keep in each address. The latter lists shielded addresses with 2+ spendable notes (label, note count, amount; too-small ones disabled) and merges each address's notes back into that address with z_mergetoaddress [addr] addr. Addresses with more notes than the batch size get several batches (the daemon locks a batch's notes before returning, so calls take disjoint notes); the dialog suggests a second run after those confirm. Per-address progress; ops keep running if the dialog is closed. The Consolidate (wallet-size) preset opens in this mode; Merge keeps "Into one". Shielded only: a transparent address can't take a merge's output. - services/note_consolidation: groupSpendableNotes, worthConsolidating, estimateBatches, consolidateInPlace (pure; driven by a merge-call function), with unit tests against a stand-in following dragonxd's rules. - fake_dragonxd: optional note wallet (FAKE_WALLET_NOTES, FAKE_MERGE_SECS) with z_mergetoaddress and its operations. - i18n: 17 new keys in all 8 languages; CJK subset rebuilt. - UI sweep: modal-consolidate target. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -467,6 +467,7 @@ set(APP_SOURCES
|
|||||||
src/app_sweep.cpp
|
src/app_sweep.cpp
|
||||||
src/app_wizard.cpp
|
src/app_wizard.cpp
|
||||||
src/services/network_refresh_service.cpp
|
src/services/network_refresh_service.cpp
|
||||||
|
src/services/note_consolidation.cpp
|
||||||
src/services/refresh_scheduler.cpp
|
src/services/refresh_scheduler.cpp
|
||||||
src/services/wallet_security_controller.cpp
|
src/services/wallet_security_controller.cpp
|
||||||
src/services/wallet_security_workflow.cpp
|
src/services/wallet_security_workflow.cpp
|
||||||
@@ -1146,6 +1147,7 @@ if(BUILD_TESTING)
|
|||||||
add_executable(ObsidianDragonTests
|
add_executable(ObsidianDragonTests
|
||||||
tests/test_phase4.cpp
|
tests/test_phase4.cpp
|
||||||
src/services/network_refresh_service.cpp
|
src/services/network_refresh_service.cpp
|
||||||
|
src/services/note_consolidation.cpp
|
||||||
src/services/refresh_scheduler.cpp
|
src/services/refresh_scheduler.cpp
|
||||||
src/services/wallet_security_controller.cpp
|
src/services/wallet_security_controller.cpp
|
||||||
src/services/wallet_security_workflow.cpp
|
src/services/wallet_security_workflow.cpp
|
||||||
|
|||||||
Binary file not shown.
@@ -1163,6 +1163,22 @@
|
|||||||
"merge_description": "Mehrere UTXOs zu einer einzelnen abgeschirmten Adresse zusammenführen. Dies kann die Wallet-Größe reduzieren und die Privatsphäre verbessern.",
|
"merge_description": "Mehrere UTXOs zu einer einzelnen abgeschirmten Adresse zusammenführen. Dies kann die Wallet-Größe reduzieren und die Privatsphäre verbessern.",
|
||||||
"merge_fee_hint": "Netzwerkgebühr für diese Transaktion.",
|
"merge_fee_hint": "Netzwerkgebühr für diese Transaktion.",
|
||||||
"merge_funds": "Gelder zusammenführen",
|
"merge_funds": "Gelder zusammenführen",
|
||||||
|
"merge_into_one": "In eine Adresse",
|
||||||
|
"merge_keep_each": "In jeder Adresse belassen",
|
||||||
|
"merge_keep_each_batches_fmt": "Adressen mit mehr als %d Notes werden in mehreren Durchgängen zusammengeführt; führen Sie es nach deren Bestätigung erneut aus, um den Rest zusammenzuführen.",
|
||||||
|
"merge_keep_each_confirm_fmt": "%d Notes in %d Adressen konsolidieren? Jede Adresse behält ihre eigenen Mittel. Gebühren: etwa %s DRGX (eine pro Durchgang).",
|
||||||
|
"merge_keep_each_desc": "Die Notes jeder Adresse werden in genau diese Adresse zurückgeführt, sodass Mittel, die Sie unter verschiedenen Bezeichnungen getrennt halten, getrennt bleiben. Nur abgeschirmte Adressen – transparente Mittel können nur abgeschirmt werden (In eine Adresse).",
|
||||||
|
"merge_keep_each_none": "Jede abgeschirmte Adresse hat bereits nur eine Note – nichts zu konsolidieren.",
|
||||||
|
"merge_keep_each_nothing_submitted": "Nichts wurde konsolidiert – der Node hat jede Zusammenführung abgelehnt.",
|
||||||
|
"merge_keep_each_row_fmt": "%s · %d Notes · %s DRGX",
|
||||||
|
"merge_keep_each_run_again": "Adressen, die in mehreren Durchgängen zusammengeführt wurden, haben jetzt eine Note pro Durchgang. Führen Sie dies nach deren Bestätigung erneut aus, um sie zu einer zusammenzuführen.",
|
||||||
|
"merge_keep_each_select": "Wählen Sie mindestens eine Adresse zum Konsolidieren aus.",
|
||||||
|
"merge_keep_each_some_failed": "Einige Zusammenführungen sind fehlgeschlagen – siehe Liste oben.",
|
||||||
|
"merge_keep_each_status_done": "%s – fertig: %d Notes in %d Durchgang/Durchgängen zusammengeführt",
|
||||||
|
"merge_keep_each_status_failed": "%s – fehlgeschlagen: %s",
|
||||||
|
"merge_keep_each_status_not_started": "%s – nicht gestartet: %s",
|
||||||
|
"merge_keep_each_status_running": "%s – %d Notes werden zusammengeführt (%d von %d Durchgängen fertig)…",
|
||||||
|
"merge_keep_each_too_small": "Zu klein zum Konsolidieren: Die Gebühr würde diese Mittel aufbrauchen.",
|
||||||
"merge_max_inputs": "Max. Inputs pro Batch",
|
"merge_max_inputs": "Max. Inputs pro Batch",
|
||||||
"merge_no_spendable": "Noch keine verfügbaren Inputs zum Konsolidieren.",
|
"merge_no_spendable": "Noch keine verfügbaren Inputs zum Konsolidieren.",
|
||||||
"merge_progress": "Konsolidierung läuft… dies kann einige Minuten dauern. Sie können dieses Fenster schließen.",
|
"merge_progress": "Konsolidierung läuft… dies kann einige Minuten dauern. Sie können dieses Fenster schließen.",
|
||||||
@@ -1175,6 +1191,7 @@
|
|||||||
"merge_src_transparent": "Transparent",
|
"merge_src_transparent": "Transparent",
|
||||||
"merge_started": "Zusammenführung gestartet",
|
"merge_started": "Zusammenführung gestartet",
|
||||||
"merge_title": "An Adresse zusammenführen",
|
"merge_title": "An Adresse zusammenführen",
|
||||||
|
"merge_where": "Wohin sollen die Mittel gehen?",
|
||||||
"mig_adopt": "Dies zu meiner Wallet machen",
|
"mig_adopt": "Dies zu meiner Wallet machen",
|
||||||
"mig_adopt_now": "Seed-Wallet übernehmen",
|
"mig_adopt_now": "Seed-Wallet übernehmen",
|
||||||
"mig_adopting": "Ihre neue Wallet wird installiert und neu gescannt…",
|
"mig_adopting": "Ihre neue Wallet wird installiert und neu gescannt…",
|
||||||
|
|||||||
@@ -1163,6 +1163,22 @@
|
|||||||
"merge_description": "Fusiona múltiples UTXOs en una sola dirección protegida. Esto puede ayudar a reducir el tamaño de la cartera y mejorar la privacidad.",
|
"merge_description": "Fusiona múltiples UTXOs en una sola dirección protegida. Esto puede ayudar a reducir el tamaño de la cartera y mejorar la privacidad.",
|
||||||
"merge_fee_hint": "Comisión de red para esta transacción.",
|
"merge_fee_hint": "Comisión de red para esta transacción.",
|
||||||
"merge_funds": "Fusionar Fondos",
|
"merge_funds": "Fusionar Fondos",
|
||||||
|
"merge_into_one": "A una sola dirección",
|
||||||
|
"merge_keep_each": "Mantener en cada dirección",
|
||||||
|
"merge_keep_each_batches_fmt": "Las direcciones con más de %d notas se fusionan en varios lotes; vuelve a ejecutarlo cuando se confirmen para fusionar el resto.",
|
||||||
|
"merge_keep_each_confirm_fmt": "¿Consolidar %d notas en %d direcciones? Cada dirección conserva sus propios fondos. Comisiones: unos %s DRGX (una por lote).",
|
||||||
|
"merge_keep_each_desc": "Las notas de cada dirección se fusionan de nuevo en esa misma dirección, así que los fondos que mantienes separados con distintas etiquetas siguen separados. Solo direcciones blindadas: los fondos transparentes solo pueden blindarse (A una sola dirección).",
|
||||||
|
"merge_keep_each_none": "Cada dirección blindada ya tiene una sola nota: no hay nada que consolidar.",
|
||||||
|
"merge_keep_each_nothing_submitted": "No se consolidó nada: el nodo rechazó todas las fusiones.",
|
||||||
|
"merge_keep_each_row_fmt": "%s · %d notas · %s DRGX",
|
||||||
|
"merge_keep_each_run_again": "Las direcciones fusionadas en varios lotes tienen ahora una nota por lote. Vuelve a ejecutarlo cuando se confirmen para fusionarlas en una.",
|
||||||
|
"merge_keep_each_select": "Marca al menos una dirección para consolidar.",
|
||||||
|
"merge_keep_each_some_failed": "Algunas fusiones fallaron; consulta la lista de arriba.",
|
||||||
|
"merge_keep_each_status_done": "%s: listo: %d notas fusionadas en %d lote(s)",
|
||||||
|
"merge_keep_each_status_failed": "%s: falló: %s",
|
||||||
|
"merge_keep_each_status_not_started": "%s: no iniciado: %s",
|
||||||
|
"merge_keep_each_status_running": "%s: fusionando %d notas (%d de %d lotes listos)…",
|
||||||
|
"merge_keep_each_too_small": "Demasiado poco para consolidar: la comisión se llevaría estos fondos.",
|
||||||
"merge_max_inputs": "Máximo de entradas por lote",
|
"merge_max_inputs": "Máximo de entradas por lote",
|
||||||
"merge_no_spendable": "Aún no hay entradas disponibles para consolidar.",
|
"merge_no_spendable": "Aún no hay entradas disponibles para consolidar.",
|
||||||
"merge_progress": "Consolidando… esto puede tardar varios minutos. Puedes cerrar esta ventana.",
|
"merge_progress": "Consolidando… esto puede tardar varios minutos. Puedes cerrar esta ventana.",
|
||||||
@@ -1175,6 +1191,7 @@
|
|||||||
"merge_src_transparent": "Transparentes",
|
"merge_src_transparent": "Transparentes",
|
||||||
"merge_started": "Operación de fusión iniciada",
|
"merge_started": "Operación de fusión iniciada",
|
||||||
"merge_title": "Fusionar a Dirección",
|
"merge_title": "Fusionar a Dirección",
|
||||||
|
"merge_where": "¿Adónde deben ir los fondos?",
|
||||||
"mig_adopt": "Hacer que esta sea mi cartera",
|
"mig_adopt": "Hacer que esta sea mi cartera",
|
||||||
"mig_adopt_now": "Adoptar cartera con frase",
|
"mig_adopt_now": "Adoptar cartera con frase",
|
||||||
"mig_adopting": "Instalando tu nueva cartera y reescaneando…",
|
"mig_adopting": "Instalando tu nueva cartera y reescaneando…",
|
||||||
|
|||||||
@@ -1163,6 +1163,22 @@
|
|||||||
"merge_description": "Fusionnez plusieurs UTXOs en une seule adresse blindée. Cela peut réduire la taille du portefeuille et améliorer la confidentialité.",
|
"merge_description": "Fusionnez plusieurs UTXOs en une seule adresse blindée. Cela peut réduire la taille du portefeuille et améliorer la confidentialité.",
|
||||||
"merge_fee_hint": "Frais réseau pour cette transaction.",
|
"merge_fee_hint": "Frais réseau pour cette transaction.",
|
||||||
"merge_funds": "Fusionner les fonds",
|
"merge_funds": "Fusionner les fonds",
|
||||||
|
"merge_into_one": "Vers une seule adresse",
|
||||||
|
"merge_keep_each": "Garder dans chaque adresse",
|
||||||
|
"merge_keep_each_batches_fmt": "Les adresses de plus de %d notes sont fusionnées en plusieurs lots ; relancez l'opération une fois ceux-ci confirmés pour fusionner le reste.",
|
||||||
|
"merge_keep_each_confirm_fmt": "Consolider %d notes dans %d adresses ? Chaque adresse garde ses propres fonds. Frais : environ %s DRGX (un par lot).",
|
||||||
|
"merge_keep_each_desc": "Les notes de chaque adresse sont refusionnées dans cette même adresse : les fonds que vous gardez séparés sous des libellés différents restent séparés. Adresses blindées uniquement — les fonds transparents ne peuvent qu'être blindés (Vers une seule adresse).",
|
||||||
|
"merge_keep_each_none": "Chaque adresse blindée ne contient déjà qu'une seule note — rien à consolider.",
|
||||||
|
"merge_keep_each_nothing_submitted": "Rien n'a été consolidé — le nœud a refusé chaque fusion.",
|
||||||
|
"merge_keep_each_row_fmt": "%s · %d notes · %s DRGX",
|
||||||
|
"merge_keep_each_run_again": "Les adresses fusionnées en plusieurs lots contiennent maintenant une note par lot. Relancez l'opération une fois ceux-ci confirmés pour les fusionner en une seule.",
|
||||||
|
"merge_keep_each_select": "Cochez au moins une adresse à consolider.",
|
||||||
|
"merge_keep_each_some_failed": "Certaines fusions ont échoué — voir la liste ci-dessus.",
|
||||||
|
"merge_keep_each_status_done": "%s — terminé : %d notes fusionnées en %d lot(s)",
|
||||||
|
"merge_keep_each_status_failed": "%s — échec : %s",
|
||||||
|
"merge_keep_each_status_not_started": "%s — non démarré : %s",
|
||||||
|
"merge_keep_each_status_running": "%s — fusion de %d notes (%d lots sur %d terminés)…",
|
||||||
|
"merge_keep_each_too_small": "Trop faible pour être consolidé : les frais absorberaient ces fonds.",
|
||||||
"merge_max_inputs": "Nombre max d'entrées par lot",
|
"merge_max_inputs": "Nombre max d'entrées par lot",
|
||||||
"merge_no_spendable": "Aucune entrée dépensable à consolider pour l'instant.",
|
"merge_no_spendable": "Aucune entrée dépensable à consolider pour l'instant.",
|
||||||
"merge_progress": "Consolidation en cours… cela peut prendre quelques minutes. Vous pouvez fermer cette fenêtre.",
|
"merge_progress": "Consolidation en cours… cela peut prendre quelques minutes. Vous pouvez fermer cette fenêtre.",
|
||||||
@@ -1175,6 +1191,7 @@
|
|||||||
"merge_src_transparent": "Transparentes",
|
"merge_src_transparent": "Transparentes",
|
||||||
"merge_started": "Opération de fusion démarrée",
|
"merge_started": "Opération de fusion démarrée",
|
||||||
"merge_title": "Fusionner vers une adresse",
|
"merge_title": "Fusionner vers une adresse",
|
||||||
|
"merge_where": "Où doivent aller les fonds ?",
|
||||||
"mig_adopt": "Faire de ceci mon portefeuille",
|
"mig_adopt": "Faire de ceci mon portefeuille",
|
||||||
"mig_adopt_now": "Adopter le portefeuille à phrase",
|
"mig_adopt_now": "Adopter le portefeuille à phrase",
|
||||||
"mig_adopting": "Installation de votre nouveau portefeuille et rescan en cours…",
|
"mig_adopting": "Installation de votre nouveau portefeuille et rescan en cours…",
|
||||||
|
|||||||
@@ -1163,6 +1163,22 @@
|
|||||||
"merge_description": "複数のUTXOを単一のシールドアドレスに統合します。ウォレットサイズの縮小とプライバシーの向上に役立ちます。",
|
"merge_description": "複数のUTXOを単一のシールドアドレスに統合します。ウォレットサイズの縮小とプライバシーの向上に役立ちます。",
|
||||||
"merge_fee_hint": "この取引にかかるネットワーク手数料です。",
|
"merge_fee_hint": "この取引にかかるネットワーク手数料です。",
|
||||||
"merge_funds": "資金を統合",
|
"merge_funds": "資金を統合",
|
||||||
|
"merge_into_one": "1 つのアドレスへ",
|
||||||
|
"merge_keep_each": "各アドレスに残す",
|
||||||
|
"merge_keep_each_batches_fmt": "ノートが %d 件を超えるアドレスは複数回に分けて統合されます。承認後にもう一度実行すると残りが統合されます。",
|
||||||
|
"merge_keep_each_confirm_fmt": "%d 件のノートを %d 個のアドレスで統合しますか?各アドレスの資金はそのまま残ります。手数料:約 %s DRGX(1 回ごとに 1 件)。",
|
||||||
|
"merge_keep_each_desc": "各アドレスのノートはそのアドレス自身に統合されるため、ラベルごとに分けている資金は分かれたままです。シールドアドレスのみ対象です。透明アドレスの資金はシールドすることしかできません(1 つのアドレスへ)。",
|
||||||
|
"merge_keep_each_none": "すべてのシールドアドレスのノートはすでに 1 つです。統合するものはありません。",
|
||||||
|
"merge_keep_each_nothing_submitted": "何も統合されませんでした。ノードがすべての統合を拒否しました。",
|
||||||
|
"merge_keep_each_row_fmt": "%s · ノート %d 件 · %s DRGX",
|
||||||
|
"merge_keep_each_run_again": "複数回に分けて統合したアドレスには、回数分のノートが残っています。承認後にもう一度実行すると 1 つにまとめられます。",
|
||||||
|
"merge_keep_each_select": "統合するアドレスを 1 つ以上選んでください。",
|
||||||
|
"merge_keep_each_some_failed": "一部の統合に失敗しました。上の一覧を確認してください。",
|
||||||
|
"merge_keep_each_status_done": "%s — 完了:%d 件のノートを %d 回で統合",
|
||||||
|
"merge_keep_each_status_failed": "%s — 失敗:%s",
|
||||||
|
"merge_keep_each_status_not_started": "%s — 開始されませんでした:%s",
|
||||||
|
"merge_keep_each_status_running": "%s — %d 件のノートを統合中(%d / %d 回完了)…",
|
||||||
|
"merge_keep_each_too_small": "少なすぎて統合できません。手数料でこの資金がなくなります。",
|
||||||
"merge_max_inputs": "バッチあたりの最大入力数",
|
"merge_max_inputs": "バッチあたりの最大入力数",
|
||||||
"merge_no_spendable": "統合できる使用可能な入力がまだありません。",
|
"merge_no_spendable": "統合できる使用可能な入力がまだありません。",
|
||||||
"merge_progress": "統合中… 数分かかることがあります。このウィンドウは閉じても構いません。",
|
"merge_progress": "統合中… 数分かかることがあります。このウィンドウは閉じても構いません。",
|
||||||
@@ -1175,6 +1191,7 @@
|
|||||||
"merge_src_transparent": "透明",
|
"merge_src_transparent": "透明",
|
||||||
"merge_started": "統合操作を開始しました",
|
"merge_started": "統合操作を開始しました",
|
||||||
"merge_title": "アドレスに統合",
|
"merge_title": "アドレスに統合",
|
||||||
|
"merge_where": "資金の移動先",
|
||||||
"mig_adopt": "これを自分のウォレットにする",
|
"mig_adopt": "これを自分のウォレットにする",
|
||||||
"mig_adopt_now": "シードウォレットを採用",
|
"mig_adopt_now": "シードウォレットを採用",
|
||||||
"mig_adopting": "新しいウォレットをインストールして再スキャン中…",
|
"mig_adopting": "新しいウォレットをインストールして再スキャン中…",
|
||||||
|
|||||||
@@ -1163,6 +1163,22 @@
|
|||||||
"merge_description": "여러 UTXO를 단일 차폐 주소로 통합합니다. 지갑 크기를 줄이고 프라이버시를 향상시킵니다.",
|
"merge_description": "여러 UTXO를 단일 차폐 주소로 통합합니다. 지갑 크기를 줄이고 프라이버시를 향상시킵니다.",
|
||||||
"merge_fee_hint": "이 거래에 대한 네트워크 수수료입니다.",
|
"merge_fee_hint": "이 거래에 대한 네트워크 수수료입니다.",
|
||||||
"merge_funds": "자금 통합",
|
"merge_funds": "자금 통합",
|
||||||
|
"merge_into_one": "한 주소로",
|
||||||
|
"merge_keep_each": "각 주소에 유지",
|
||||||
|
"merge_keep_each_batches_fmt": "노트가 %d개를 넘는 주소는 여러 번에 나눠 병합됩니다. 확인된 후 다시 실행하면 나머지가 병합됩니다.",
|
||||||
|
"merge_keep_each_confirm_fmt": "노트 %d개를 주소 %d개에서 통합할까요? 각 주소는 자기 자금을 그대로 유지합니다. 수수료: 약 %s DRGX(배치마다 1회).",
|
||||||
|
"merge_keep_each_desc": "각 주소의 노트는 같은 주소로 다시 병합되므로, 다른 라벨로 나눠 둔 자금은 계속 나뉜 상태로 유지됩니다. 차폐 주소만 해당되며, 투명 자금은 차폐만 할 수 있습니다(한 주소로).",
|
||||||
|
"merge_keep_each_none": "모든 차폐 주소에 노트가 이미 하나뿐입니다. 통합할 것이 없습니다.",
|
||||||
|
"merge_keep_each_nothing_submitted": "아무것도 통합되지 않았습니다. 노드가 모든 병합을 거부했습니다.",
|
||||||
|
"merge_keep_each_row_fmt": "%s · 노트 %d개 · %s DRGX",
|
||||||
|
"merge_keep_each_run_again": "여러 번에 나눠 병합된 주소에는 배치마다 노트가 하나씩 남아 있습니다. 확인된 후 다시 실행하면 하나로 병합됩니다.",
|
||||||
|
"merge_keep_each_select": "통합할 주소를 하나 이상 선택하세요.",
|
||||||
|
"merge_keep_each_some_failed": "일부 병합에 실패했습니다. 위 목록을 확인하세요.",
|
||||||
|
"merge_keep_each_status_done": "%s — 완료: 노트 %d개를 %d번에 걸쳐 병합",
|
||||||
|
"merge_keep_each_status_failed": "%s — 실패: %s",
|
||||||
|
"merge_keep_each_status_not_started": "%s — 시작되지 않음: %s",
|
||||||
|
"merge_keep_each_status_running": "%s — 노트 %d개 병합 중(%d/%d 배치 완료)…",
|
||||||
|
"merge_keep_each_too_small": "너무 적어서 통합할 수 없습니다. 수수료가 이 자금을 모두 소모합니다.",
|
||||||
"merge_max_inputs": "배치당 최대 입력 수",
|
"merge_max_inputs": "배치당 최대 입력 수",
|
||||||
"merge_no_spendable": "아직 통합할 사용 가능한 입력이 없습니다.",
|
"merge_no_spendable": "아직 통합할 사용 가능한 입력이 없습니다.",
|
||||||
"merge_progress": "통합 중… 몇 분 정도 걸릴 수 있습니다. 이 창을 닫아도 됩니다.",
|
"merge_progress": "통합 중… 몇 분 정도 걸릴 수 있습니다. 이 창을 닫아도 됩니다.",
|
||||||
@@ -1175,6 +1191,7 @@
|
|||||||
"merge_src_transparent": "투명",
|
"merge_src_transparent": "투명",
|
||||||
"merge_started": "통합 작업이 시작되었습니다",
|
"merge_started": "통합 작업이 시작되었습니다",
|
||||||
"merge_title": "주소로 통합",
|
"merge_title": "주소로 통합",
|
||||||
|
"merge_where": "자금을 어디로 보낼까요?",
|
||||||
"mig_adopt": "이 지갑을 내 지갑으로 사용",
|
"mig_adopt": "이 지갑을 내 지갑으로 사용",
|
||||||
"mig_adopt_now": "시드 지갑 사용",
|
"mig_adopt_now": "시드 지갑 사용",
|
||||||
"mig_adopting": "새 지갑을 설치하고 재스캔하는 중…",
|
"mig_adopting": "새 지갑을 설치하고 재스캔하는 중…",
|
||||||
|
|||||||
@@ -1163,6 +1163,22 @@
|
|||||||
"merge_description": "Fundir múltiplos UTXOs em um único endereço blindado. Isso pode ajudar a reduzir o tamanho da carteira e melhorar a privacidade.",
|
"merge_description": "Fundir múltiplos UTXOs em um único endereço blindado. Isso pode ajudar a reduzir o tamanho da carteira e melhorar a privacidade.",
|
||||||
"merge_fee_hint": "Taxa de rede para esta transação.",
|
"merge_fee_hint": "Taxa de rede para esta transação.",
|
||||||
"merge_funds": "Fundir Fundos",
|
"merge_funds": "Fundir Fundos",
|
||||||
|
"merge_into_one": "Para um só endereço",
|
||||||
|
"merge_keep_each": "Manter em cada endereço",
|
||||||
|
"merge_keep_each_batches_fmt": "Endereços com mais de %d notas são mesclados em vários lotes; execute de novo depois que forem confirmados para mesclar o restante.",
|
||||||
|
"merge_keep_each_confirm_fmt": "Consolidar %d notas em %d endereços? Cada endereço mantém seus próprios fundos. Taxas: cerca de %s DRGX (uma por lote).",
|
||||||
|
"merge_keep_each_desc": "As notas de cada endereço são mescladas de volta nesse mesmo endereço, então os fundos que você mantém separados com rótulos diferentes continuam separados. Só endereços blindados — fundos transparentes só podem ser blindados (Para um só endereço).",
|
||||||
|
"merge_keep_each_none": "Cada endereço blindado já tem uma única nota — nada a consolidar.",
|
||||||
|
"merge_keep_each_nothing_submitted": "Nada foi consolidado — o nó recusou todas as mesclagens.",
|
||||||
|
"merge_keep_each_row_fmt": "%s · %d notas · %s DRGX",
|
||||||
|
"merge_keep_each_run_again": "Endereços mesclados em vários lotes agora têm uma nota por lote. Execute de novo depois que forem confirmados para mesclá-las em uma.",
|
||||||
|
"merge_keep_each_select": "Marque pelo menos um endereço para consolidar.",
|
||||||
|
"merge_keep_each_some_failed": "Algumas mesclagens falharam — veja a lista acima.",
|
||||||
|
"merge_keep_each_status_done": "%s — concluído: %d notas mescladas em %d lote(s)",
|
||||||
|
"merge_keep_each_status_failed": "%s — falhou: %s",
|
||||||
|
"merge_keep_each_status_not_started": "%s — não iniciado: %s",
|
||||||
|
"merge_keep_each_status_running": "%s — mesclando %d notas (%d de %d lotes concluídos)…",
|
||||||
|
"merge_keep_each_too_small": "Pequeno demais para consolidar: a taxa consumiria esses fundos.",
|
||||||
"merge_max_inputs": "Máx. de entradas por lote",
|
"merge_max_inputs": "Máx. de entradas por lote",
|
||||||
"merge_no_spendable": "Ainda não há entradas disponíveis para consolidar.",
|
"merge_no_spendable": "Ainda não há entradas disponíveis para consolidar.",
|
||||||
"merge_progress": "Consolidando… isso pode levar alguns minutos. Você pode fechar esta janela.",
|
"merge_progress": "Consolidando… isso pode levar alguns minutos. Você pode fechar esta janela.",
|
||||||
@@ -1175,6 +1191,7 @@
|
|||||||
"merge_src_transparent": "Transparente",
|
"merge_src_transparent": "Transparente",
|
||||||
"merge_started": "Operação de fusão iniciada",
|
"merge_started": "Operação de fusão iniciada",
|
||||||
"merge_title": "Fundir para Endereço",
|
"merge_title": "Fundir para Endereço",
|
||||||
|
"merge_where": "Para onde devem ir os fundos?",
|
||||||
"mig_adopt": "Tornar esta minha carteira",
|
"mig_adopt": "Tornar esta minha carteira",
|
||||||
"mig_adopt_now": "Adotar carteira semente",
|
"mig_adopt_now": "Adotar carteira semente",
|
||||||
"mig_adopting": "Instalando sua nova carteira e reescaneando…",
|
"mig_adopting": "Instalando sua nova carteira e reescaneando…",
|
||||||
|
|||||||
@@ -1163,6 +1163,22 @@
|
|||||||
"merge_description": "Объедините несколько UTXO в один экранированный адрес. Это может уменьшить размер кошелька и улучшить конфиденциальность.",
|
"merge_description": "Объедините несколько UTXO в один экранированный адрес. Это может уменьшить размер кошелька и улучшить конфиденциальность.",
|
||||||
"merge_fee_hint": "Сетевая комиссия за эту транзакцию.",
|
"merge_fee_hint": "Сетевая комиссия за эту транзакцию.",
|
||||||
"merge_funds": "Объединить средства",
|
"merge_funds": "Объединить средства",
|
||||||
|
"merge_into_one": "На один адрес",
|
||||||
|
"merge_keep_each": "Оставить в каждом адресе",
|
||||||
|
"merge_keep_each_batches_fmt": "Адреса с более чем %d заметками объединяются в несколько партий; запустите снова после их подтверждения, чтобы объединить остальное.",
|
||||||
|
"merge_keep_each_confirm_fmt": "Объединить %d заметок на %d адресах? Каждый адрес сохраняет свои средства. Комиссии: около %s DRGX (по одной на партию).",
|
||||||
|
"merge_keep_each_desc": "Заметки каждого адреса объединяются обратно в тот же адрес, поэтому средства, которые вы держите раздельно под разными метками, остаются раздельными. Только экранированные адреса — прозрачные средства можно только экранировать («На один адрес»).",
|
||||||
|
"merge_keep_each_none": "На каждом экранированном адресе уже одна заметка — объединять нечего.",
|
||||||
|
"merge_keep_each_nothing_submitted": "Ничего не объединено — узел отклонил все объединения.",
|
||||||
|
"merge_keep_each_row_fmt": "%s · заметок: %d · %s DRGX",
|
||||||
|
"merge_keep_each_run_again": "На адресах, объединённых в несколько партий, теперь по одной заметке на партию. Запустите снова после их подтверждения, чтобы объединить их в одну.",
|
||||||
|
"merge_keep_each_select": "Отметьте хотя бы один адрес для объединения.",
|
||||||
|
"merge_keep_each_some_failed": "Некоторые объединения не удались — см. список выше.",
|
||||||
|
"merge_keep_each_status_done": "%s — готово: объединено заметок: %d, партий: %d",
|
||||||
|
"merge_keep_each_status_failed": "%s — ошибка: %s",
|
||||||
|
"merge_keep_each_status_not_started": "%s — не запущено: %s",
|
||||||
|
"merge_keep_each_status_running": "%s — объединение %d заметок (готово партий: %d из %d)…",
|
||||||
|
"merge_keep_each_too_small": "Слишком мало для объединения: комиссия съест эти средства.",
|
||||||
"merge_max_inputs": "Макс. входов за партию",
|
"merge_max_inputs": "Макс. входов за партию",
|
||||||
"merge_no_spendable": "Пока нет доступных для расходования входов для объединения.",
|
"merge_no_spendable": "Пока нет доступных для расходования входов для объединения.",
|
||||||
"merge_progress": "Объединение… это может занять несколько минут. Вы можете закрыть это окно.",
|
"merge_progress": "Объединение… это может занять несколько минут. Вы можете закрыть это окно.",
|
||||||
@@ -1175,6 +1191,7 @@
|
|||||||
"merge_src_transparent": "Прозрачные",
|
"merge_src_transparent": "Прозрачные",
|
||||||
"merge_started": "Операция объединения начата",
|
"merge_started": "Операция объединения начата",
|
||||||
"merge_title": "Объединить на адрес",
|
"merge_title": "Объединить на адрес",
|
||||||
|
"merge_where": "Куда направить средства?",
|
||||||
"mig_adopt": "Сделать это моим кошельком",
|
"mig_adopt": "Сделать это моим кошельком",
|
||||||
"mig_adopt_now": "Принять сид-кошелёк",
|
"mig_adopt_now": "Принять сид-кошелёк",
|
||||||
"mig_adopting": "Установка вашего нового кошелька и пересканирование…",
|
"mig_adopting": "Установка вашего нового кошелька и пересканирование…",
|
||||||
|
|||||||
@@ -1163,6 +1163,22 @@
|
|||||||
"merge_description": "将多个 UTXO 合并到一个屏蔽地址。这可以帮助减小钱包大小并提高隐私性。",
|
"merge_description": "将多个 UTXO 合并到一个屏蔽地址。这可以帮助减小钱包大小并提高隐私性。",
|
||||||
"merge_fee_hint": "此交易的网络手续费。",
|
"merge_fee_hint": "此交易的网络手续费。",
|
||||||
"merge_funds": "合并资金",
|
"merge_funds": "合并资金",
|
||||||
|
"merge_into_one": "合并到一个地址",
|
||||||
|
"merge_keep_each": "保留在各自地址",
|
||||||
|
"merge_keep_each_batches_fmt": "超过 %d 张票据的地址会分多批合并;确认后再运行一次即可合并其余部分。",
|
||||||
|
"merge_keep_each_confirm_fmt": "整合 %d 张票据,涉及 %d 个地址?每个地址保留各自的资金。手续费:约 %s DRGX(每批一次)。",
|
||||||
|
"merge_keep_each_desc": "每个地址的票据会合并回该地址本身,因此您用不同标签分开存放的资金仍保持分开。仅限屏蔽地址——透明资金只能被屏蔽(合并到一个地址)。",
|
||||||
|
"merge_keep_each_none": "每个屏蔽地址都只有一张票据——无需整合。",
|
||||||
|
"merge_keep_each_nothing_submitted": "没有任何整合——节点拒绝了所有合并。",
|
||||||
|
"merge_keep_each_row_fmt": "%s · %d 张票据 · %s DRGX",
|
||||||
|
"merge_keep_each_run_again": "分多批合并的地址现在每批各有一张票据。确认后再运行一次,即可把它们合并为一张。",
|
||||||
|
"merge_keep_each_select": "请至少勾选一个要整合的地址。",
|
||||||
|
"merge_keep_each_some_failed": "部分合并失败——请查看上方列表。",
|
||||||
|
"merge_keep_each_status_done": "%s——完成:已合并 %d 张票据,共 %d 批",
|
||||||
|
"merge_keep_each_status_failed": "%s——失败:%s",
|
||||||
|
"merge_keep_each_status_not_started": "%s——未开始:%s",
|
||||||
|
"merge_keep_each_status_running": "%s——正在合并 %d 张票据(已完成 %d/%d 批)…",
|
||||||
|
"merge_keep_each_too_small": "金额太小,无法整合:手续费会耗尽这些资金。",
|
||||||
"merge_max_inputs": "每批最大输入数",
|
"merge_max_inputs": "每批最大输入数",
|
||||||
"merge_no_spendable": "暂无可合并的可用输入。",
|
"merge_no_spendable": "暂无可合并的可用输入。",
|
||||||
"merge_progress": "正在合并…这可能需要几分钟。您可以关闭此窗口。",
|
"merge_progress": "正在合并…这可能需要几分钟。您可以关闭此窗口。",
|
||||||
@@ -1175,6 +1191,7 @@
|
|||||||
"merge_src_transparent": "透明",
|
"merge_src_transparent": "透明",
|
||||||
"merge_started": "合并操作已开始",
|
"merge_started": "合并操作已开始",
|
||||||
"merge_title": "合并到地址",
|
"merge_title": "合并到地址",
|
||||||
|
"merge_where": "资金应转到哪里?",
|
||||||
"mig_adopt": "将其设为我的钱包",
|
"mig_adopt": "将其设为我的钱包",
|
||||||
"mig_adopt_now": "采用助记词钱包",
|
"mig_adopt_now": "采用助记词钱包",
|
||||||
"mig_adopting": "正在安装您的新钱包并重新扫描…",
|
"mig_adopting": "正在安装您的新钱包并重新扫描…",
|
||||||
|
|||||||
@@ -25,6 +25,176 @@ def load_english_keys():
|
|||||||
|
|
||||||
# All translations for the 158 missing keys
|
# All translations for the 158 missing keys
|
||||||
TRANSLATIONS = {
|
TRANSLATIONS = {
|
||||||
|
"merge_where": {
|
||||||
|
"de": "Wohin sollen die Mittel gehen?",
|
||||||
|
"es": "¿Adónde deben ir los fondos?",
|
||||||
|
"fr": "Où doivent aller les fonds ?",
|
||||||
|
"pt": "Para onde devem ir os fundos?",
|
||||||
|
"ru": "Куда направить средства?",
|
||||||
|
"zh": "资金应转到哪里?",
|
||||||
|
"ja": "資金の移動先",
|
||||||
|
"ko": "자금을 어디로 보낼까요?",
|
||||||
|
},
|
||||||
|
"merge_into_one": {
|
||||||
|
"de": "In eine Adresse",
|
||||||
|
"es": "A una sola dirección",
|
||||||
|
"fr": "Vers une seule adresse",
|
||||||
|
"pt": "Para um só endereço",
|
||||||
|
"ru": "На один адрес",
|
||||||
|
"zh": "合并到一个地址",
|
||||||
|
"ja": "1 つのアドレスへ",
|
||||||
|
"ko": "한 주소로",
|
||||||
|
},
|
||||||
|
"merge_keep_each": {
|
||||||
|
"de": "In jeder Adresse belassen",
|
||||||
|
"es": "Mantener en cada dirección",
|
||||||
|
"fr": "Garder dans chaque adresse",
|
||||||
|
"pt": "Manter em cada endereço",
|
||||||
|
"ru": "Оставить в каждом адресе",
|
||||||
|
"zh": "保留在各自地址",
|
||||||
|
"ja": "各アドレスに残す",
|
||||||
|
"ko": "각 주소에 유지",
|
||||||
|
},
|
||||||
|
"merge_keep_each_desc": {
|
||||||
|
"de": "Die Notes jeder Adresse werden in genau diese Adresse zurückgeführt, sodass Mittel, die Sie unter verschiedenen Bezeichnungen getrennt halten, getrennt bleiben. Nur abgeschirmte Adressen – transparente Mittel können nur abgeschirmt werden (In eine Adresse).",
|
||||||
|
"es": "Las notas de cada dirección se fusionan de nuevo en esa misma dirección, así que los fondos que mantienes separados con distintas etiquetas siguen separados. Solo direcciones blindadas: los fondos transparentes solo pueden blindarse (A una sola dirección).",
|
||||||
|
"fr": "Les notes de chaque adresse sont refusionnées dans cette même adresse : les fonds que vous gardez séparés sous des libellés différents restent séparés. Adresses blindées uniquement — les fonds transparents ne peuvent qu'être blindés (Vers une seule adresse).",
|
||||||
|
"pt": "As notas de cada endereço são mescladas de volta nesse mesmo endereço, então os fundos que você mantém separados com rótulos diferentes continuam separados. Só endereços blindados — fundos transparentes só podem ser blindados (Para um só endereço).",
|
||||||
|
"ru": "Заметки каждого адреса объединяются обратно в тот же адрес, поэтому средства, которые вы держите раздельно под разными метками, остаются раздельными. Только экранированные адреса — прозрачные средства можно только экранировать («На один адрес»).",
|
||||||
|
"zh": "每个地址的票据会合并回该地址本身,因此您用不同标签分开存放的资金仍保持分开。仅限屏蔽地址——透明资金只能被屏蔽(合并到一个地址)。",
|
||||||
|
"ja": "各アドレスのノートはそのアドレス自身に統合されるため、ラベルごとに分けている資金は分かれたままです。シールドアドレスのみ対象です。透明アドレスの資金はシールドすることしかできません(1 つのアドレスへ)。",
|
||||||
|
"ko": "각 주소의 노트는 같은 주소로 다시 병합되므로, 다른 라벨로 나눠 둔 자금은 계속 나뉜 상태로 유지됩니다. 차폐 주소만 해당되며, 투명 자금은 차폐만 할 수 있습니다(한 주소로).",
|
||||||
|
},
|
||||||
|
"merge_keep_each_none": {
|
||||||
|
"de": "Jede abgeschirmte Adresse hat bereits nur eine Note – nichts zu konsolidieren.",
|
||||||
|
"es": "Cada dirección blindada ya tiene una sola nota: no hay nada que consolidar.",
|
||||||
|
"fr": "Chaque adresse blindée ne contient déjà qu'une seule note — rien à consolider.",
|
||||||
|
"pt": "Cada endereço blindado já tem uma única nota — nada a consolidar.",
|
||||||
|
"ru": "На каждом экранированном адресе уже одна заметка — объединять нечего.",
|
||||||
|
"zh": "每个屏蔽地址都只有一张票据——无需整合。",
|
||||||
|
"ja": "すべてのシールドアドレスのノートはすでに 1 つです。統合するものはありません。",
|
||||||
|
"ko": "모든 차폐 주소에 노트가 이미 하나뿐입니다. 통합할 것이 없습니다.",
|
||||||
|
},
|
||||||
|
"merge_keep_each_row_fmt": {
|
||||||
|
"de": "%s · %d Notes · %s DRGX",
|
||||||
|
"es": "%s · %d notas · %s DRGX",
|
||||||
|
"fr": "%s · %d notes · %s DRGX",
|
||||||
|
"pt": "%s · %d notas · %s DRGX",
|
||||||
|
"ru": "%s · заметок: %d · %s DRGX",
|
||||||
|
"zh": "%s · %d 张票据 · %s DRGX",
|
||||||
|
"ja": "%s · ノート %d 件 · %s DRGX",
|
||||||
|
"ko": "%s · 노트 %d개 · %s DRGX",
|
||||||
|
},
|
||||||
|
"merge_keep_each_too_small": {
|
||||||
|
"de": "Zu klein zum Konsolidieren: Die Gebühr würde diese Mittel aufbrauchen.",
|
||||||
|
"es": "Demasiado poco para consolidar: la comisión se llevaría estos fondos.",
|
||||||
|
"fr": "Trop faible pour être consolidé : les frais absorberaient ces fonds.",
|
||||||
|
"pt": "Pequeno demais para consolidar: a taxa consumiria esses fundos.",
|
||||||
|
"ru": "Слишком мало для объединения: комиссия съест эти средства.",
|
||||||
|
"zh": "金额太小,无法整合:手续费会耗尽这些资金。",
|
||||||
|
"ja": "少なすぎて統合できません。手数料でこの資金がなくなります。",
|
||||||
|
"ko": "너무 적어서 통합할 수 없습니다. 수수료가 이 자금을 모두 소모합니다.",
|
||||||
|
},
|
||||||
|
"merge_keep_each_batches_fmt": {
|
||||||
|
"de": "Adressen mit mehr als %d Notes werden in mehreren Durchgängen zusammengeführt; führen Sie es nach deren Bestätigung erneut aus, um den Rest zusammenzuführen.",
|
||||||
|
"es": "Las direcciones con más de %d notas se fusionan en varios lotes; vuelve a ejecutarlo cuando se confirmen para fusionar el resto.",
|
||||||
|
"fr": "Les adresses de plus de %d notes sont fusionnées en plusieurs lots ; relancez l'opération une fois ceux-ci confirmés pour fusionner le reste.",
|
||||||
|
"pt": "Endereços com mais de %d notas são mesclados em vários lotes; execute de novo depois que forem confirmados para mesclar o restante.",
|
||||||
|
"ru": "Адреса с более чем %d заметками объединяются в несколько партий; запустите снова после их подтверждения, чтобы объединить остальное.",
|
||||||
|
"zh": "超过 %d 张票据的地址会分多批合并;确认后再运行一次即可合并其余部分。",
|
||||||
|
"ja": "ノートが %d 件を超えるアドレスは複数回に分けて統合されます。承認後にもう一度実行すると残りが統合されます。",
|
||||||
|
"ko": "노트가 %d개를 넘는 주소는 여러 번에 나눠 병합됩니다. 확인된 후 다시 실행하면 나머지가 병합됩니다.",
|
||||||
|
},
|
||||||
|
"merge_keep_each_confirm_fmt": {
|
||||||
|
"de": "%d Notes in %d Adressen konsolidieren? Jede Adresse behält ihre eigenen Mittel. Gebühren: etwa %s DRGX (eine pro Durchgang).",
|
||||||
|
"es": "¿Consolidar %d notas en %d direcciones? Cada dirección conserva sus propios fondos. Comisiones: unos %s DRGX (una por lote).",
|
||||||
|
"fr": "Consolider %d notes dans %d adresses ? Chaque adresse garde ses propres fonds. Frais : environ %s DRGX (un par lot).",
|
||||||
|
"pt": "Consolidar %d notas em %d endereços? Cada endereço mantém seus próprios fundos. Taxas: cerca de %s DRGX (uma por lote).",
|
||||||
|
"ru": "Объединить %d заметок на %d адресах? Каждый адрес сохраняет свои средства. Комиссии: около %s DRGX (по одной на партию).",
|
||||||
|
"zh": "整合 %d 张票据,涉及 %d 个地址?每个地址保留各自的资金。手续费:约 %s DRGX(每批一次)。",
|
||||||
|
"ja": "%d 件のノートを %d 個のアドレスで統合しますか?各アドレスの資金はそのまま残ります。手数料:約 %s DRGX(1 回ごとに 1 件)。",
|
||||||
|
"ko": "노트 %d개를 주소 %d개에서 통합할까요? 각 주소는 자기 자금을 그대로 유지합니다. 수수료: 약 %s DRGX(배치마다 1회).",
|
||||||
|
},
|
||||||
|
"merge_keep_each_select": {
|
||||||
|
"de": "Wählen Sie mindestens eine Adresse zum Konsolidieren aus.",
|
||||||
|
"es": "Marca al menos una dirección para consolidar.",
|
||||||
|
"fr": "Cochez au moins une adresse à consolider.",
|
||||||
|
"pt": "Marque pelo menos um endereço para consolidar.",
|
||||||
|
"ru": "Отметьте хотя бы один адрес для объединения.",
|
||||||
|
"zh": "请至少勾选一个要整合的地址。",
|
||||||
|
"ja": "統合するアドレスを 1 つ以上選んでください。",
|
||||||
|
"ko": "통합할 주소를 하나 이상 선택하세요.",
|
||||||
|
},
|
||||||
|
"merge_keep_each_nothing_submitted": {
|
||||||
|
"de": "Nichts wurde konsolidiert – der Node hat jede Zusammenführung abgelehnt.",
|
||||||
|
"es": "No se consolidó nada: el nodo rechazó todas las fusiones.",
|
||||||
|
"fr": "Rien n'a été consolidé — le nœud a refusé chaque fusion.",
|
||||||
|
"pt": "Nada foi consolidado — o nó recusou todas as mesclagens.",
|
||||||
|
"ru": "Ничего не объединено — узел отклонил все объединения.",
|
||||||
|
"zh": "没有任何整合——节点拒绝了所有合并。",
|
||||||
|
"ja": "何も統合されませんでした。ノードがすべての統合を拒否しました。",
|
||||||
|
"ko": "아무것도 통합되지 않았습니다. 노드가 모든 병합을 거부했습니다.",
|
||||||
|
},
|
||||||
|
"merge_keep_each_some_failed": {
|
||||||
|
"de": "Einige Zusammenführungen sind fehlgeschlagen – siehe Liste oben.",
|
||||||
|
"es": "Algunas fusiones fallaron; consulta la lista de arriba.",
|
||||||
|
"fr": "Certaines fusions ont échoué — voir la liste ci-dessus.",
|
||||||
|
"pt": "Algumas mesclagens falharam — veja a lista acima.",
|
||||||
|
"ru": "Некоторые объединения не удались — см. список выше.",
|
||||||
|
"zh": "部分合并失败——请查看上方列表。",
|
||||||
|
"ja": "一部の統合に失敗しました。上の一覧を確認してください。",
|
||||||
|
"ko": "일부 병합에 실패했습니다. 위 목록을 확인하세요.",
|
||||||
|
},
|
||||||
|
"merge_keep_each_status_not_started": {
|
||||||
|
"de": "%s – nicht gestartet: %s",
|
||||||
|
"es": "%s: no iniciado: %s",
|
||||||
|
"fr": "%s — non démarré : %s",
|
||||||
|
"pt": "%s — não iniciado: %s",
|
||||||
|
"ru": "%s — не запущено: %s",
|
||||||
|
"zh": "%s——未开始:%s",
|
||||||
|
"ja": "%s — 開始されませんでした:%s",
|
||||||
|
"ko": "%s — 시작되지 않음: %s",
|
||||||
|
},
|
||||||
|
"merge_keep_each_status_failed": {
|
||||||
|
"de": "%s – fehlgeschlagen: %s",
|
||||||
|
"es": "%s: falló: %s",
|
||||||
|
"fr": "%s — échec : %s",
|
||||||
|
"pt": "%s — falhou: %s",
|
||||||
|
"ru": "%s — ошибка: %s",
|
||||||
|
"zh": "%s——失败:%s",
|
||||||
|
"ja": "%s — 失敗:%s",
|
||||||
|
"ko": "%s — 실패: %s",
|
||||||
|
},
|
||||||
|
"merge_keep_each_status_done": {
|
||||||
|
"de": "%s – fertig: %d Notes in %d Durchgang/Durchgängen zusammengeführt",
|
||||||
|
"es": "%s: listo: %d notas fusionadas en %d lote(s)",
|
||||||
|
"fr": "%s — terminé : %d notes fusionnées en %d lot(s)",
|
||||||
|
"pt": "%s — concluído: %d notas mescladas em %d lote(s)",
|
||||||
|
"ru": "%s — готово: объединено заметок: %d, партий: %d",
|
||||||
|
"zh": "%s——完成:已合并 %d 张票据,共 %d 批",
|
||||||
|
"ja": "%s — 完了:%d 件のノートを %d 回で統合",
|
||||||
|
"ko": "%s — 완료: 노트 %d개를 %d번에 걸쳐 병합",
|
||||||
|
},
|
||||||
|
"merge_keep_each_status_running": {
|
||||||
|
"de": "%s – %d Notes werden zusammengeführt (%d von %d Durchgängen fertig)…",
|
||||||
|
"es": "%s: fusionando %d notas (%d de %d lotes listos)…",
|
||||||
|
"fr": "%s — fusion de %d notes (%d lots sur %d terminés)…",
|
||||||
|
"pt": "%s — mesclando %d notas (%d de %d lotes concluídos)…",
|
||||||
|
"ru": "%s — объединение %d заметок (готово партий: %d из %d)…",
|
||||||
|
"zh": "%s——正在合并 %d 张票据(已完成 %d/%d 批)…",
|
||||||
|
"ja": "%s — %d 件のノートを統合中(%d / %d 回完了)…",
|
||||||
|
"ko": "%s — 노트 %d개 병합 중(%d/%d 배치 완료)…",
|
||||||
|
},
|
||||||
|
"merge_keep_each_run_again": {
|
||||||
|
"de": "Adressen, die in mehreren Durchgängen zusammengeführt wurden, haben jetzt eine Note pro Durchgang. Führen Sie dies nach deren Bestätigung erneut aus, um sie zu einer zusammenzuführen.",
|
||||||
|
"es": "Las direcciones fusionadas en varios lotes tienen ahora una nota por lote. Vuelve a ejecutarlo cuando se confirmen para fusionarlas en una.",
|
||||||
|
"fr": "Les adresses fusionnées en plusieurs lots contiennent maintenant une note par lot. Relancez l'opération une fois ceux-ci confirmés pour les fusionner en une seule.",
|
||||||
|
"pt": "Endereços mesclados em vários lotes agora têm uma nota por lote. Execute de novo depois que forem confirmados para mesclá-las em uma.",
|
||||||
|
"ru": "На адресах, объединённых в несколько партий, теперь по одной заметке на партию. Запустите снова после их подтверждения, чтобы объединить их в одну.",
|
||||||
|
"zh": "分多批合并的地址现在每批各有一张票据。确认后再运行一次,即可把它们合并为一张。",
|
||||||
|
"ja": "複数回に分けて統合したアドレスには、回数分のノートが残っています。承認後にもう一度実行すると 1 つにまとめられます。",
|
||||||
|
"ko": "여러 번에 나눠 병합된 주소에는 배치마다 노트가 하나씩 남아 있습니다. 확인된 후 다시 실행하면 하나로 병합됩니다.",
|
||||||
|
},
|
||||||
"mining_addr_explainer": {
|
"mining_addr_explainer": {
|
||||||
"de": "Empfängt nur Solo-Mining-Belohnungen – das Netzwerk lehnt Zahlungen an diese Adresse ab. Belohnungen werden automatisch abgeschirmt, sobald sie gereift sind.",
|
"de": "Empfängt nur Solo-Mining-Belohnungen – das Netzwerk lehnt Zahlungen an diese Adresse ab. Belohnungen werden automatisch abgeschirmt, sobald sie gereift sind.",
|
||||||
"es": "Solo recibe recompensas de minería en solitario; la red rechaza los pagos a esta dirección. Las recompensas se blindan automáticamente cuando maduran.",
|
"es": "Solo recibe recompensas de minería en solitario; la red rechaza los pagos a esta dirección. Las recompensas se blindan automáticamente cuando maduran.",
|
||||||
|
|||||||
@@ -401,6 +401,10 @@ void App::buildSweepCatalog()
|
|||||||
add("modal-merge", ui::NavPage::Send,
|
add("modal-merge", ui::NavPage::Send,
|
||||||
[](App&) { ui::ShieldDialog::showMerge(); },
|
[](App&) { ui::ShieldDialog::showMerge(); },
|
||||||
[](App&) { ui::ShieldDialog::hide(); });
|
[](App&) { ui::ShieldDialog::hide(); });
|
||||||
|
// Consolidate (wallet-size preset): keeps each address's funds in that address by default.
|
||||||
|
add("modal-consolidate", ui::NavPage::Send,
|
||||||
|
[](App&) { ui::ShieldDialog::showConsolidate(); },
|
||||||
|
[](App&) { ui::ShieldDialog::hide(); });
|
||||||
// z->t transfer so the (converted) deshielding DialogWarningHeader renders.
|
// z->t transfer so the (converted) deshielding DialogWarningHeader renders.
|
||||||
add("modal-transfer", ui::NavPage::Overview,
|
add("modal-transfer", ui::NavPage::Overview,
|
||||||
[](App& a) {
|
[](App& a) {
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
// DragonX Wallet - ImGui Edition
|
||||||
|
// Copyright 2024-2026 The Hush Developers
|
||||||
|
// Released under the GPLv3
|
||||||
|
|
||||||
|
#include "note_consolidation.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <map>
|
||||||
|
|
||||||
|
namespace dragonx {
|
||||||
|
namespace services {
|
||||||
|
|
||||||
|
std::vector<NoteGroup> groupSpendableNotes(const nlohmann::json& zListUnspent)
|
||||||
|
{
|
||||||
|
std::map<std::string, NoteGroup> byAddress;
|
||||||
|
if (zListUnspent.is_array()) {
|
||||||
|
for (const auto& n : zListUnspent) {
|
||||||
|
if (!n.is_object()) continue;
|
||||||
|
const std::string address = n.value("address", std::string());
|
||||||
|
if (address.empty()) continue;
|
||||||
|
if (n.value("confirmations", 0) < 1) continue; // z_mergetoaddress spends confirmed notes only
|
||||||
|
if (!n.value("spendable", true)) continue; // watch-only: no key to spend it
|
||||||
|
NoteGroup& g = byAddress[address];
|
||||||
|
g.address = address;
|
||||||
|
++g.notes;
|
||||||
|
g.amount += n.value("amount", 0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::vector<NoteGroup> out;
|
||||||
|
out.reserve(byAddress.size());
|
||||||
|
for (auto& kv : byAddress) out.push_back(std::move(kv.second));
|
||||||
|
std::sort(out.begin(), out.end(), [](const NoteGroup& a, const NoteGroup& b) {
|
||||||
|
if (a.notes != b.notes) return a.notes > b.notes;
|
||||||
|
return a.address < b.address;
|
||||||
|
});
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool worthConsolidating(const NoteGroup& group, double fee)
|
||||||
|
{
|
||||||
|
// The daemon rejects a merge whose fee is more than what it nets (merged value < 2 x fee).
|
||||||
|
return group.notes >= 2 && group.amount > 2.0 * fee;
|
||||||
|
}
|
||||||
|
|
||||||
|
int estimateBatches(int notes, int limit)
|
||||||
|
{
|
||||||
|
if (notes < 2) return 0;
|
||||||
|
if (limit < 2) limit = 2;
|
||||||
|
return notes / limit + ((notes % limit) >= 2 ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
AddressRun consolidateInPlace(const std::string& address, const MergeCall& call, int maxBatches)
|
||||||
|
{
|
||||||
|
AddressRun run;
|
||||||
|
run.address = address;
|
||||||
|
for (int b = 0; b < maxBatches; ++b) {
|
||||||
|
MergeCallResult r = call(address);
|
||||||
|
if (!r.error.empty()) {
|
||||||
|
run.error = r.error; // earlier batches (if any) are already submitted and stand
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
run.opids.push_back(r.opid);
|
||||||
|
run.notesMerged += r.merging;
|
||||||
|
run.remaining = r.remaining;
|
||||||
|
if (r.remaining < 2) break; // one leftover note can't be merged into its own address
|
||||||
|
}
|
||||||
|
return run;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace services
|
||||||
|
} // namespace dragonx
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
// DragonX Wallet - ImGui Edition
|
||||||
|
// Copyright 2024-2026 The Hush Developers
|
||||||
|
// Released under the GPLv3
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
// Consolidating notes WITHOUT moving funds between addresses: each address's notes are merged back into
|
||||||
|
// that same address (z_mergetoaddress [addr] addr), so funds kept apart under different labels stay apart
|
||||||
|
// while the wallet sheds notes (fewer notes = smaller wallet, faster witness rebuilds and scans).
|
||||||
|
// Shielded only: a transparent address can't receive a non-coinbase output on DragonX (private chain).
|
||||||
|
// Pure apart from the merge call it is handed, so the test target can drive it.
|
||||||
|
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace dragonx {
|
||||||
|
namespace services {
|
||||||
|
|
||||||
|
// One shielded address's spendable notes (confirmed, spendable), from z_listunspent.
|
||||||
|
struct NoteGroup {
|
||||||
|
std::string address;
|
||||||
|
int notes = 0;
|
||||||
|
double amount = 0.0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Spendable shielded notes grouped by address, most notes first. Transparent UTXOs and unconfirmed or
|
||||||
|
// watch-only notes are left out (z_mergetoaddress only spends confirmed notes it has keys for).
|
||||||
|
std::vector<NoteGroup> groupSpendableNotes(const nlohmann::json& zListUnspent);
|
||||||
|
|
||||||
|
// Worth consolidating in place: at least two notes, and more than one batch's fee to work with.
|
||||||
|
bool worthConsolidating(const NoteGroup& group, double fee);
|
||||||
|
|
||||||
|
// Merge calls needed to bring `notes` down when each call takes at most `limit` of them. A lone leftover
|
||||||
|
// note isn't a call (the daemon refuses to merge one note into its own address).
|
||||||
|
int estimateBatches(int notes, int limit);
|
||||||
|
|
||||||
|
// What one z_mergetoaddress call reported.
|
||||||
|
struct MergeCallResult {
|
||||||
|
std::string opid;
|
||||||
|
int merging = 0; // notes this call is merging
|
||||||
|
int remaining = 0; // notes of the address it left for a later call
|
||||||
|
std::string error; // non-empty: the call failed (nothing was submitted)
|
||||||
|
};
|
||||||
|
using MergeCall = std::function<MergeCallResult(const std::string& address)>;
|
||||||
|
|
||||||
|
// The outcome for one address.
|
||||||
|
struct AddressRun {
|
||||||
|
std::string address;
|
||||||
|
std::vector<std::string> opids; // one per submitted batch
|
||||||
|
int notesMerged = 0;
|
||||||
|
int remaining = 0; // notes still left after the last batch
|
||||||
|
std::string error; // the call that stopped it, if any (earlier batches still went out)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Submit merges of `address` into itself until fewer than two of its notes are left unmerged. The daemon
|
||||||
|
// locks a batch's notes before the call returns, so each call takes the next batch. At most maxBatches.
|
||||||
|
AddressRun consolidateInPlace(const std::string& address, const MergeCall& call, int maxBatches = 20);
|
||||||
|
|
||||||
|
} // namespace services
|
||||||
|
} // namespace dragonx
|
||||||
@@ -8,12 +8,15 @@
|
|||||||
#include "../../data/wallet_state.h"
|
#include "../../data/wallet_state.h"
|
||||||
#include "../../rpc/rpc_client.h"
|
#include "../../rpc/rpc_client.h"
|
||||||
#include "../../rpc/rpc_worker.h"
|
#include "../../rpc/rpc_worker.h"
|
||||||
|
#include "../../services/note_consolidation.h"
|
||||||
#include "../../util/i18n.h"
|
#include "../../util/i18n.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"
|
||||||
#include "imgui.h"
|
#include "imgui.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <set>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
@@ -44,6 +47,18 @@ static bool s_scope_loaded = false;
|
|||||||
static int s_t_count = 0, s_z_count = 0;
|
static int s_t_count = 0, s_z_count = 0;
|
||||||
static double s_t_amount = 0.0, s_z_amount = 0.0;
|
static double s_t_amount = 0.0, s_z_amount = 0.0;
|
||||||
static bool s_creating_addr = false; // z_getnewaddress in flight (empty state)
|
static bool s_creating_addr = false; // z_getnewaddress in flight (empty state)
|
||||||
|
// "Keep each address's funds in that address": every ticked address's notes merge back into it, so
|
||||||
|
// separately labelled funds stay apart. Shielded only (a transparent address can't take change).
|
||||||
|
static bool s_per_address = false;
|
||||||
|
static std::vector<services::NoteGroup> s_groups; // spendable shielded notes by address (scope load)
|
||||||
|
static std::set<std::string> s_selected; // addresses ticked for per-address consolidation
|
||||||
|
struct PerAddressRun {
|
||||||
|
services::AddressRun run;
|
||||||
|
std::vector<int> state; // per opid: 0 = running, 1 = done, 2 = failed
|
||||||
|
std::string failure; // first failed op's message
|
||||||
|
};
|
||||||
|
static std::vector<PerAddressRun> s_runs; // after submit
|
||||||
|
static bool s_runs_submitting = false;
|
||||||
|
|
||||||
static void resetTransient()
|
static void resetTransient()
|
||||||
{
|
{
|
||||||
@@ -53,6 +68,8 @@ static void resetTransient()
|
|||||||
s_status_message.clear();
|
s_status_message.clear();
|
||||||
s_operation_id.clear();
|
s_operation_id.clear();
|
||||||
s_creating_addr = false;
|
s_creating_addr = false;
|
||||||
|
s_runs.clear();
|
||||||
|
s_runs_submitting = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Count + sum spendable transparent UTXOs and shielded notes so the user can see the scope of a
|
// Count + sum spendable transparent UTXOs and shielded notes so the user can see the scope of a
|
||||||
@@ -63,6 +80,7 @@ static void loadScope(App* app)
|
|||||||
s_scope_loading = true; s_scope_loaded = false;
|
s_scope_loading = true; s_scope_loaded = false;
|
||||||
app->worker()->post([app, rpc = app->rpc()]() -> rpc::RPCWorker::MainCb {
|
app->worker()->post([app, rpc = app->rpc()]() -> rpc::RPCWorker::MainCb {
|
||||||
int tC = 0, zC = 0; double tA = 0.0, zA = 0.0; std::string error;
|
int tC = 0, zC = 0; double tA = 0.0, zA = 0.0; std::string error;
|
||||||
|
std::vector<services::NoteGroup> groups;
|
||||||
try {
|
try {
|
||||||
rpc::RPCClient::TraceScope trace("Shield dialog / Scope count");
|
rpc::RPCClient::TraceScope trace("Shield dialog / Scope count");
|
||||||
nlohmann::json us = rpc->call("listunspent", nlohmann::json::array({0}));
|
nlohmann::json us = rpc->call("listunspent", nlohmann::json::array({0}));
|
||||||
@@ -73,10 +91,15 @@ static void loadScope(App* app)
|
|||||||
if (zs.is_array()) for (const auto& z : zs) {
|
if (zs.is_array()) for (const auto& z : zs) {
|
||||||
if (z.value("confirmations", 0) >= 1) { ++zC; zA += z.value("amount", 0.0); }
|
if (z.value("confirmations", 0) >= 1) { ++zC; zA += z.value("amount", 0.0); }
|
||||||
}
|
}
|
||||||
|
groups = services::groupSpendableNotes(zs);
|
||||||
} catch (const std::exception& e) { error = e.what(); }
|
} catch (const std::exception& e) { error = e.what(); }
|
||||||
return [tC, zC, tA, zA, error]() {
|
return [tC, zC, tA, zA, error, groups]() {
|
||||||
s_scope_loading = false; s_scope_loaded = error.empty();
|
s_scope_loading = false; s_scope_loaded = error.empty();
|
||||||
s_t_count = tC; s_z_count = zC; s_t_amount = tA; s_z_amount = zA;
|
s_t_count = tC; s_z_count = zC; s_t_amount = tA; s_z_amount = zA;
|
||||||
|
s_groups = groups;
|
||||||
|
s_selected.clear(); // every address worth it, ticked by default
|
||||||
|
for (const auto& g : s_groups)
|
||||||
|
if (services::worthConsolidating(g, s_fee)) s_selected.insert(g.address);
|
||||||
// Clamp the source to what actually has inputs (unless the user is mid-op).
|
// Clamp the source to what actually has inputs (unless the user is mid-op).
|
||||||
const bool tOk = tC > 0, zOk = zC > 0;
|
const bool tOk = tC > 0, zOk = zC > 0;
|
||||||
if (!s_operation_pending) {
|
if (!s_operation_pending) {
|
||||||
@@ -108,6 +131,9 @@ void ShieldDialog::show(Mode mode)
|
|||||||
s_scope_loaded = false; s_scope_loading = false;
|
s_scope_loaded = false; s_scope_loading = false;
|
||||||
s_t_count = s_z_count = 0; s_t_amount = s_z_amount = 0.0;
|
s_t_count = s_z_count = 0; s_t_amount = s_z_amount = 0.0;
|
||||||
s_last_poll = 0.0;
|
s_last_poll = 0.0;
|
||||||
|
s_per_address = false;
|
||||||
|
s_groups.clear();
|
||||||
|
s_selected.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ShieldDialog::showShieldCoinbase(const std::string& fromAddress)
|
void ShieldDialog::showShieldCoinbase(const std::string& fromAddress)
|
||||||
@@ -128,6 +154,7 @@ void ShieldDialog::showConsolidate()
|
|||||||
show(Mode::MergeToAddress);
|
show(Mode::MergeToAddress);
|
||||||
s_consolidate = true;
|
s_consolidate = true;
|
||||||
s_src = 1; // wallet-bloat consolidation targets shielded notes (witness bloat)
|
s_src = 1; // wallet-bloat consolidation targets shielded notes (witness bloat)
|
||||||
|
s_per_address = true; // shed notes without moving funds between (labelled) addresses
|
||||||
}
|
}
|
||||||
|
|
||||||
void ShieldDialog::hide()
|
void ShieldDialog::hide()
|
||||||
@@ -142,6 +169,14 @@ 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; }
|
||||||
|
|
||||||
|
// A caption-style line that wraps at the dialog edge (Type().textColored doesn't wrap).
|
||||||
|
static void wrappedCaption(ImU32 color, const char* text)
|
||||||
|
{
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(color));
|
||||||
|
material::Type().textWrapped(material::TypeStyle::Caption, text);
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
}
|
||||||
|
|
||||||
static std::string shortAddr(const std::string& a)
|
static std::string shortAddr(const std::string& a)
|
||||||
{
|
{
|
||||||
if (a.size() <= 20) return a;
|
if (a.size() <= 20) return a;
|
||||||
@@ -226,6 +261,121 @@ static void submitOperation(App* app)
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The ticked addresses that are worth consolidating, in list order.
|
||||||
|
static std::vector<services::NoteGroup> selectedGroups()
|
||||||
|
{
|
||||||
|
std::vector<services::NoteGroup> out;
|
||||||
|
for (const auto& g : s_groups)
|
||||||
|
if (s_selected.count(g.address) && services::worthConsolidating(g, s_fee)) out.push_back(g);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Per-address consolidation. One background job submits, address by address, merges of each address's
|
||||||
|
// notes back into that address — several batches when it holds more notes than the batch size.
|
||||||
|
// z_mergetoaddress locks a batch's notes before it returns, so back-to-back calls take disjoint notes.
|
||||||
|
static void submitPerAddress(App* app)
|
||||||
|
{
|
||||||
|
const auto groups = selectedGroups();
|
||||||
|
if (groups.empty() || !app->worker()) return;
|
||||||
|
s_runs_submitting = true;
|
||||||
|
s_operation_pending = true;
|
||||||
|
s_op_terminal = false;
|
||||||
|
s_status_message = TR("shield_submitting");
|
||||||
|
s_last_poll = ImGui::GetTime();
|
||||||
|
const double fee = s_fee;
|
||||||
|
const int limit = s_utxo_limit;
|
||||||
|
app->worker()->post([app, rpc = app->rpc(), groups, fee, limit]() -> rpc::RPCWorker::MainCb {
|
||||||
|
std::vector<services::AddressRun> runs;
|
||||||
|
for (const auto& g : groups) {
|
||||||
|
runs.push_back(services::consolidateInPlace(g.address, [&](const std::string& addr) {
|
||||||
|
services::MergeCallResult r;
|
||||||
|
try {
|
||||||
|
rpc::RPCClient::TraceScope trace("Shield dialog / Consolidate in place");
|
||||||
|
// fromaddrs = [addr], toaddr = addr: the address's notes merge back into it.
|
||||||
|
const nlohmann::json res = rpc->call("z_mergetoaddress",
|
||||||
|
{nlohmann::json::array({addr}), addr, fee, limit, limit});
|
||||||
|
r.opid = res.value("opid", std::string());
|
||||||
|
r.merging = res.value("mergingNotes", 0);
|
||||||
|
r.remaining = res.value("remainingNotes", 0);
|
||||||
|
if (r.opid.empty()) r.error = "no operation id";
|
||||||
|
} catch (const std::exception& e) { r.error = e.what(); }
|
||||||
|
return r;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return [app, runs]() {
|
||||||
|
s_runs_submitting = false;
|
||||||
|
s_runs.clear();
|
||||||
|
int submitted = 0;
|
||||||
|
for (const auto& r : runs) {
|
||||||
|
PerAddressRun pr;
|
||||||
|
pr.run = r;
|
||||||
|
pr.state.assign(r.opids.size(), 0);
|
||||||
|
for (const auto& id : r.opids) { app->trackOperation(id); ++submitted; }
|
||||||
|
s_runs.push_back(std::move(pr));
|
||||||
|
}
|
||||||
|
if (submitted > 0) {
|
||||||
|
s_status_message = TR("merge_progress");
|
||||||
|
Notifications::instance().success(TR("merge_started"));
|
||||||
|
} else {
|
||||||
|
s_operation_pending = false; s_op_terminal = true;
|
||||||
|
s_status_message = TR("merge_keep_each_nothing_submitted");
|
||||||
|
Notifications::instance().error(TR("merge_keep_each_nothing_submitted"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Progress of a per-address run: poll every ~2s until each batch's operation is done or failed. Unfiltered,
|
||||||
|
// like the shared poller (some daemons reject the per-opid form); only our operations are read.
|
||||||
|
static void pollPerAddress(App* app)
|
||||||
|
{
|
||||||
|
if (s_runs.empty() || s_op_terminal || !app->worker()) return;
|
||||||
|
const double now = ImGui::GetTime();
|
||||||
|
if (now - s_last_poll < 2.0) return;
|
||||||
|
s_last_poll = now;
|
||||||
|
app->worker()->post([rpc = app->rpc()]() -> rpc::RPCWorker::MainCb {
|
||||||
|
nlohmann::json result; std::string error;
|
||||||
|
try {
|
||||||
|
rpc::RPCClient::TraceScope trace("Shield dialog / Consolidate in place status");
|
||||||
|
result = rpc->call("z_getoperationstatus", nlohmann::json::array());
|
||||||
|
} catch (const std::exception& e) { error = e.what(); }
|
||||||
|
return [result, error]() {
|
||||||
|
if (!error.empty() || !result.is_array()) return; // transient — retry next tick
|
||||||
|
for (const auto& op : result) {
|
||||||
|
if (!op.is_object() || !op.contains("id") || !op["id"].is_string()) continue;
|
||||||
|
const std::string id = op["id"].get<std::string>();
|
||||||
|
const std::string status = (op.contains("status") && op["status"].is_string())
|
||||||
|
? op["status"].get<std::string>() : std::string();
|
||||||
|
for (auto& r : s_runs) {
|
||||||
|
for (size_t i = 0; i < r.run.opids.size(); ++i) {
|
||||||
|
if (r.run.opids[i] != id || r.state[i] != 0) continue;
|
||||||
|
if (status == "success") r.state[i] = 1;
|
||||||
|
else if (status == "failed" || status == "cancelled") {
|
||||||
|
r.state[i] = 2;
|
||||||
|
if (r.failure.empty() && op.contains("error") && op["error"].is_object())
|
||||||
|
r.failure = op["error"].value("message", std::string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bool allDone = true;
|
||||||
|
int failed = 0;
|
||||||
|
for (const auto& r : s_runs)
|
||||||
|
for (int st : r.state) { if (st == 0) allDone = false; if (st == 2) ++failed; }
|
||||||
|
if (!allDone) return;
|
||||||
|
s_operation_pending = false;
|
||||||
|
s_op_terminal = true;
|
||||||
|
if (failed == 0) {
|
||||||
|
s_status_message = TR("shield_completed");
|
||||||
|
Notifications::instance().success(TR("shield_merge_done"));
|
||||||
|
} else {
|
||||||
|
s_status_message = TR("merge_keep_each_some_failed");
|
||||||
|
Notifications::instance().error(TR("merge_keep_each_some_failed"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Live-progress self-poll: while an op is in flight, poll z_getoperationstatus every ~2s so the modal
|
// Live-progress self-poll: while an op is in flight, poll z_getoperationstatus every ~2s so the modal
|
||||||
// shows "Consolidating… → Done/Failed" without a manual button. (The shared poller also tracks it for
|
// shows "Consolidating… → Done/Failed" without a manual button. (The shared poller also tracks it for
|
||||||
// balance refresh; this drives only the inline display.)
|
// balance refresh; this drives only the inline display.)
|
||||||
@@ -290,6 +440,7 @@ void ShieldDialog::render(App* app)
|
|||||||
const auto& state = app->getWalletState();
|
const auto& state = app->getWalletState();
|
||||||
autoSelectDestination(state);
|
autoSelectDestination(state);
|
||||||
pollOperation(app);
|
pollOperation(app);
|
||||||
|
pollPerAddress(app);
|
||||||
if (isMerge && !s_scope_loaded && !s_scope_loading && s_operation_id.empty()) loadScope(app);
|
if (isMerge && !s_scope_loaded && !s_scope_loading && s_operation_id.empty()) loadScope(app);
|
||||||
|
|
||||||
// ── Description ──────────────────────────────────────────────────────────────────────────────
|
// ── Description ──────────────────────────────────────────────────────────────────────────────
|
||||||
@@ -298,7 +449,9 @@ void ShieldDialog::render(App* app)
|
|||||||
: TR("shield_description"));
|
: TR("shield_description"));
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
|
||||||
const bool opInFlight = !s_operation_id.empty(); // submitted — inputs frozen, showing progress
|
// Submitted — inputs frozen, showing progress.
|
||||||
|
const bool opInFlight = !s_operation_id.empty() || s_runs_submitting || !s_runs.empty();
|
||||||
|
const bool perAddress = isMerge && s_per_address;
|
||||||
|
|
||||||
// ── Merge: scope + source selector ──────────────────────────────────────────────────────────
|
// ── Merge: scope + source selector ──────────────────────────────────────────────────────────
|
||||||
if (isMerge && !opInFlight) {
|
if (isMerge && !opInFlight) {
|
||||||
@@ -313,9 +466,17 @@ void ShieldDialog::render(App* app)
|
|||||||
}
|
}
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
|
||||||
|
// Where the funds go: all into one address, or each address's notes back into that address.
|
||||||
|
ImGui::TextUnformatted(TR("merge_where")); // own line: the choices are long in some languages
|
||||||
|
int where = s_per_address ? 1 : 0;
|
||||||
|
ImGui::RadioButton(TR("merge_into_one"), &where, 0); ImGui::SameLine();
|
||||||
|
ImGui::RadioButton(TR("merge_keep_each"), &where, 1);
|
||||||
|
s_per_address = (where == 1);
|
||||||
|
ImGui::Spacing();
|
||||||
|
|
||||||
// Source selector — only offer the types that actually have inputs.
|
// Source selector — only offer the types that actually have inputs.
|
||||||
const bool tOk = s_t_count > 0, zOk = s_z_count > 0;
|
const bool tOk = s_t_count > 0, zOk = s_z_count > 0;
|
||||||
if (tOk && zOk) {
|
if (tOk && zOk && !s_per_address) {
|
||||||
ImGui::AlignTextToFramePadding();
|
ImGui::AlignTextToFramePadding();
|
||||||
ImGui::TextUnformatted(TR("merge_source"));
|
ImGui::TextUnformatted(TR("merge_source"));
|
||||||
ImGui::SameLine(0, Layout::spacingLg());
|
ImGui::SameLine(0, Layout::spacingLg());
|
||||||
@@ -324,6 +485,46 @@ void ShieldDialog::render(App* app)
|
|||||||
ImGui::RadioButton(TR("merge_src_both"), &s_src, 2);
|
ImGui::RadioButton(TR("merge_src_both"), &s_src, 2);
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keep each address's funds in place: pick the addresses. Shielded only — a transparent address
|
||||||
|
// can't take a merge's output, its funds can only be shielded ("Into one address").
|
||||||
|
if (s_per_address) {
|
||||||
|
wrappedCaption(material::OnSurfaceMedium(), TR("merge_keep_each_desc"));
|
||||||
|
ImGui::Spacing();
|
||||||
|
int listed = 0;
|
||||||
|
for (const auto& g : s_groups) if (g.notes >= 2) ++listed;
|
||||||
|
if (s_scope_loaded && listed == 0) {
|
||||||
|
wrappedCaption(material::Warning(), TR("merge_keep_each_none"));
|
||||||
|
} else if (listed > 0) {
|
||||||
|
const float rowH = ImGui::GetFrameHeightWithSpacing();
|
||||||
|
const float listH = std::min(static_cast<float>(listed), 7.0f) * rowH + ImGui::GetStyle().WindowPadding.y * 2.0f;
|
||||||
|
if (ImGui::BeginChild("##ConsolidateEach", ImVec2(-1, listH), ImGuiChildFlags_Borders)) {
|
||||||
|
for (const auto& g : s_groups) {
|
||||||
|
if (g.notes < 2) continue;
|
||||||
|
const bool worth = services::worthConsolidating(g, s_fee);
|
||||||
|
bool ticked = worth && s_selected.count(g.address) > 0;
|
||||||
|
const std::string label = app->getAddressLabel(g.address);
|
||||||
|
char row[256];
|
||||||
|
std::snprintf(row, sizeof(row), TR("merge_keep_each_row_fmt"),
|
||||||
|
label.empty() ? shortAddr(g.address).c_str() : label.c_str(),
|
||||||
|
g.notes, fmtAmt(g.amount).c_str());
|
||||||
|
ImGui::PushID(g.address.c_str());
|
||||||
|
ImGui::BeginDisabled(!worth);
|
||||||
|
if (ImGui::Checkbox(row, &ticked)) {
|
||||||
|
if (ticked) s_selected.insert(g.address);
|
||||||
|
else s_selected.erase(g.address);
|
||||||
|
}
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
|
||||||
|
material::Tooltip("%s%s%s", g.address.c_str(), worth ? "" : "\n\n",
|
||||||
|
worth ? "" : TR("merge_keep_each_too_small"));
|
||||||
|
ImGui::PopID();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::EndChild();
|
||||||
|
}
|
||||||
|
ImGui::Spacing();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Shield coinbase: from address ───────────────────────────────────────────────────────────
|
// ── Shield coinbase: from address ───────────────────────────────────────────────────────────
|
||||||
@@ -335,6 +536,7 @@ void ShieldDialog::render(App* app)
|
|||||||
|
|
||||||
// ── Destination (z-address) ─────────────────────────────────────────────────────────────────
|
// ── Destination (z-address) ─────────────────────────────────────────────────────────────────
|
||||||
if (!opInFlight) {
|
if (!opInFlight) {
|
||||||
|
if (!perAddress) {
|
||||||
ImGui::TextUnformatted(TR("shield_to_address"));
|
ImGui::TextUnformatted(TR("shield_to_address"));
|
||||||
if (state.z_addresses.empty()) {
|
if (state.z_addresses.empty()) {
|
||||||
material::Type().textColored(material::TypeStyle::Caption, material::Warning(), TR("shield_no_zaddr_hint"));
|
material::Type().textColored(material::TypeStyle::Caption, material::Warning(), TR("shield_no_zaddr_hint"));
|
||||||
@@ -381,6 +583,7 @@ void ShieldDialog::render(App* app)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
} // !perAddress
|
||||||
|
|
||||||
// ── Advanced (fee + batch size) ─────────────────────────────────────────────────────────
|
// ── Advanced (fee + batch size) ─────────────────────────────────────────────────────────
|
||||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||||
@@ -406,7 +609,16 @@ void ShieldDialog::render(App* app)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Batch hint: one run only merges up to the limit; large sets need repeats.
|
// Batch hint: one run only merges up to the limit; large sets need repeats.
|
||||||
if (isMerge && s_scope_loaded && srcCount() > s_utxo_limit) {
|
if (perAddress) {
|
||||||
|
bool multi = false;
|
||||||
|
for (const auto& g : selectedGroups()) if (services::estimateBatches(g.notes, s_utxo_limit) > 1) multi = true;
|
||||||
|
if (multi) {
|
||||||
|
char hb[200];
|
||||||
|
std::snprintf(hb, sizeof(hb), TR("merge_keep_each_batches_fmt"), s_utxo_limit);
|
||||||
|
ImGui::Spacing();
|
||||||
|
wrappedCaption(material::Warning(), hb);
|
||||||
|
}
|
||||||
|
} else if (isMerge && s_scope_loaded && srcCount() > s_utxo_limit) {
|
||||||
char hb[160];
|
char hb[160];
|
||||||
std::snprintf(hb, sizeof(hb), TR("merge_batch_fmt"), s_utxo_limit);
|
std::snprintf(hb, sizeof(hb), TR("merge_batch_fmt"), s_utxo_limit);
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
@@ -415,6 +627,46 @@ void ShieldDialog::render(App* app)
|
|||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Per-address progress ────────────────────────────────────────────────────────────────────
|
||||||
|
if (!s_runs.empty()) {
|
||||||
|
bool leftovers = false;
|
||||||
|
for (const auto& r : s_runs) {
|
||||||
|
const std::string label = app->getAddressLabel(r.run.address);
|
||||||
|
const char* name = label.empty() ? nullptr : label.c_str();
|
||||||
|
const std::string shortA = shortAddr(r.run.address);
|
||||||
|
int done = 0, failed = 0;
|
||||||
|
for (int st : r.state) { if (st == 1) ++done; if (st == 2) ++failed; }
|
||||||
|
const int batches = static_cast<int>(r.run.opids.size());
|
||||||
|
char line[320];
|
||||||
|
ImU32 col = material::OnSurfaceMedium();
|
||||||
|
if (batches == 0) {
|
||||||
|
std::snprintf(line, sizeof(line), TR("merge_keep_each_status_not_started"), name ? name : shortA.c_str(),
|
||||||
|
r.run.error.c_str());
|
||||||
|
col = material::Error();
|
||||||
|
} else if (failed > 0) {
|
||||||
|
std::snprintf(line, sizeof(line), TR("merge_keep_each_status_failed"), name ? name : shortA.c_str(),
|
||||||
|
r.failure.c_str());
|
||||||
|
col = material::Error();
|
||||||
|
} else if (done == batches) {
|
||||||
|
std::snprintf(line, sizeof(line), TR("merge_keep_each_status_done"), name ? name : shortA.c_str(),
|
||||||
|
r.run.notesMerged, batches);
|
||||||
|
col = material::Success();
|
||||||
|
} else {
|
||||||
|
std::snprintf(line, sizeof(line), TR("merge_keep_each_status_running"), name ? name : shortA.c_str(),
|
||||||
|
r.run.notesMerged, done, batches);
|
||||||
|
}
|
||||||
|
wrappedCaption(col, line);
|
||||||
|
if (batches > 1 || r.run.remaining >= 2) leftovers = true;
|
||||||
|
}
|
||||||
|
if (leftovers) {
|
||||||
|
ImGui::Spacing();
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(material::OnSurfaceMedium()));
|
||||||
|
ImGui::TextWrapped("%s", TR("merge_keep_each_run_again"));
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
}
|
||||||
|
ImGui::Spacing();
|
||||||
|
}
|
||||||
|
|
||||||
// ── Live progress / status ──────────────────────────────────────────────────────────────────
|
// ── Live progress / status ──────────────────────────────────────────────────────────────────
|
||||||
if (!s_status_message.empty()) {
|
if (!s_status_message.empty()) {
|
||||||
if (s_operation_pending && !s_op_terminal)
|
if (s_operation_pending && !s_op_terminal)
|
||||||
@@ -447,7 +699,16 @@ void ShieldDialog::render(App* app)
|
|||||||
|
|
||||||
// Confirm summary (inline, before the fund-moving call). Merge/consolidate shows amount + input
|
// Confirm summary (inline, before the fund-moving call). Merge/consolidate shows amount + input
|
||||||
// count; shield-coinbase just gets the button relabel (its inputs aren't enumerated here).
|
// count; shield-coinbase just gets the button relabel (its inputs aren't enumerated here).
|
||||||
if (s_confirm && isMerge) {
|
if (s_confirm && perAddress) {
|
||||||
|
int notes = 0, batches = 0;
|
||||||
|
const auto sel = selectedGroups();
|
||||||
|
for (const auto& g : sel) { notes += g.notes; batches += services::estimateBatches(g.notes, s_utxo_limit); }
|
||||||
|
char cb[320];
|
||||||
|
std::snprintf(cb, sizeof(cb), TR("merge_keep_each_confirm_fmt"), notes, static_cast<int>(sel.size()),
|
||||||
|
fmtAmt(s_fee * batches).c_str());
|
||||||
|
ImGui::TextWrapped("%s", cb);
|
||||||
|
ImGui::Spacing();
|
||||||
|
} else if (s_confirm && isMerge) {
|
||||||
char cb[200];
|
char cb[200];
|
||||||
std::snprintf(cb, sizeof(cb), TR("merge_confirm_fmt"),
|
std::snprintf(cb, sizeof(cb), TR("merge_confirm_fmt"),
|
||||||
fmtAmt(srcAmount()).c_str(), srcCount(), shortAddr(s_to_address).c_str());
|
fmtAmt(srcAmount()).c_str(), srcCount(), shortAddr(s_to_address).c_str());
|
||||||
@@ -457,19 +718,21 @@ void ShieldDialog::render(App* app)
|
|||||||
|
|
||||||
bool can_submit = haveDest && connected && !syncing;
|
bool can_submit = haveDest && connected && !syncing;
|
||||||
if (isMerge && s_scope_loaded && srcCount() == 0) can_submit = false;
|
if (isMerge && s_scope_loaded && srcCount() == 0) can_submit = false;
|
||||||
|
if (perAddress) can_submit = connected && !syncing && s_scope_loaded && !selectedGroups().empty();
|
||||||
|
|
||||||
float footerBtnW = shieldBtn.width + cancelBtn.width + ImGui::GetStyle().ItemSpacing.x;
|
float footerBtnW = shieldBtn.width + cancelBtn.width + ImGui::GetStyle().ItemSpacing.x;
|
||||||
material::BeginOverlayDialogFooter(footerBtnW, /*drawSeparator=*/false);
|
material::BeginOverlayDialogFooter(footerBtnW, /*drawSeparator=*/false);
|
||||||
|
|
||||||
if (!can_submit) ImGui::BeginDisabled();
|
if (!can_submit) ImGui::BeginDisabled();
|
||||||
if (material::TactileButton(primaryLabel, ImVec2(shieldBtn.width, 0), S.resolveFont(shieldBtn.font))) {
|
if (material::TactileButton(primaryLabel, ImVec2(shieldBtn.width, 0), S.resolveFont(shieldBtn.font))) {
|
||||||
if (s_confirm) { submitOperation(app); }
|
if (s_confirm) { if (perAddress) submitPerAddress(app); else submitOperation(app); }
|
||||||
else { s_confirm = true; } // first click → show the confirm summary
|
else { s_confirm = true; } // first click → show the confirm summary
|
||||||
}
|
}
|
||||||
if (!can_submit) ImGui::EndDisabled();
|
if (!can_submit) ImGui::EndDisabled();
|
||||||
if (!can_submit && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
|
if (!can_submit && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
|
||||||
if (!connected) material::Tooltip("%s", TR("send_tooltip_not_connected"));
|
if (!connected) material::Tooltip("%s", TR("send_tooltip_not_connected"));
|
||||||
else if (syncing) material::Tooltip("%s", TR("send_tooltip_syncing"));
|
else if (syncing) material::Tooltip("%s", TR("send_tooltip_syncing"));
|
||||||
|
else if (perAddress) material::Tooltip("%s", TR("merge_keep_each_select"));
|
||||||
else if (!haveDest) material::Tooltip("%s", TR("shield_select_z"));
|
else if (!haveDest) material::Tooltip("%s", TR("shield_select_z"));
|
||||||
else if (isMerge && srcCount() == 0) material::Tooltip("%s", TR("merge_no_spendable"));
|
else if (isMerge && srcCount() == 0) material::Tooltip("%s", TR("merge_no_spendable"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2519,6 +2519,23 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["merge_back"] = "Back";
|
strings_["merge_back"] = "Back";
|
||||||
strings_["merge_progress"] = "Consolidating\xE2\x80\xA6 this can take a few minutes. You can close this window.";
|
strings_["merge_progress"] = "Consolidating\xE2\x80\xA6 this can take a few minutes. You can close this window.";
|
||||||
strings_["merge_no_spendable"] = "No spendable inputs to consolidate yet.";
|
strings_["merge_no_spendable"] = "No spendable inputs to consolidate yet.";
|
||||||
|
strings_["merge_where"] = "Where should the funds go?";
|
||||||
|
strings_["merge_into_one"] = "Into one address";
|
||||||
|
strings_["merge_keep_each"] = "Keep in each address";
|
||||||
|
strings_["merge_keep_each_desc"] = "Each address's notes are merged back into that same address, so funds you keep apart under different labels stay apart. Shielded addresses only — transparent funds can only be shielded (Into one address).";
|
||||||
|
strings_["merge_keep_each_none"] = "Every shielded address already holds a single note — nothing to consolidate.";
|
||||||
|
strings_["merge_keep_each_row_fmt"] = "%s · %d notes · %s DRGX";
|
||||||
|
strings_["merge_keep_each_too_small"] = "Too small to consolidate: the fee would use up these funds.";
|
||||||
|
strings_["merge_keep_each_batches_fmt"] = "Addresses with more than %d notes are merged in several batches; run it again after they confirm to merge the rest.";
|
||||||
|
strings_["merge_keep_each_confirm_fmt"] = "Consolidate %d notes in %d addresses? Each address keeps its own funds. Fees: about %s DRGX (one per batch).";
|
||||||
|
strings_["merge_keep_each_select"] = "Tick at least one address to consolidate.";
|
||||||
|
strings_["merge_keep_each_nothing_submitted"] = "Nothing was consolidated — the node refused every merge.";
|
||||||
|
strings_["merge_keep_each_some_failed"] = "Some merges failed — see the list above.";
|
||||||
|
strings_["merge_keep_each_status_not_started"] = "%s — not started: %s";
|
||||||
|
strings_["merge_keep_each_status_failed"] = "%s — failed: %s";
|
||||||
|
strings_["merge_keep_each_status_done"] = "%s — done: %d notes merged in %d batch(es)";
|
||||||
|
strings_["merge_keep_each_status_running"] = "%s — merging %d notes (%d of %d batches done)…";
|
||||||
|
strings_["merge_keep_each_run_again"] = "Addresses merged in several batches now hold one note per batch. Run this again after those confirm to merge them into one.";
|
||||||
strings_["done"] = "Done";
|
strings_["done"] = "Done";
|
||||||
|
|
||||||
// --- Transaction Details Dialog ---
|
// --- Transaction Details Dialog ---
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
#include "resources/embedded_resources.h"
|
#include "resources/embedded_resources.h"
|
||||||
#include "services/network_refresh_service.h"
|
#include "services/network_refresh_service.h"
|
||||||
#include "services/refresh_scheduler.h"
|
#include "services/refresh_scheduler.h"
|
||||||
|
#include "services/note_consolidation.h"
|
||||||
#include "services/wallet_security_controller.h"
|
#include "services/wallet_security_controller.h"
|
||||||
#include "services/wallet_security_workflow.h"
|
#include "services/wallet_security_workflow.h"
|
||||||
#include "services/wallet_security_workflow_executor.h"
|
#include "services/wallet_security_workflow_executor.h"
|
||||||
@@ -4456,6 +4457,18 @@ void testDaemonStopStringsTranslated()
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Per-address consolidation strings.
|
||||||
|
void testConsolidationStringsTranslated()
|
||||||
|
{
|
||||||
|
expectKeysTranslated({
|
||||||
|
"merge_where", "merge_into_one", "merge_keep_each", "merge_keep_each_desc", "merge_keep_each_none",
|
||||||
|
"merge_keep_each_row_fmt", "merge_keep_each_too_small", "merge_keep_each_batches_fmt",
|
||||||
|
"merge_keep_each_confirm_fmt", "merge_keep_each_select", "merge_keep_each_nothing_submitted",
|
||||||
|
"merge_keep_each_some_failed", "merge_keep_each_status_not_started", "merge_keep_each_status_failed",
|
||||||
|
"merge_keep_each_status_done", "merge_keep_each_status_running", "merge_keep_each_run_again",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Mining addresses: the strings that explain transparent addresses only receive mining rewards.
|
// Mining addresses: the strings that explain transparent addresses only receive mining rewards.
|
||||||
void testMiningAddressStringsTranslated()
|
void testMiningAddressStringsTranslated()
|
||||||
{
|
{
|
||||||
@@ -4616,6 +4629,84 @@ void testDebugLogTailRules()
|
|||||||
EXPECT_FALSE(EmbeddedDaemon::processIsDaemon(0));
|
EXPECT_FALSE(EmbeddedDaemon::processIsDaemon(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Per-address consolidation: each address's notes merge back into that same address, so separately
|
||||||
|
// labelled funds stay separate while the wallet sheds notes.
|
||||||
|
void testNoteConsolidation()
|
||||||
|
{
|
||||||
|
using namespace dragonx::services;
|
||||||
|
const nlohmann::json unspent = nlohmann::json::parse(R"([
|
||||||
|
{"address":"zsA","amount":1.0,"confirmations":5,"spendable":true},
|
||||||
|
{"address":"zsA","amount":2.0,"confirmations":5,"spendable":true},
|
||||||
|
{"address":"zsA","amount":0.5,"confirmations":1},
|
||||||
|
{"address":"zsA","amount":9.0,"confirmations":0,"spendable":true},
|
||||||
|
{"address":"zsB","amount":3.0,"confirmations":9,"spendable":true},
|
||||||
|
{"address":"zsW","amount":4.0,"confirmations":9,"spendable":false},
|
||||||
|
{"address":"zsW","amount":4.0,"confirmations":9,"spendable":false},
|
||||||
|
{"amount":1.0,"confirmations":9}
|
||||||
|
])");
|
||||||
|
const auto groups = groupSpendableNotes(unspent);
|
||||||
|
EXPECT_EQ(groups.size(), size_t(2)); // watch-only and address-less left out
|
||||||
|
EXPECT_EQ(groups[0].address, std::string("zsA")); // most notes first
|
||||||
|
EXPECT_EQ(groups[0].notes, 3); // the unconfirmed note is not spendable yet
|
||||||
|
EXPECT_NEAR(groups[0].amount, 3.5, 1e-9);
|
||||||
|
EXPECT_EQ(groups[1].notes, 1);
|
||||||
|
EXPECT_TRUE(groupSpendableNotes(nlohmann::json::object()).empty());
|
||||||
|
|
||||||
|
const double fee = 0.0001;
|
||||||
|
EXPECT_TRUE(worthConsolidating(groups[0], fee));
|
||||||
|
EXPECT_FALSE(worthConsolidating(groups[1], fee)); // a single note: nothing to merge
|
||||||
|
EXPECT_FALSE(worthConsolidating(NoteGroup{"zsD", 4, 0.00015}, fee)); // the fee would eat it
|
||||||
|
|
||||||
|
EXPECT_EQ(estimateBatches(0, 90), 0);
|
||||||
|
EXPECT_EQ(estimateBatches(1, 90), 0);
|
||||||
|
EXPECT_EQ(estimateBatches(2, 90), 1);
|
||||||
|
EXPECT_EQ(estimateBatches(90, 90), 1);
|
||||||
|
EXPECT_EQ(estimateBatches(91, 90), 1); // the lone leftover note is not a batch
|
||||||
|
EXPECT_EQ(estimateBatches(92, 90), 2);
|
||||||
|
EXPECT_EQ(estimateBatches(182, 90), 3);
|
||||||
|
EXPECT_EQ(estimateBatches(5, 1), 2); // a limit below 2 is treated as 2
|
||||||
|
|
||||||
|
// A stand-in daemon: each call merges up to `limit` of the address's unlocked notes into itself and
|
||||||
|
// refuses a lone note, like z_mergetoaddress.
|
||||||
|
auto daemonFor = [](int notes, int limit, int failOnCall) {
|
||||||
|
auto left = std::make_shared<int>(notes);
|
||||||
|
auto calls = std::make_shared<int>(0);
|
||||||
|
return [=](const std::string&) {
|
||||||
|
MergeCallResult r;
|
||||||
|
if (++*calls == failOnCall) { r.error = "Insufficient funds"; return r; }
|
||||||
|
if (*left < 2) { r.error = "Destination address is also the only source address"; return r; }
|
||||||
|
r.merging = std::min(limit, *left);
|
||||||
|
*left -= r.merging;
|
||||||
|
r.remaining = *left;
|
||||||
|
r.opid = "opid-" + std::to_string(*calls);
|
||||||
|
return r;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
for (int notes : {2, 90, 91, 92, 250}) {
|
||||||
|
const AddressRun run = consolidateInPlace("zsA", daemonFor(notes, 90, -1));
|
||||||
|
EXPECT_TRUE(run.error.empty());
|
||||||
|
EXPECT_EQ(static_cast<int>(run.opids.size()), estimateBatches(notes, 90));
|
||||||
|
EXPECT_TRUE(run.remaining < 2);
|
||||||
|
EXPECT_EQ(run.notesMerged + run.remaining, notes);
|
||||||
|
}
|
||||||
|
{ // the first call fails: nothing submitted
|
||||||
|
const AddressRun run = consolidateInPlace("zsA", daemonFor(10, 90, 1));
|
||||||
|
EXPECT_TRUE(run.opids.empty());
|
||||||
|
EXPECT_FALSE(run.error.empty());
|
||||||
|
}
|
||||||
|
{ // a later call fails: the earlier batch stands, the error is reported
|
||||||
|
const AddressRun run = consolidateInPlace("zsA", daemonFor(200, 90, 2));
|
||||||
|
EXPECT_EQ(run.opids.size(), size_t(1));
|
||||||
|
EXPECT_EQ(run.notesMerged, 90);
|
||||||
|
EXPECT_FALSE(run.error.empty());
|
||||||
|
}
|
||||||
|
{ // bounded
|
||||||
|
const AddressRun run = consolidateInPlace("zsA", daemonFor(1000, 2, -1), /*maxBatches=*/5);
|
||||||
|
EXPECT_EQ(run.opids.size(), size_t(5));
|
||||||
|
EXPECT_TRUE(run.remaining >= 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void testDatadirLockGate()
|
void testDatadirLockGate()
|
||||||
{
|
{
|
||||||
using dragonx::daemon::EmbeddedDaemon;
|
using dragonx::daemon::EmbeddedDaemon;
|
||||||
@@ -9233,6 +9324,7 @@ int main()
|
|||||||
testConfSetsKey();
|
testConfSetsKey();
|
||||||
testOwnershipRecord();
|
testOwnershipRecord();
|
||||||
testDebugLogTailRules();
|
testDebugLogTailRules();
|
||||||
|
testNoteConsolidation();
|
||||||
testStopDeliveryPolicy();
|
testStopDeliveryPolicy();
|
||||||
testProcStatParse();
|
testProcStatParse();
|
||||||
testStopProgressTracker();
|
testStopProgressTracker();
|
||||||
@@ -9245,6 +9337,7 @@ int main()
|
|||||||
testChooseDaemonStatus();
|
testChooseDaemonStatus();
|
||||||
testDaemonStopStringsTranslated();
|
testDaemonStopStringsTranslated();
|
||||||
testMiningAddressStringsTranslated();
|
testMiningAddressStringsTranslated();
|
||||||
|
testConsolidationStringsTranslated();
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
testWindowsStopNeverKillsForeignDaemon();
|
testWindowsStopNeverKillsForeignDaemon();
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -120,6 +120,8 @@ inherits the GUI's environment.
|
|||||||
| `FAKE_MINING` | 0 | internal miner on: `stop` waits for cs_main first (`GenerateBitcoins` join) |
|
| `FAKE_MINING` | 0 | internal miner on: `stop` waits for cs_main first (`GenerateBitcoins` join) |
|
||||||
| `FAKE_REBUILD_SILENT` | 0 | hold cs_main like a rebuild but log no rebuild markers (a busy node whose output says nothing) |
|
| `FAKE_REBUILD_SILENT` | 0 | hold cs_main like a rebuild but log no rebuild markers (a busy node whose output says nothing) |
|
||||||
| `FAKE_STUCK_SHUTDOWN` | 0 | the Sep 29 orphan: hold the datadir lock, no listener, ignore SIGTERM, never exit |
|
| `FAKE_STUCK_SHUTDOWN` | 0 | the Sep 29 orphan: hold the datadir lock, no listener, ignore SIGTERM, never exit |
|
||||||
|
| `FAKE_WALLET_NOTES` | (off) | a note wallet: `addr:count:amount,...` shielded notes; enables `z_mergetoaddress` + its operations (dragonxd rules: confirmed unlocked notes, a batch locked on return, merged note next block) |
|
||||||
|
| `FAKE_MERGE_SECS` | 6 | how long a `z_mergetoaddress` operation runs before it succeeds |
|
||||||
| `FAKE_HEIGHT`, `FAKE_REBUILD_TOTAL`, `FAKE_NOTES`, `FAKE_VERSION` | 3327145, 62583, 13632, 1030050 | cosmetic numbers |
|
| `FAKE_HEIGHT`, `FAKE_REBUILD_TOTAL`, `FAKE_NOTES`, `FAKE_VERSION` | 3327145, 62583, 13632, 1030050 | cosmetic numbers |
|
||||||
|
|
||||||
## Test cases
|
## Test cases
|
||||||
|
|||||||
@@ -407,6 +407,187 @@ struct Canned {
|
|||||||
std::string errMsg;
|
std::string errMsg;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------
|
||||||
|
// Optional note wallet (FAKE_WALLET_NOTES="addr:count:amount,..."): shielded notes per address, plus
|
||||||
|
// z_mergetoaddress and its async operations with dragonxd's rules — confirmed, unlocked notes only;
|
||||||
|
// a batch's notes are locked when the call returns; "only source == destination with one note" is
|
||||||
|
// refused; the merged note appears one block later. Everything here runs under g_csMain (execOne).
|
||||||
|
// ---------------------------------------------------------------------------------------------
|
||||||
|
struct FakeNote { long id; std::string address; double amount; long height; bool locked; };
|
||||||
|
struct FakeOp {
|
||||||
|
std::string id;
|
||||||
|
std::vector<long> noteIds;
|
||||||
|
std::string to;
|
||||||
|
double fee = 0.0;
|
||||||
|
double finishAt = 0.0; // unixMs()/1000.0
|
||||||
|
std::string status = "executing";
|
||||||
|
long long created = 0;
|
||||||
|
};
|
||||||
|
bool g_noteWallet = false;
|
||||||
|
std::vector<FakeNote> g_notes;
|
||||||
|
std::vector<FakeOp> g_ops;
|
||||||
|
long g_noteSeq = 0;
|
||||||
|
long g_opSeq = 0;
|
||||||
|
double g_mergeSecs = 6.0;
|
||||||
|
|
||||||
|
void loadNoteWallet()
|
||||||
|
{
|
||||||
|
const char* spec = std::getenv("FAKE_WALLET_NOTES");
|
||||||
|
if (!spec || !*spec) return;
|
||||||
|
g_noteWallet = true;
|
||||||
|
g_mergeSecs = envDouble("FAKE_MERGE_SECS", 6.0);
|
||||||
|
std::string s(spec);
|
||||||
|
size_t pos = 0;
|
||||||
|
while (pos <= s.size()) {
|
||||||
|
size_t comma = s.find(',', pos);
|
||||||
|
if (comma == std::string::npos) comma = s.size();
|
||||||
|
const std::string item = s.substr(pos, comma - pos);
|
||||||
|
const size_t c1 = item.find(':'), c2 = item.find(':', c1 == std::string::npos ? 0 : c1 + 1);
|
||||||
|
if (c1 != std::string::npos && c2 != std::string::npos) {
|
||||||
|
const std::string addr = item.substr(0, c1);
|
||||||
|
const long count = std::strtol(item.c_str() + c1 + 1, nullptr, 10);
|
||||||
|
const double amount = std::strtod(item.c_str() + c2 + 1, nullptr);
|
||||||
|
for (long i = 0; i < count; ++i)
|
||||||
|
g_notes.push_back({++g_noteSeq, addr, amount, g_height.load() - 10 - i, false});
|
||||||
|
}
|
||||||
|
pos = comma + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> noteAddresses()
|
||||||
|
{
|
||||||
|
std::vector<std::string> out;
|
||||||
|
for (const auto& n : g_notes)
|
||||||
|
if (std::find(out.begin(), out.end(), n.address) == out.end()) out.push_back(n.address);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Operations past their run time succeed: their notes are spent, one merged note lands next block.
|
||||||
|
void settleOps()
|
||||||
|
{
|
||||||
|
const double now = unixMs() / 1000.0;
|
||||||
|
for (auto& op : g_ops) {
|
||||||
|
if (op.status != "executing" || now < op.finishAt) continue;
|
||||||
|
double sum = 0.0;
|
||||||
|
for (long id : op.noteIds) {
|
||||||
|
auto it = std::find_if(g_notes.begin(), g_notes.end(), [&](const FakeNote& n) { return n.id == id; });
|
||||||
|
if (it != g_notes.end()) { sum += it->amount; g_notes.erase(it); }
|
||||||
|
}
|
||||||
|
g_notes.push_back({++g_noteSeq, op.to, sum - op.fee, g_height.load() + 1, false});
|
||||||
|
op.status = "success";
|
||||||
|
event("merge-done", op.id + " notes=" + std::to_string(op.noteIds.size()) + " to=" + op.to);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
json opStatus(const FakeOp& op)
|
||||||
|
{
|
||||||
|
json o = {{"id", op.id}, {"status", op.status}, {"creation_time", op.created}, {"method", "z_mergetoaddress"}};
|
||||||
|
if (op.status == "success") o["result"] = {{"txid", fakeHash(1000000 + std::stol(op.id.substr(5)))}};
|
||||||
|
return o;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The note-wallet answer for `m`, or c.known = false when it isn't a note-wallet method.
|
||||||
|
Canned noteWalletReply(const std::string& m, const json& params)
|
||||||
|
{
|
||||||
|
Canned c;
|
||||||
|
settleOps();
|
||||||
|
const long h = g_height.load();
|
||||||
|
if (m == "z_listunspent") {
|
||||||
|
json arr = json::array();
|
||||||
|
for (const auto& n : g_notes) {
|
||||||
|
const long conf = std::max(0L, h - n.height + 1);
|
||||||
|
arr.push_back({{"txid", fakeHash(5000000 + n.id)}, {"outindex", 0}, {"confirmations", conf},
|
||||||
|
{"spendable", true}, {"address", n.address}, {"amount", n.amount}, {"memo", "f6"},
|
||||||
|
{"change", false}});
|
||||||
|
}
|
||||||
|
c.result = arr;
|
||||||
|
} else if (m == "z_listaddresses") {
|
||||||
|
json arr = json::array({kFakeZAddr});
|
||||||
|
for (const auto& a : noteAddresses()) arr.push_back(a);
|
||||||
|
c.result = arr;
|
||||||
|
} else if (m == "z_gettotalbalance" || m == "z_getbalance") {
|
||||||
|
const std::string only = (m == "z_getbalance" && !params.empty() && params[0].is_string())
|
||||||
|
? params[0].get<std::string>() : std::string();
|
||||||
|
double sum = 0.0;
|
||||||
|
for (const auto& n : g_notes)
|
||||||
|
if (only.empty() || n.address == only) sum += n.amount;
|
||||||
|
if (m == "z_getbalance") c.result = sum;
|
||||||
|
else {
|
||||||
|
char buf[64];
|
||||||
|
std::snprintf(buf, sizeof(buf), "%.8f", sum);
|
||||||
|
c.result = {{"transparent", "0.00"}, {"private", buf}, {"total", buf}};
|
||||||
|
}
|
||||||
|
} else if (m == "z_mergetoaddress") {
|
||||||
|
if (params.size() < 2 || !params[0].is_array() || !params[1].is_string()) {
|
||||||
|
c.errCode = -8; c.errMsg = "Invalid parameter";
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
std::vector<std::string> from;
|
||||||
|
bool anySapling = false;
|
||||||
|
for (const auto& a : params[0]) {
|
||||||
|
if (!a.is_string()) continue;
|
||||||
|
if (a.get<std::string>() == "ANY_SAPLING" || a.get<std::string>() == "*") anySapling = true;
|
||||||
|
else from.push_back(a.get<std::string>());
|
||||||
|
}
|
||||||
|
const std::string to = params[1].get<std::string>();
|
||||||
|
const double fee = params.size() > 2 && params[2].is_number() ? params[2].get<double>() : 0.0001;
|
||||||
|
long limit = params.size() > 4 && params[4].is_number() ? params[4].get<long>() : 90;
|
||||||
|
if (limit <= 0) limit = 1000000;
|
||||||
|
std::vector<long> picked;
|
||||||
|
double merging = 0.0, remainingValue = 0.0;
|
||||||
|
long counter = 0;
|
||||||
|
for (const auto& n : g_notes) {
|
||||||
|
if (n.locked || h - n.height + 1 < 1) continue;
|
||||||
|
if (!anySapling && std::find(from.begin(), from.end(), n.address) == from.end()) continue;
|
||||||
|
++counter;
|
||||||
|
if (static_cast<long>(picked.size()) < limit) { picked.push_back(n.id); merging += n.amount; }
|
||||||
|
else remainingValue += n.amount;
|
||||||
|
}
|
||||||
|
if (picked.empty()) { c.errCode = -6; c.errMsg = "Could not find any funds to merge."; return c; }
|
||||||
|
if (from.size() == 1 && !anySapling && from[0] == to && picked.size() == 1) {
|
||||||
|
c.errCode = -8;
|
||||||
|
c.errMsg = "Destination address is also the only source address, and all its funds are already merged.";
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
if (merging - fee < fee) {
|
||||||
|
c.errCode = -6; c.errMsg = "Insufficient funds to pay the fee"; return c;
|
||||||
|
}
|
||||||
|
for (auto& n : g_notes)
|
||||||
|
if (std::find(picked.begin(), picked.end(), n.id) != picked.end()) n.locked = true;
|
||||||
|
FakeOp op;
|
||||||
|
op.id = "opid-" + std::to_string(++g_opSeq);
|
||||||
|
op.noteIds = picked;
|
||||||
|
op.to = to;
|
||||||
|
op.fee = fee;
|
||||||
|
op.created = unixMs() / 1000;
|
||||||
|
op.finishAt = unixMs() / 1000.0 + g_mergeSecs;
|
||||||
|
g_ops.push_back(op);
|
||||||
|
event("merge", op.id + " notes=" + std::to_string(picked.size()) + " to=" + to);
|
||||||
|
c.result = {{"remainingUTXOs", 0}, {"remainingTransparentValue", 0.0},
|
||||||
|
{"remainingNotes", counter - static_cast<long>(picked.size())},
|
||||||
|
{"remainingShieldedValue", remainingValue}, {"mergingUTXOs", 0},
|
||||||
|
{"mergingTransparentValue", 0.0}, {"mergingNotes", static_cast<long>(picked.size())},
|
||||||
|
{"mergingShieldedValue", merging}, {"opid", op.id}};
|
||||||
|
} else if (m == "z_getoperationstatus" || m == "z_getoperationresult") {
|
||||||
|
std::vector<std::string> filter;
|
||||||
|
if (!params.empty() && params[0].is_array())
|
||||||
|
for (const auto& id : params[0]) if (id.is_string()) filter.push_back(id.get<std::string>());
|
||||||
|
json arr = json::array();
|
||||||
|
std::vector<FakeOp> keep;
|
||||||
|
for (const auto& op : g_ops) {
|
||||||
|
const bool match = filter.empty() || std::find(filter.begin(), filter.end(), op.id) != filter.end();
|
||||||
|
const bool finished = op.status != "executing";
|
||||||
|
if (match && (m == "z_getoperationstatus" || finished)) arr.push_back(opStatus(op));
|
||||||
|
if (!(m == "z_getoperationresult" && match && finished)) keep.push_back(op);
|
||||||
|
}
|
||||||
|
g_ops.swap(keep);
|
||||||
|
c.result = arr;
|
||||||
|
} else {
|
||||||
|
c.known = false;
|
||||||
|
}
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
Canned cannedReply(const std::string& m, const json& params)
|
Canned cannedReply(const std::string& m, const json& params)
|
||||||
{
|
{
|
||||||
Canned c;
|
Canned c;
|
||||||
@@ -500,6 +681,9 @@ Canned cannedReply(const std::string& m, const json& params)
|
|||||||
c.result = json::array();
|
c.result = json::array();
|
||||||
} else if (m == "setban" || m == "clearbanned") {
|
} else if (m == "setban" || m == "clearbanned") {
|
||||||
c.result = nullptr;
|
c.result = nullptr;
|
||||||
|
} else if (m == "z_mergetoaddress") {
|
||||||
|
c.errCode = -8; // only the note wallet (FAKE_WALLET_NOTES) can merge; execOne answers it there
|
||||||
|
c.errMsg = "fake_dragonxd: set FAKE_WALLET_NOTES to merge notes";
|
||||||
} else if (m == "z_listaddresses") {
|
} else if (m == "z_listaddresses") {
|
||||||
c.result = json::array({kFakeZAddr});
|
c.result = json::array({kFakeZAddr});
|
||||||
} else if (m == "getaddressesbyaccount" || m == "listaddresses") {
|
} else if (m == "getaddressesbyaccount" || m == "listaddresses") {
|
||||||
@@ -589,6 +773,10 @@ json execOne(const json& req, int& status)
|
|||||||
std::lock_guard<std::mutex> lk(g_csMain);
|
std::lock_guard<std::mutex> lk(g_csMain);
|
||||||
c = cannedReply(m, params);
|
c = cannedReply(m, params);
|
||||||
if (m == "setgenerate") g_minerOn = !params.empty() && params[0].is_boolean() && params[0].get<bool>();
|
if (m == "setgenerate") g_minerOn = !params.empty() && params[0].is_boolean() && params[0].get<bool>();
|
||||||
|
if (g_noteWallet) {
|
||||||
|
Canned w = noteWalletReply(m, params);
|
||||||
|
if (w.known) c = w;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (c.errCode != 0) return fail(c.errCode, c.errMsg);
|
if (c.errCode != 0) return fail(c.errCode, c.errMsg);
|
||||||
return rpcReply(c.result, nullptr, id);
|
return rpcReply(c.result, nullptr, id);
|
||||||
@@ -1240,6 +1428,7 @@ int main(int argc, char** argv)
|
|||||||
}
|
}
|
||||||
g_expectedAuth = "Basic " + base64(g_cfg.rpcUser + ":" + g_cfg.rpcPassword);
|
g_expectedAuth = "Basic " + base64(g_cfg.rpcUser + ":" + g_cfg.rpcPassword);
|
||||||
g_height = g_cfg.height;
|
g_height = g_cfg.height;
|
||||||
|
loadNoteWallet(); // FAKE_WALLET_NOTES (optional)
|
||||||
g_minerOn = g_cfg.mining;
|
g_minerOn = g_cfg.mining;
|
||||||
|
|
||||||
const socket_t listener = bindListener(g_cfg.rpcPort);
|
const socket_t listener = bindListener(g_cfg.rpcPort);
|
||||||
|
|||||||
Reference in New Issue
Block a user