From 71ed92b9f1345d50311e7f6e5046d07d22ec6fea Mon Sep 17 00:00:00 2001 From: DanS Date: Fri, 2 Oct 2026 22:15:56 -0500 Subject: [PATCH] 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 --- CMakeLists.txt | 2 + res/fonts/NotoSansCJK-Subset.ttf | Bin 766800 -> 768680 bytes res/lang/de.json | 17 ++ res/lang/es.json | 17 ++ res/lang/fr.json | 17 ++ res/lang/ja.json | 17 ++ res/lang/ko.json | 17 ++ res/lang/pt.json | 17 ++ res/lang/ru.json | 17 ++ res/lang/zh.json | 17 ++ scripts/add_missing_translations.py | 170 ++++++++++++++++ src/app_sweep.cpp | 4 + src/services/note_consolidation.cpp | 71 +++++++ src/services/note_consolidation.h | 63 ++++++ src/ui/windows/shield_dialog.cpp | 275 +++++++++++++++++++++++++- src/util/i18n.cpp | 17 ++ tests/test_phase4.cpp | 93 +++++++++ tools/fake_dragonxd/README.md | 2 + tools/fake_dragonxd/fake_dragonxd.cpp | 189 ++++++++++++++++++ 19 files changed, 1016 insertions(+), 6 deletions(-) create mode 100644 src/services/note_consolidation.cpp create mode 100644 src/services/note_consolidation.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 0623697..3bd4607 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -467,6 +467,7 @@ set(APP_SOURCES src/app_sweep.cpp src/app_wizard.cpp src/services/network_refresh_service.cpp + src/services/note_consolidation.cpp src/services/refresh_scheduler.cpp src/services/wallet_security_controller.cpp src/services/wallet_security_workflow.cpp @@ -1146,6 +1147,7 @@ if(BUILD_TESTING) add_executable(ObsidianDragonTests tests/test_phase4.cpp src/services/network_refresh_service.cpp + src/services/note_consolidation.cpp src/services/refresh_scheduler.cpp src/services/wallet_security_controller.cpp src/services/wallet_security_workflow.cpp diff --git a/res/fonts/NotoSansCJK-Subset.ttf b/res/fonts/NotoSansCJK-Subset.ttf index df8ff4da522588a4d373ab04f41618315c3b2d42..e5c963b34a690b26a4427db1bd79ee71ce958589 100644 GIT binary patch delta 27562 zcmZ_02Ury6_x^oPpV=9)V6P~Oy&}{BRf&sTup{=WWcDvmp91yy&f0yn9yGE~hn%)Vh zaSfJaemaPT#i>q+i$q z^Uk(e-uBAVr!MnL2(`7sKucJ!cXkrW8$y|#;~ehF&XqZ9Yl;U4c2bI~`z*iE*i&u8~*Uu>m z89Q6~)fH%CX2VSi2fAksocEuBfk7O&b97KSd(iyUroX$szW?z4<$G~A`(JUnlH-bb zMZLXg=lW)b;Pl#BZEHzjD8>ep!-&k>D zg^gu5+}Utz!#5jF@^O5_p$!K&?AyR<*|2p(`i9LL)^C`(A)b$Z8~SW$vZ2iSH|x@X zb&J-;^LgaD!Px<<>$)h9s;=^rSac(WNuE4rH>vE-Kr2UchCe8VK+VixZ z(|$_34Wt!Z`+jYYwcXaTzSah;t-xoGwWZePU#q3QOns5cvIbI@rH)D+nL0FeNNS(d z9;uP3%~PB4(KOXFwR&oq)Do$MQe9K?q~=V`!H1curb=r*0&8Bbd9vm)mp`sKx8}r} zeQUO?S-ED(n#F4-JJ)ns({@dXHS+2ktBDU;YfK4o;uh?HR|LsJH)3`psp(kmq-rEy9?ieF0Y6z`N8Db-Wl zQc9#0Nhz9=C&gIxa@EhPZu0T{sw=C$S#@|->Z(=1D(4bAs#POa#jF~%DqvO3RpnQe zSS79853EdDIeul`l{Hs-uk>75X=TNg9xF@mQD9|`m1gpX7-Codnqyx#Jv%PTMYaaqE$Vauv4 zE4B32((6l8mSh4;z9lP{EaP(@5=QbZab5h!;uniAE#9+u-r^yPV;6T%ezv&vVqr0o zGLtfr{!V%aBt@?HKB+k$&cLKPNi~xyC6!Amz3A4W%Zn~7nzv}?qKS*zFKV|a*TSs} z)ddF@>|C&P!L$YK7KASdT2N^IPr&?g^IhlNn73x0WA0CLZ_d3wck$f0bBE3?G{-q- z#GJu%2F&R*r~RB3a{}fxm{WI-IQ#kRnzOxTSDCHMdNJ$CtcSC1%(^t|{H(LHj?dcX zoRv0f=B(+nViI2_K2Lm{cs23+#8rt46Z&p18f=!~s1Ce0W-W8jQ>Gpf(1JfrxGLeu}5{&M=W>6fOTnSNsWs_84I zPn$kr`pD^V(|b?vF+JzBKc^j>wsqPDVA{lKBc}yVYs{nVG|y>Ord487Q9kld%R5b( z`rfXIsi|b;)cI3ePHiyNHQ|23jfCq7-z8j3IGb=L;dH{Wgrf;Z5)LOENZ6OKH(^^s zdcx*}w1nje(-IOArX&nc=$+6yp>{&egrZZnPuV1;;33q%qRy3_gAyeFGSMaP*$h+eWV$y?V5B;poKCQvCb)XYt45kH#O2 z-y6Rzerx>Z_)R>ztdCzCzb1ZF{EGNR@$=*7#?OeK9zP|1QvCS%(ed%|9pfY7OOARx z>fWeZqpplP$H#_IgGY5A)p=C-s0yRXj4C)P-^f=Z?~go_^*A~5@W=xrCy!L)cE?SR z3yo_Q*EFtvTs>!8?YN3@72*oS>3kS`*h{`k#;I{ioE#^O_;th!V8rtgPe(i+@o2<@ z5%>A(?1)o#ibfnHTSp|0m^h;Oh$bU^hCd(v?eL+)V~58Kj~*U9EcehWLyintKV;32 zWkZ$>2^kVR#Cu4|*gs=$$6kxQ5PLrMWbB?;=epP-v9Ynyu>)fV#74yi#Cpb7jjb5# z9$SPhVQE}3u5NR%#P_C(=f(2#wVs$^!n)J=q1rZqI(0eo1(p< zJ)9@WAb z6&w{9RXwUgRJo`!QH7)OcKEx)&mEq2c+lZqhm#%VbQsoQaEFKvMh7+Wb>z#)XOWL0 z4@K^e+!MJqa&u%_eGxk&c0_E7*btE#(J!KRM30D$ z5$z+|M1)1OhzN>k6j44xi$J@-+r4ggx7~_%liE4+w#(i2L)(*WkF`x~JH2f}+xWKa z+qP>P+_q`kfVO^Z>$k1d)~jum@Y~@x!cT^u2#*f$7v3$rb9njiQsE`Si-zZG^SaH; zHqYAJZL_jXpEf<)bZZmYrd=EFHtuaowJF+0XuYxZg4TYmy0(f8`!Vc#*!i&2VaLJ_ zhwTX49=1MgVc3wcXlGc@unuAE!~Dbi!s>;2hE)p79cG3}EswWMYdNE3e9K}j^S6{* zBJ@+}yU;74mqIUuo(er4dN6c<=)Tasp&LWjhPDa~4Gj)$6j~>=VyIhavCtf$R*RP{ zp0~KuqDPCaEjqPm8*(RPZOH17l_B#(=7r1(Ner18GCjl@GBTuWNa+w~!H@zWIYX=v zBlunLpTWNeKMQ^ud@A@v@X=u3V4vWc!5+^EOTYZrZhJ ziKfMx7H*oSY0joblaEc_H@Vm3K$Cq34Ge8AR#%>fGo#s|a)#03lt7!c4Spld*|-Kz326QuJ^d!-Fmm{-Kux3-u`+M>J6zkxL$SN559l-zIFOO^Zm*9itiTRVZIG~ zeSAHA%lYQ;b=1wMd#LXIx_jzwt(#hRUft<+Ti0z_H>A#qIt}Xh*72$1S;wkl)KPsh zd_MZT^?B;^$mfpFC7+o-V|-$Ldia#|$?fA(dvWdZwOwjkwT)W0YF({$q1M@2Cu*&$ z)uxtDt>U$c)GAoZs-@O^QM2=tn)hltYev;<>HW_8w)Zvfwcg3zGrXsHPw{qokMNHly0vZSi*w9{V6*Hme%>U{N+zy{_onN>@}SqZS+N1 z2K5Q-Un;NOgDt)6tvb(;vf+l(qTZMbUw%57FGR(O+9KO}IjzkXi`r*#G=AFq`ltD8 zfq)h`zhPuQQN6=OA5k4XKdj@b3QCq(U%D#ae_6Aa%K5D4v-VQC$WdKfltEmzKbY}P z8DPsTDs6U#b$l<)Ynww9TFr0PwY)Q3geZx{L^rXHSWgTRn~KfF5HVB?6FZ4r!~xqT zs7z5_DZi=p)COv>+CmLeBh@IizdBGItj4Ir)Dh}6^#{$*mRM`7UDJNlykBUqwBL1A zcj$Ta{CXk1sP3kh)hp<+`Y?Tj9Tbf~(v2%5|Z*^x82u_7u zf-r6$gz+mOOz2$Y|b+$8p7Oigkzoa)^g_`K;A)E@BqTXZy_vdLwVR`t_WdeCGrNssv{6mPD5CI7(!}m!bwxVgRpiS;fMA# zI+k`B!a8nS_Y}hV$>bxP!UjgUu>@h`rat67gw2B?Y$;AoKu8ZH#~^GCfUwO$7DLz` zL_R^-kq%*J4YCu$uKI)y?4HJp49&?|2z!S@*heS#-GQ*54j%}Ca8M<*duR}Z!#SON zVq`}mARMI<#~wpCJ_o{yX5@DWC$B>|wFAO8Qy`q~1>sB-gtK()?5_~cr9n7fn=q0K zl>EX<2p4?_qy6?2giGZiT;}ALZ$P*b3E{hr5We?>aFu z>;@;iIS<0EVDcQo?Nt!&FgenGM^YOXMDe7mWBX0@2NbEQeT<_DVj3=w1+F>E;m2 zOo8ap1!CEpOc&q z(YE^_wmSqdf{L}L107_DQJo0o?x>QHKxvnj-09N3!?_7Mm@?|TDczvU47cY`>9#s&fof|^VL!3v2 z=8u86fW@}(ABahuYcZ8x!fi|cfVgbE6XJ4uyn=<4TnXYzs!5(pP+bAcblE)yGqG#^wAeBB3 zsmvfq9&Uu~Wof)zDx~rkAXV4}sp2U}m5xHHyb4m4)sU*vk!m9#Rp)q5I#HuPBroC} z0;wh)sl^~_v#-w!NOem?@*M@KUO1%sjUhEC2g%QAL26hWl7CV17o>nkkQ%uVMje!g zyoA(b8KkD1pjl5y%~wJSW;7vGDzp@&mNXW&5mIXdQX3OeI3sK83W*nbrFOqUiZ}Cc!DA=xD07g zc}PiAVsRRzr3$2FT&|#l$qON^ybftqB%xv{e?VHJLQ18~YbQfWn+<6l?XO=7X~Pvr z8=pYh#6UMsV^mvwAf;D@wAGJr@V2>-wv!#dK-yK9(CDregs*qASa)B8w1+w0ONI6^ zs(l9`@#I3<&-5Kw4e6jap#z5~`{498X>1GZ>8@EQ1Pmpd?`n$!*ElBs64(I(FkRGs@9zKTjh(-6<1=5rJ zke&uX`k7My!n{6fNZ9B3Qb;dFNWZe`U(umg8z8+7h4dS9{2PtGp;LcQ%|B&GZ@K*) z9eB^e{)C1F7IXs>=9 z$PE-S8?xU($PJwhA^X#-fLxFReIYlh2f1-c$U!>fCVe0`T>-h-49LwxAP0-&0pyU= zkVCm1dJuBUA&|oYA-C!Rx%E8AZ89LYodmhveaP)Od1PP69fm@Vngh8bgXr`lo_mKOShCCn~^1wvMgC0VTE)F@S7v$KH zkcR|89?I>*))R(9P+$q$n(!ZUT^~PA`i$(H6SnMWJ?-D zUdsAd)(P_RdytdcLtaV8Qz+}|m5@`LKwe8V(mIeGkk_q-%>7^9Fa`2PvWc~^DT(>t zJQ4Di7|7{8AaA8)+h}zAXvjN$fV^`A4*4P@ z__ibDOLXS)e#lo`2)BQC4D$E%{3;9c8Vi&AyL|m9WbWtkk7FUp4@V5b^_wT!s8_BUAH;Qv4JG`7tASA`njWG!*jBH2Mp7 zy=QED#wcE}YG15@{E|6+B@-6U>sye2V`RTGWq(k~w;c19QN8O9`8^f;ONIP5C;UJ= zA4fs{XE)?eKSIu+5emPgRg_RDY7Hn_Cn&sVqBwFvF^5C3nnB4? z8;VPJC^;uV$@KvI;10!g8kBq6nhG#L<=Zx zlcAL4x_f=H5lZQD6M&;v*=POF*d-1*Iw{sdfm8rvpk2 zI_R|siuWEUHE%#y9m=4>PzG~bbP$x7 zsZe6iLU9hQ24&c3C?m>4i8~HuR0x##_fW>H=SD^~j#5wHWD}F1Ou7SQN?#}mSD{QB z17!xCpIH`4;wUJy7DJg$hvyE0GT#@<0tU5^lPsbVi>d6A1Sm@x$nw@ul5;~@$rPk4 zVE)(ihLU;2o(WZ|097tceut{8fT|WDe?iqb(P#ut5$_ttuxf>o1l8K-D?@tn&qL^Is&zJ z2vi@5T!LC>Db%_}$y2Dl=b+YG0JT2tHONcOK=qppr`oU$RQ~{|0o9-eRwREwZFCH3 zkO{R(d#Fv-AX|19tyR`0w>g-3!(N(huZrr)IP7E_Vb0>e?8QJ zWuOk43UzQNsL>6e#xO0h`=Ab`w8I`j9XXMUCm(tN?TrM93b%i^51~r*& zD;Z>pO1_7>nn5~KDaYE!P}hxwx}Fhi%mH=NK&YGRK;6Pf(u+XdS`+Fv52)K|Xonlr zon@i!ssWW}!|I+&Q1?=)eeAQJ;}151dguz&BR!!W{R`@G26y5e)Kd)N8;R_NdYWpU zDM#Kg|7QH|-x58a?XVk#diP@jx|`jiU(+ym+_ z^Xb*Ixlo_e+ZQdNzD$AoD`kGQ80za}sK2rBe$RyZM`Nge(#f~nE8cB@`hFYKzbN0| zC!v1m1@+?^sQ*yXPccw4yr5=Qfre(#gr(3V4Vvsq)pq2j#nzO>s&?<66 zWs&>_t;%U=Rj)#;#tEyZLi0Qit;PvxUWcH0e-EuzVQ95WL-VNwt??8dZnZn1Kedtx0WYP1`|hHV#_z7jSAJjiI%mJS~2Q z)>4KRMq0T+YfUBEEP@tJ&)XG&7EuCP`&`f>YeDNU7+Mq~2d-kh)x)#*z|`aOfz|4(QG*>4cjGPoMFXsQ-74O;9sPH01Zg*Ge{+VE}Ac!f%f zy9{kqK4|ew%jhA{#&D9cb0`%h8=o86gbvUqK7=;8Gqforfs;<%0c{$`ayQmy7KfHt z7uu|z&}JWpHs>s~xxYZ0mjP`-b7%{hdtP1F7T1Bc#Ca6jvbNBcGy3E^&{lE6)rFv~ zVOrK6gSO6sw!Sa44UAw@S@I6r<~Pu`e1x`@YHVXP+o{M7%Db}#v|Ws9Hz(b*8QNa< z**5^%eyX|uB(wtz;$RfCL)V}kX#?$;!k~`x#fh=dPBMR|n452C?DQ09XSwkl^LoBL zv(4x@R%}_c_sh=J)|+dc^gg0-!x+5j~+yPv=AXnK^t$r=GJOUL1k; zs|4*86?@G|f2XlG8=(EkT)$-+-+My)iz)fQK>lGGJ~6nA2hcJzp$l7}i#wo8DbVFb z(3RQHwPH@_`nS*>{h^z?pyv>wyO_{(mV%xu9(wM6(DST>?s^w`-fhtHor7MW8g#yu z>V+mkFWeA%kp$>P-3VV7%SmXsxH~xs-E9T*Qaho$kAYq~3VN9#&^?YoFMA4lx%<#7 zICDd8F0w|ELY)Ss+}-jb@coKEO<*i3Q@daF7l2|6!6>8+_=8wSuOjl6~) z-iLgE-nJKc0=*rVydb4V_z^lCaRYk$T5#&^IdLRNd%~z9A42ay?>fvPzd(=jAaUeJ z=pAd2xzIaN=1wEYOX!`ukh9RcG$2%}Ygw`ydba{(0r?Yp_k+-TaBPn?(0f)Ubh0M{ z@737Jr>)R?GY!2dPw$WX>Xp*>*$%z0JE0SOGobg|0=<7DGMli^fNszSiX?)(fIf&4 z4~mCAm~Df7$XLQaqJ7C)!t}<_>6p&s0`yoa7TbY1H$oqxlHTMI^r6+sT=F;cVSUL} z=)+lL!@q?-qBI#su0xLtAxz82{3MhtA@87%YDLaNkDo)HLLbf4j^@}g0pu9@2m06` zvYNRsM4Y4ew3++{eOzO*4Ep#Sq&2w)eF7t#a0dEB$~*B0^hvDbNsMfAFX&U~_!P=N zh1(N6NhVw!_6&(o>M^c3hb@(?OLlkGDZ#7w3(aX#~(_y#%;mHI3e)NBr( zO-1GuAxojp<>Yf2)jY~JkH+V*pyqcY%=3bZWCS@1ePKDm8d>-=zf~_ohLRtkCsEC$ zdE_2+UNg`aGwLPz$OOW=SUSPUr{mC<(Ys~cp)cpg<-^JE&{xzYG`fP(C3_G?o_rem zN>02ohOm8AbHd#+r6?IhsL<-#B%WM^zNQMH18bO)RDlGO4ba!Rku>6D1*f$kY+NT3 z=6u~N=32o3@a@p>J+Wc0k`kqg!S|PtQxJTzWe6 ztqP%ITlYfW#sb_%#kSprzMZ+>Zl`|xe&{=N(vL8&I~$R+(05HG8PIn#YF_!!_XH9; zv9~f=4}D()vIhEoDz={y9`GcWpdYM27C}G6ydN4z-a$W1rw?;^gsL5(?7UKJev{tcJOKR`)w-<`%6n%P^t;UQ-5JpDF;(~0LBC(0+<^XI zDD;O_$sy>EyvQc#KUE{nyU-tV^2d~imtORz3DAEoNp?c#_qh5ml=Rs|@)kPZ0rlq} zp}!ap{iPdWgugOvuZlx|t&?5Qe;Wq-{>Fz;r9WB_F8{1UsK8qm=vx-;I|k@{ z$LQZTBb?|j7R}!vze4}OTz&We{UZnd(~EHXCqKeTGH5ts5A;kL%)AYQXSxPXz!3V7 zt1!gILupNZfT6O#R)9=_)6hj2`dAo7S@HyiV+eT!!;B(aS_5F@ z$U}yaUtqWlBrjm(oJoF#kt>p%fsvb&<~{-=k1x3i!_^5RZ+UV7Mm}${9Y+4yFbcSm z12779CD+-hP&;x0M&SnJTNp)Jk-uRSO@UFY5aC3{KENoR0HZ`r!ie0w$PO4KJ;`wx zrRb!)Oy0mKJ%O-)83s`13JecM?eRT~vYe;vRv6{-lW8!@tHjxjPxP=tBeD-h#o~lf zRB|Ju$wL^GIk56c7*+C-7;*;T6c04Mscnx#n+3}-DGt;Gpy9fDE2KAB9e!SKmRI+Ber>NrSyLgRJWr!F1#twfd* z8m-rtT!v9UnEVK%!8q~`hF>6A1*2guGJ{I_E?5o~PF0&KqpMkJkyTnnQEmr-1H+y|p`D2y(C zFuJnByUm2reH@G)OJMZ!h0$jijJ~~K^rPIoVr~o=3S;0Jav#PZgLEZyZZK6GOoawB zz0t0uAK~YJBl#B7?MO7)sQDJhDxL)X+;>> z&{RS-hiQZ|52J&_OA!|AhzW#FjkpCPj%viwsknK}f80f`JcTimjU%fPHjdAo;#lpx*7a`G0&%<@h?btC6sB<3K|3SH; z?!s8U2*wK9O{UFcDzVZz6vnC`7%9zQtZoluO;Pe1M(RQsYsbJyYf9e1ST_g8dPcEv z7!2NcU~Fb8Ht&V8r6q4*q+0yq!q}D%#&(5#17imZZD)QMyJ%O=Ii(p(U2jd5}{m9(kWc0Tl!?=AC#@$Nf9~k!&VLW&VqrfgHinX z2FAO5Fy3>|`D-MMzd7MY9R|N#G(J($%v^BbG#p}i{-moP9CAxIlqqm%PO_M6=Zap4 zj3Z0QE^-$R!%XBSwMb-!$l`PWHvbn zM@|)vTm|6B?N83Zk*7Iv(vWKy9C=&Ak*_Tr`MbeUU=SPy!{I1I2MYIvqX-QYwa7_0 zilxF)oDr3%L9WB$wg`@roUr6aI7%&q!<{xtH-)22C>$PB;V4VHY=X1{~Gb!QuH29A2f#0XV!L!cof)j@ldG@S*WK z>|56)58&{92}gZKTYn-P4H&VX07t{xaQG*~5tx(w4M$@U4t@}IG$}%Uf}`mnIGQbl z)6qN;j^O!lgapCC65;%C5lcOc23d=<(U8`wuwC)W@8>TD#EN>D1299`5cN*w$+<9K9)H@7-|p;j+&=IQnus?~HKtH^?$L22jp{68RC1L4o8s z9D_M7IvS1`1{Fi6V`(q;9vnkf!7;Qx9K-Sv=MXrCyAXOlJOhpq92D1_%!6Yjr5kw? z4&J2Vh|fdrz%e=TpbC3MO8JV^U*s8;;4$|Kw+I zOql=&kD87IDlnCOr=H^2X{^|3f59<*792CYNfI10UCA0a64@z{wJ>WU9JBM1SUBb| zin)}0ZUP+h=;(Z=YJp15!?BS0UD$#gB-i0s#GEgpvWtcgra9>i9E&%?vE&gP%Syqq zJOs@hD;RmQKOFo>~M6GPC`eLmDTq)z> zAr(-QG8Hv{QdD=<;jAwTj~ciKc;yI`bLt)Tq&76-3wl=0*~+zYwE{w}{9d=h%t9_= zM?uS(r%>)Z<%<{X>M7(jeY)f;kvmV}y!xjrEqYj$a@Oh`B6KNOv1hlkMRS(Qqq|sY zh~_n6a-ICOM-BJPQ?`ZZ$Xl|ZTS={ltPv8AT<{sj7BB^~E$Pm!8Yb)ht)Ndfqk_ zdwUDv1#1-WEfG*RPeUz_Ibv0hT1AHiTSan;-38SpccDD4RovYA*A#N)XgIh)?gEay zh4W~i&U=JcvMRcGMuqmxQ9YMbx~NON@UGR1M{I87UZHK7l2r%xDpM|3Y1bTiEL|0E z<#%h~R??Lp0J*WSRxfvUM za#gTOl+B+b;ankU@3}(q)EgC5cw}yrj!Jy{Mn054dDH;!+5Z}O8M_9vGMLnrcc42N za=se*-1C*>Bd@Bds=G(o{GK&DYkGT^^sXTs2`nC7-cdS7#TG(Z6Zh$NS1AR|C0E9M zs$Sl$Q9aj+uD*fc<=q*wz9aB;6IENogP@%D11fVJ?_#7{LUl<=>cr&1mt|JLP81Tw}vz5#l{+CByA z=J%>u=tQ?>u^|s13b`|W3C(yRB;_6kQ`sfD312S$29tkbq$V4G3(e}1{ zqc<>n$>^PVL@F%R1+P5Am4_6#mZam%l)yUmtXYdy>h3NuO?E)n#KxZv2yZi@gw#*T zt@<43>gauHa3i6MfYz-uA*dPq1m(D}O=z95C0E{{d^rV=ji25NMfT46q?eF7SIpcY z<(KNf1^H2w6P5L>nPp$2X;oG19mCz*U8rH#Nli2I?6+MMzex>S`;8P5F12obDPu-! zp>IZ@P^OV&I2!1tl(9yL6op{1>pyW%#G%g@ZZJAJWq4+umMUchlm`XDpYsSbTjt-n z^0|AfYJsddq^xK0XzSLWoL0Ltm%1o*M%4>zG|{(}Smx8Timiq4j0majr#AZeIfYmMA{iwZ`=9L=OGFM9A^Pz^a*LKHQt z_)BW`(%rjcwgXGVo}XTcg+I;jEqP`vZJm+Or?u$zDN%9>+#k?2aL11BhXVITxfs`z z^LS-$mkKc`6CQRz-tOK>V>TG2d~@yHySG)Vy??bnd#+wz|Ej?wiWeImQpLY_-HR86 zE^&>7;{Oa6`im|z0~$4(HMoSIdtyY#1s;BGLl=ZJ3P`B3#n@YAi(~(+y-(_zb0p#W zq~Q(Bsuk;+gUneLZfmzno*KA-m zG^5QqmTdn9#4;>vufxx*Dk=>!mzxcf>ie6Wi!{hQWj4&b1O+)WUm_P-n0d*}YE9bb zZ_r!(HP1T+XhL=0p@s`zd`e*1I@}@W#6Q>fu?(h>|u5~XK>+q}|b`<>eMwUw=M`SxEWmz7a)r0r?VGwQdTSaP@r7CZD zVb}*Slni6|!?QAB=Vb_u4JE^E8IrCvHuEuePaB&h3T2Pv{oAxbG1$jmvvn^XWL7Sj z-DvyDqOdpotKPfp0Z9Xb%tD{lm>y(SEx=02DkEFOm*6QA`_cQPD?w&MPMW8Qxs2WI z%G=Sz?E877TT`=8z`w58MZ;12`t3{ZaEh;PGOUcO*jbC8XUWd~&8+O&vy5(PHl@up zuAXmdmgK74`dfBuR=GFJDq*`du9;b};TLJ`skYJO1Dxs|r+SGOSx(rgx{>8b_I`1T z6W=CxzBrQfxLH=I+WSZT=4OER7mN13bcz;F(VCrfPWgF{_~P^Iadxu+D@!DRpN1Oi{oK8uyrO2*Hd+IDHvNH5#@3P&qbCx}=?erzKT;fRE=r#tp zIN0o{G+;S2O!^RPR;Nravz)P=|Kg0@_y0Pd&aJ;?*|ufs5@HtlTr=ZB%mP$waY)wT z$xi6$5HpPRmu%DJ@cCd%z|cd)7>m%mNmnWaq+vz%GMtYlU(tC^lw4l9q9ckj5CjLBKV;v2A4 z+~Th=EH}#ylI3pI;r++HRznoE0<4xOWwo{1p_XWktcu>ST3-x7F3^ftpq? zt3T>k!+1}JpA~0Kfxk7)T8%I(%}PUmYlF231FYS=bTHW3XB|L{b;vq~q1FlOBt}}_ zIIXjYw=P?k`M)1zGe5O2K#2-w98$j8Z$r2vE>n4 zURu6%u^#eT^+^K~MXuV%J6bx$Ud!2QEXOU+YT0Hw|J$U0cHjm(oys!x#Y}*0h7NJE zEEoU3dN{1cS-p6xImecx3l7_be_v%elr`-EIX0K3&Tls(KgZ3~#jmU-;M+riml>t`!v*Tku;7F!cr^c2kiWRDX#2cw&^b|{Lzn9mdcc)GPZlR0bBfSR@E%0zq)5@@xKL{T_LuDDo1`P z>ur`FM>foSN2jvXX8vi^j>YyfyDFEn#jew@p4$r8PT2~4b<>W;R)B7Pc8ouD;F_Ib zThr`Uc5?07`Kqw(+6}8)mcmZ{1njf%>}c%l+f$v&n#wk3ch5Gb@Ktsl_|ar`6nsZu ze0C61*>AbEJFAki1-IS);!L&a)rzNcoYb1n$ro@vDbsnwTAk(b0hsLi#lJLm zePoSc#b+{o2oV+fH#-DcbSdow9erOIe=% zH*?u)d@-FZaa@+e%l|FOdDJP*ipxq8?@#({K70R|$Vd~}f7a(Q|97kNk~Qf+o9!du zm-3rOllE3S=2O=G&+&XQU}w=5^wH-YcJ*hwWRK5260H90(tjhi1Nz!;xxN3-vqpS2 z{(m!?EuURnUo!f2XttoMvv#|$>G;wwyY|1n5~u*rVn3%idk?U6b_#s^aQwUB%kk># z0mGgq`;F$y;4gi6hc(T8JzAd0iuO_VagC$C9zVa9?w8csdqvg}m*33O$d_ZlJm%`1 z?Wr}Fip11( zUs`Lq^2LO`6WRxi>_hVZjX3*F>5H?=t#SX!@0Xn{`=Dh{_jT{D&lz|MX7S?&Q~k&; zRi34Qx))ATBh3;y@@Jp$@brc!H+wxJO{e56X7UGIJbCeAa%!42QNXNi`k%ZU}%cxyb8t;yD8tmOI9G_0~_ScyonW?Qq7YR$ErbFtQ1Xe~vWwcJ{T&DLt3 zPHpGu)CTOaHt}bod#xSTP8{T^)ovWN_FDUK#5!ml#Bu8|Pq0q#1nUe=Tj#9vxM=Z# zOFYZ^4wrd~cn#lMH>{huYCW=k!gcFc>sS29gz)By%j?u_{_st!h>stD)s@1zL@)CRS6cxfN`+Tiv*##$4t1Z%oA-%7F8 zTN|xi)*kBs&-u<;7pxzxTh;@M|9l{8s$a6ET9(wLi`lU3DAQ?`wklYac(U2onqtkf z)>+4`Z!P;2+V<mZ1N}H$X;9V?^^P| zYy5Tb=dJb?YyJZTj!5};ZS}uvYyMs1&%ZzSTl?=?+P`c3dGzP4>-i&QnihF=!NqpZ z{A7Nzz_;X2NzHngb%jq!Q+k*s!Y1%TA+Po@RAz_QkW&BmbwU2qhkv)LtZus(|Kq{u zHW9h_u}v~F%uEZMMSzmz*3-l_0pSzHY<+67|PqUj)foW#NG1V9C+_B=WSl@9U zb~p1|SFP)&r&+@+X_mHrFv}#l^frr>cx*kfo?1U!zgW+#=hh4BC5thqmCGz*7BUN) zMU%XGn+-eE<$P4Tw) zSbQoz6JLt2#W&(x@xA!B_)+{MW=eu2ORA(hC5Pl9<(6Ef0=#rvLUNbNNadv}lDAY# zsw??Rfl{#4N{W!WNZq7fQeUaRG*B8W#Y)4ZI4NElD~*#TNYkVl(ky9?v_M)UrAVpL zI%%V{S=uV?koHIiq{Gs2>6CPu-)mfwzLTy?H>7*g1L-H}sr0k-Ome=EUQ2JJ57Iv} zWLef^LpJ5SazVMcTv{$GSCXsB-f}Iuj$BXnlLO=?a&x(b943d$5ptv)C3ls3$$jPi z@<4g894il%N64e)vGO>1k~~G8D$kT>$#dj+a;lsrZ;*G$`{iTuDfz5?LB1qkk-wMk z%a5G$WBC{PSNS*jo%~78R76EqRK=m>R`MwYmBLCHdjN`mTGIYt=eAgpmtO{t6kOZYEQMd+D{#z4pO7lSaqm6 zT#Zvlsbf^9Ize5fu2I*j8`N}lw|YQ5te#V^s@F@aH`P1pJ@ujbO#NMbt7d3|CTpsu zYYxrQa%ly%B3iMeE+fp6&PH00)=Uf0LbWihwbo9H)S|S`+IVfUHdULh&DQ2=$=Vui zt+r0vsHJP$w4K@!?WA^EJEvXLu4q@aAGDj=PukDgbM05{H|dXIMEoUBQbG^p#`}=;s&+~kFh_aOw8VpKwyT(?Q(5mGs$(6QorBYY9 zbxr8kAk{>gbej@wBF3I&gvOR78EaWXVVG=ht37J3I-qjZVRbYZaZ;z$Y0-z$ph=hTWzDIKEIF0e7wb!PHC;nCo$BiP zIzcym;tvl(5b)tUMZ8|-rd+J{LIh~~YYF{Vo0Xjtw)Im$SRb-U+3z>`lS9v#-YGiQ&L!0 z!CYdhnwqAzsb?CP1k+d+qnf+Ts9Q`sbEj!r!jXI`h5R>6>ixqx?wrkU3^fgZ#49 z4NkHAVzr_ho4O3DL7dD@)q`sx0UARyXbvr*wMYvHw+fX(nGyfwUbbq{uQi&qceL-+`G#ctTWVEw9> z+rLV}KKKH@hy(B=`~=5fI!?x!I0rLvd9aOD12^MV+>SeNFXrJ<6eL$bk7#d6CX);Zv z=`@eNq@}c+vS^K4-`Y&OD2Lnx7agJFbc)Vc<;J*5+cGhOTs7Pz*VXn~+t9YMt!-P| z{w{l;?QFZ+$7~P#lw0dca%)}v-CkFUO|$RWk@jOd#?H2xb^({*(wxqdcqY&0<(yUU zznqW&W%4c%U@L#TH`mMb7J19OEN`{9ktgs(p3JFUj(5<@^YXo;-YK4b(Q z5`U)RR6TVKPYW57r=RUip7C3p>7OOdLdxWfUsGmbWAdzsG5G?01AiVeCeMj=CeIBy zlfMW#lQYGd{&}>?^FrF>`R9mpr{2R0weuz~iuERcS=gJr_)QVbDTi(oDB8KJfB8KIyye)7nZx0;HKb-AY-Vr#Kck(V;$JxA_ z_izsX$a^W5_wjx{zz5k{;Gb+MKE!!^nDaT;R<&__gpcwu?r=QNESKUFQPtW+{`p+b za(VIWJ9apqxi&_%Cw`M`gQdscqgy}f+oE)P_&mZ*z{VonR=-QRW1EU~ zCx~~a;Ivrx{t@aO_$SJ@fzxk-ZPq^GU{P@K1)T2GTS&aBr`Irex=L%;o9p=#ylvio zFV{OMU*G~&SyfS&tLmzzYOI>6ma3h)UsU>tMCXg@RW)3_uO>)jPE#`_FITB`l9$_5 zj@qXVszWMYYh6lL(DAydZlQ0~9dsAnT|cdd>38Idc^~LedW@c=r|KDcmS}B}UZ$7p z)xKWG3-k{8FnT~A5s#fVO8iyHR5x*^mZ>Avcb#crZZJ2RTg~m}Zqv~`U>-7w=AWj! zNiu!R0Fz?gGVhyFW(qGc8D@@jU6zy62D8a*G5buOIbx2RQ{aJxQcxDkLnTSN8W0cl z;TrH;!A;N({tjK?F?bT5fh2eV2ExlQ1ct*%_yp2LHCYm8+u;Y;3A^DCuY_ar9TtUv z7h*ZAh!9_!*&cr{*wf5S%D1h2#9*b;BRHh44MiS6+|d<^?yG7iL#aXe1M z>6n3^`#2Zp;X?cpm*UsB5?A8}+=4rCH|F3zJb<})7-cJtE})C3ywk=l)Q&n(CwhP$ zrUCRay-un8z1Uz1&8LO5n3mDkw361+x3rPAP&WNY`{^gjqoZ_!evub2ZOO7NYs=Y6 zwz9qcGF#o&w6*P(w!Up_o7%^1FZ-PBYX{gt_BA`izG2_AZwc?`+Ie=Y8WFs9;_*mF zrG551-|h!sZz0_j)Ka1}lCNHoDjKQAyC#}ZP(xdz9VG3;5201j{Ut|+nUUsWH+4HxQY18TyT+_H8_j03R|+Iw8swxj2m#C3Z8=vUL3>;c z4Wv14bO=m@?$Ekkg+#W)Fz719#h;Ct91HIWAa)hoBkCQ7x+NQXzS;n@9x&d%b;Z{ovz& ziXCi++EhDCB4CcqbRYeLuW+SDKJ@=Zsj?n8?$qtaal)MtbwHIC;?$JKuMlrStVt2p zOmLi8pq2=0Ry)?LFT|RP!kRilo9lJU7~b^L?+I@{6yC(rX0`rCZwRC2h(7HbWsD=u zrA3nF9wAM@nI!X?K9U@+Lh;V?2 z-~sL^07L_yn-u^e3WxFXSTdu~Q%S)iNdX+{3Lb)O>nI@l>fvEP1dG-YJVahK0yyM{ zpD_Y7R1T5Bm-ma%>qQg|56Zhm?=TQ1Iv~sz1cZ)1boRl6LvZ%D$+JTA;0MLrdgu{h zxm^54cklb$#h-ice(-D%d+@=TzbKCZ!BfE5#~qw@!C@D9*HvOJJ95{-O^^NctG>6$ z+W&uN9lhz$MVEG8$xM%Cd|LBluIW|I=$gz+E!37lvN{>HQ@AR4*JZRB%8dx(-QRk@ n&GBW@s!m{=Hfl6q3<>hrhH0}#^HA@Rj5cF94l)wQ^8xxFU0#+o delta 25859 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 diff --git a/res/lang/de.json b/res/lang/de.json index 10df873..0a4a69b 100644 --- a/res/lang/de.json +++ b/res/lang/de.json @@ -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_fee_hint": "Netzwerkgebühr für diese Transaktion.", "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_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.", @@ -1175,6 +1191,7 @@ "merge_src_transparent": "Transparent", "merge_started": "Zusammenführung gestartet", "merge_title": "An Adresse zusammenführen", + "merge_where": "Wohin sollen die Mittel gehen?", "mig_adopt": "Dies zu meiner Wallet machen", "mig_adopt_now": "Seed-Wallet übernehmen", "mig_adopting": "Ihre neue Wallet wird installiert und neu gescannt…", diff --git a/res/lang/es.json b/res/lang/es.json index 06533be..3857954 100644 --- a/res/lang/es.json +++ b/res/lang/es.json @@ -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_fee_hint": "Comisión de red para esta transacción.", "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_no_spendable": "Aún no hay entradas disponibles para consolidar.", "merge_progress": "Consolidando… esto puede tardar varios minutos. Puedes cerrar esta ventana.", @@ -1175,6 +1191,7 @@ "merge_src_transparent": "Transparentes", "merge_started": "Operación de fusión iniciada", "merge_title": "Fusionar a Dirección", + "merge_where": "¿Adónde deben ir los fondos?", "mig_adopt": "Hacer que esta sea mi cartera", "mig_adopt_now": "Adoptar cartera con frase", "mig_adopting": "Instalando tu nueva cartera y reescaneando…", diff --git a/res/lang/fr.json b/res/lang/fr.json index 4ba91c6..bb25d3a 100644 --- a/res/lang/fr.json +++ b/res/lang/fr.json @@ -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_fee_hint": "Frais réseau pour cette transaction.", "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_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.", @@ -1175,6 +1191,7 @@ "merge_src_transparent": "Transparentes", "merge_started": "Opération de fusion démarrée", "merge_title": "Fusionner vers une adresse", + "merge_where": "Où doivent aller les fonds ?", "mig_adopt": "Faire de ceci mon portefeuille", "mig_adopt_now": "Adopter le portefeuille à phrase", "mig_adopting": "Installation de votre nouveau portefeuille et rescan en cours…", diff --git a/res/lang/ja.json b/res/lang/ja.json index 3ad1ec4..2e3f362 100644 --- a/res/lang/ja.json +++ b/res/lang/ja.json @@ -1163,6 +1163,22 @@ "merge_description": "複数のUTXOを単一のシールドアドレスに統合します。ウォレットサイズの縮小とプライバシーの向上に役立ちます。", "merge_fee_hint": "この取引にかかるネットワーク手数料です。", "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_no_spendable": "統合できる使用可能な入力がまだありません。", "merge_progress": "統合中… 数分かかることがあります。このウィンドウは閉じても構いません。", @@ -1175,6 +1191,7 @@ "merge_src_transparent": "透明", "merge_started": "統合操作を開始しました", "merge_title": "アドレスに統合", + "merge_where": "資金の移動先", "mig_adopt": "これを自分のウォレットにする", "mig_adopt_now": "シードウォレットを採用", "mig_adopting": "新しいウォレットをインストールして再スキャン中…", diff --git a/res/lang/ko.json b/res/lang/ko.json index 947a9eb..50065c1 100644 --- a/res/lang/ko.json +++ b/res/lang/ko.json @@ -1163,6 +1163,22 @@ "merge_description": "여러 UTXO를 단일 차폐 주소로 통합합니다. 지갑 크기를 줄이고 프라이버시를 향상시킵니다.", "merge_fee_hint": "이 거래에 대한 네트워크 수수료입니다.", "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_no_spendable": "아직 통합할 사용 가능한 입력이 없습니다.", "merge_progress": "통합 중… 몇 분 정도 걸릴 수 있습니다. 이 창을 닫아도 됩니다.", @@ -1175,6 +1191,7 @@ "merge_src_transparent": "투명", "merge_started": "통합 작업이 시작되었습니다", "merge_title": "주소로 통합", + "merge_where": "자금을 어디로 보낼까요?", "mig_adopt": "이 지갑을 내 지갑으로 사용", "mig_adopt_now": "시드 지갑 사용", "mig_adopting": "새 지갑을 설치하고 재스캔하는 중…", diff --git a/res/lang/pt.json b/res/lang/pt.json index 0a814d5..29b16ad 100644 --- a/res/lang/pt.json +++ b/res/lang/pt.json @@ -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_fee_hint": "Taxa de rede para esta transação.", "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_no_spendable": "Ainda não há entradas disponíveis para consolidar.", "merge_progress": "Consolidando… isso pode levar alguns minutos. Você pode fechar esta janela.", @@ -1175,6 +1191,7 @@ "merge_src_transparent": "Transparente", "merge_started": "Operação de fusão iniciada", "merge_title": "Fundir para Endereço", + "merge_where": "Para onde devem ir os fundos?", "mig_adopt": "Tornar esta minha carteira", "mig_adopt_now": "Adotar carteira semente", "mig_adopting": "Instalando sua nova carteira e reescaneando…", diff --git a/res/lang/ru.json b/res/lang/ru.json index e7530c9..5b0e217 100644 --- a/res/lang/ru.json +++ b/res/lang/ru.json @@ -1163,6 +1163,22 @@ "merge_description": "Объедините несколько UTXO в один экранированный адрес. Это может уменьшить размер кошелька и улучшить конфиденциальность.", "merge_fee_hint": "Сетевая комиссия за эту транзакцию.", "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_no_spendable": "Пока нет доступных для расходования входов для объединения.", "merge_progress": "Объединение… это может занять несколько минут. Вы можете закрыть это окно.", @@ -1175,6 +1191,7 @@ "merge_src_transparent": "Прозрачные", "merge_started": "Операция объединения начата", "merge_title": "Объединить на адрес", + "merge_where": "Куда направить средства?", "mig_adopt": "Сделать это моим кошельком", "mig_adopt_now": "Принять сид-кошелёк", "mig_adopting": "Установка вашего нового кошелька и пересканирование…", diff --git a/res/lang/zh.json b/res/lang/zh.json index feacb31..c91879f 100644 --- a/res/lang/zh.json +++ b/res/lang/zh.json @@ -1163,6 +1163,22 @@ "merge_description": "将多个 UTXO 合并到一个屏蔽地址。这可以帮助减小钱包大小并提高隐私性。", "merge_fee_hint": "此交易的网络手续费。", "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_no_spendable": "暂无可合并的可用输入。", "merge_progress": "正在合并…这可能需要几分钟。您可以关闭此窗口。", @@ -1175,6 +1191,7 @@ "merge_src_transparent": "透明", "merge_started": "合并操作已开始", "merge_title": "合并到地址", + "merge_where": "资金应转到哪里?", "mig_adopt": "将其设为我的钱包", "mig_adopt_now": "采用助记词钱包", "mig_adopting": "正在安装您的新钱包并重新扫描…", diff --git a/scripts/add_missing_translations.py b/scripts/add_missing_translations.py index c8908fa..11dac15 100644 --- a/scripts/add_missing_translations.py +++ b/scripts/add_missing_translations.py @@ -25,6 +25,176 @@ def load_english_keys(): # All translations for the 158 missing keys 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": { "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.", diff --git a/src/app_sweep.cpp b/src/app_sweep.cpp index c2a369b..279c6c1 100644 --- a/src/app_sweep.cpp +++ b/src/app_sweep.cpp @@ -401,6 +401,10 @@ void App::buildSweepCatalog() add("modal-merge", ui::NavPage::Send, [](App&) { ui::ShieldDialog::showMerge(); }, [](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. add("modal-transfer", ui::NavPage::Overview, [](App& a) { diff --git a/src/services/note_consolidation.cpp b/src/services/note_consolidation.cpp new file mode 100644 index 0000000..811f2cb --- /dev/null +++ b/src/services/note_consolidation.cpp @@ -0,0 +1,71 @@ +// DragonX Wallet - ImGui Edition +// Copyright 2024-2026 The Hush Developers +// Released under the GPLv3 + +#include "note_consolidation.h" + +#include +#include + +namespace dragonx { +namespace services { + +std::vector groupSpendableNotes(const nlohmann::json& zListUnspent) +{ + std::map 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 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 diff --git a/src/services/note_consolidation.h b/src/services/note_consolidation.h new file mode 100644 index 0000000..bae82df --- /dev/null +++ b/src/services/note_consolidation.h @@ -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 + +#include +#include +#include + +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 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; + +// The outcome for one address. +struct AddressRun { + std::string address; + std::vector 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 diff --git a/src/ui/windows/shield_dialog.cpp b/src/ui/windows/shield_dialog.cpp index f7961fc..c9f5124 100644 --- a/src/ui/windows/shield_dialog.cpp +++ b/src/ui/windows/shield_dialog.cpp @@ -8,12 +8,15 @@ #include "../../data/wallet_state.h" #include "../../rpc/rpc_client.h" #include "../../rpc/rpc_worker.h" +#include "../../services/note_consolidation.h" #include "../../util/i18n.h" #include "../notifications.h" #include "../schema/ui_schema.h" #include "../material/draw_helpers.h" #include "imgui.h" +#include +#include #include #include #include @@ -44,6 +47,18 @@ static bool s_scope_loaded = false; static int s_t_count = 0, s_z_count = 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) +// "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 s_groups; // spendable shielded notes by address (scope load) +static std::set s_selected; // addresses ticked for per-address consolidation +struct PerAddressRun { + services::AddressRun run; + std::vector state; // per opid: 0 = running, 1 = done, 2 = failed + std::string failure; // first failed op's message +}; +static std::vector s_runs; // after submit +static bool s_runs_submitting = false; static void resetTransient() { @@ -53,6 +68,8 @@ static void resetTransient() s_status_message.clear(); s_operation_id.clear(); 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 @@ -63,6 +80,7 @@ static void loadScope(App* app) s_scope_loading = true; s_scope_loaded = false; 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; + std::vector groups; try { rpc::RPCClient::TraceScope trace("Shield dialog / Scope count"); 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 (z.value("confirmations", 0) >= 1) { ++zC; zA += z.value("amount", 0.0); } } + groups = services::groupSpendableNotes(zs); } 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_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). const bool tOk = tC > 0, zOk = zC > 0; if (!s_operation_pending) { @@ -108,6 +131,9 @@ void ShieldDialog::show(Mode mode) s_scope_loaded = false; s_scope_loading = false; s_t_count = s_z_count = 0; s_t_amount = s_z_amount = 0.0; s_last_poll = 0.0; + s_per_address = false; + s_groups.clear(); + s_selected.clear(); } void ShieldDialog::showShieldCoinbase(const std::string& fromAddress) @@ -128,6 +154,7 @@ void ShieldDialog::showConsolidate() show(Mode::MergeToAddress); s_consolidate = true; 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() @@ -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; } +// 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) { 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 selectedGroups() +{ + std::vector 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 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(); + const std::string status = (op.contains("status") && op["status"].is_string()) + ? op["status"].get() : 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 // shows "Consolidating… → Done/Failed" without a manual button. (The shared poller also tracks it for // balance refresh; this drives only the inline display.) @@ -290,6 +440,7 @@ void ShieldDialog::render(App* app) const auto& state = app->getWalletState(); autoSelectDestination(state); pollOperation(app); + pollPerAddress(app); if (isMerge && !s_scope_loaded && !s_scope_loading && s_operation_id.empty()) loadScope(app); // ── Description ────────────────────────────────────────────────────────────────────────────── @@ -298,7 +449,9 @@ void ShieldDialog::render(App* app) : TR("shield_description")); 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 ────────────────────────────────────────────────────────── if (isMerge && !opInFlight) { @@ -313,9 +466,17 @@ void ShieldDialog::render(App* app) } 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. const bool tOk = s_t_count > 0, zOk = s_z_count > 0; - if (tOk && zOk) { + if (tOk && zOk && !s_per_address) { ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(TR("merge_source")); ImGui::SameLine(0, Layout::spacingLg()); @@ -324,6 +485,46 @@ void ShieldDialog::render(App* app) ImGui::RadioButton(TR("merge_src_both"), &s_src, 2); 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(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 ─────────────────────────────────────────────────────────── @@ -335,6 +536,7 @@ void ShieldDialog::render(App* app) // ── Destination (z-address) ───────────────────────────────────────────────────────────────── if (!opInFlight) { + if (!perAddress) { ImGui::TextUnformatted(TR("shield_to_address")); if (state.z_addresses.empty()) { material::Type().textColored(material::TypeStyle::Caption, material::Warning(), TR("shield_no_zaddr_hint")); @@ -381,6 +583,7 @@ void ShieldDialog::render(App* app) } } ImGui::Spacing(); + } // !perAddress // ── Advanced (fee + batch size) ───────────────────────────────────────────────────────── 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. - 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]; std::snprintf(hb, sizeof(hb), TR("merge_batch_fmt"), s_utxo_limit); ImGui::Spacing(); @@ -415,6 +627,46 @@ void ShieldDialog::render(App* app) 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(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 ────────────────────────────────────────────────────────────────── if (!s_status_message.empty()) { 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 // 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(sel.size()), + fmtAmt(s_fee * batches).c_str()); + ImGui::TextWrapped("%s", cb); + ImGui::Spacing(); + } else if (s_confirm && isMerge) { char cb[200]; std::snprintf(cb, sizeof(cb), TR("merge_confirm_fmt"), 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; 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; material::BeginOverlayDialogFooter(footerBtnW, /*drawSeparator=*/false); if (!can_submit) ImGui::BeginDisabled(); 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 } if (!can_submit) ImGui::EndDisabled(); if (!can_submit && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) { if (!connected) material::Tooltip("%s", TR("send_tooltip_not_connected")); 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 (isMerge && srcCount() == 0) material::Tooltip("%s", TR("merge_no_spendable")); } diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index 9978651..d58232b 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -2519,6 +2519,23 @@ void I18n::loadBuiltinEnglish() strings_["merge_back"] = "Back"; 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_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"; // --- Transaction Details Dialog --- diff --git a/tests/test_phase4.cpp b/tests/test_phase4.cpp index 35f2653..e2d663b 100644 --- a/tests/test_phase4.cpp +++ b/tests/test_phase4.cpp @@ -28,6 +28,7 @@ #include "resources/embedded_resources.h" #include "services/network_refresh_service.h" #include "services/refresh_scheduler.h" +#include "services/note_consolidation.h" #include "services/wallet_security_controller.h" #include "services/wallet_security_workflow.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. void testMiningAddressStringsTranslated() { @@ -4616,6 +4629,84 @@ void testDebugLogTailRules() 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(notes); + auto calls = std::make_shared(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(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() { using dragonx::daemon::EmbeddedDaemon; @@ -9233,6 +9324,7 @@ int main() testConfSetsKey(); testOwnershipRecord(); testDebugLogTailRules(); + testNoteConsolidation(); testStopDeliveryPolicy(); testProcStatParse(); testStopProgressTracker(); @@ -9245,6 +9337,7 @@ int main() testChooseDaemonStatus(); testDaemonStopStringsTranslated(); testMiningAddressStringsTranslated(); + testConsolidationStringsTranslated(); #ifdef _WIN32 testWindowsStopNeverKillsForeignDaemon(); #endif diff --git a/tools/fake_dragonxd/README.md b/tools/fake_dragonxd/README.md index 7dd1688..fa96a38 100644 --- a/tools/fake_dragonxd/README.md +++ b/tools/fake_dragonxd/README.md @@ -120,6 +120,8 @@ inherits the GUI's environment. | `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_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 | ## Test cases diff --git a/tools/fake_dragonxd/fake_dragonxd.cpp b/tools/fake_dragonxd/fake_dragonxd.cpp index ce24e84..5f2c056 100644 --- a/tools/fake_dragonxd/fake_dragonxd.cpp +++ b/tools/fake_dragonxd/fake_dragonxd.cpp @@ -407,6 +407,187 @@ struct Canned { 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 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 g_notes; +std::vector 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 noteAddresses() +{ + std::vector 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(); + 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 from; + bool anySapling = false; + for (const auto& a : params[0]) { + if (!a.is_string()) continue; + if (a.get() == "ANY_SAPLING" || a.get() == "*") anySapling = true; + else from.push_back(a.get()); + } + const std::string to = params[1].get(); + const double fee = params.size() > 2 && params[2].is_number() ? params[2].get() : 0.0001; + long limit = params.size() > 4 && params[4].is_number() ? params[4].get() : 90; + if (limit <= 0) limit = 1000000; + std::vector 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(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(picked.size())}, + {"remainingShieldedValue", remainingValue}, {"mergingUTXOs", 0}, + {"mergingTransparentValue", 0.0}, {"mergingNotes", static_cast(picked.size())}, + {"mergingShieldedValue", merging}, {"opid", op.id}}; + } else if (m == "z_getoperationstatus" || m == "z_getoperationresult") { + std::vector filter; + if (!params.empty() && params[0].is_array()) + for (const auto& id : params[0]) if (id.is_string()) filter.push_back(id.get()); + json arr = json::array(); + std::vector 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 c; @@ -500,6 +681,9 @@ Canned cannedReply(const std::string& m, const json& params) c.result = json::array(); } else if (m == "setban" || m == "clearbanned") { 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") { c.result = json::array({kFakeZAddr}); } else if (m == "getaddressesbyaccount" || m == "listaddresses") { @@ -589,6 +773,10 @@ json execOne(const json& req, int& status) std::lock_guard lk(g_csMain); c = cannedReply(m, params); if (m == "setgenerate") g_minerOn = !params.empty() && params[0].is_boolean() && params[0].get(); + if (g_noteWallet) { + Canned w = noteWalletReply(m, params); + if (w.known) c = w; + } } if (c.errCode != 0) return fail(c.errCode, c.errMsg); 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_height = g_cfg.height; + loadNoteWallet(); // FAKE_WALLET_NOTES (optional) g_minerOn = g_cfg.mining; const socket_t listener = bindListener(g_cfg.rpcPort);