From fae8b20fb7932c7197b018a8c7c1ad6db1f9513a Mon Sep 17 00:00:00 2001 From: DanS Date: Fri, 4 Sep 2026 04:02:27 -0500 Subject: [PATCH] fix: FAQ CJK glyphs, outgoing-tx history, lite encryption, migrate & sync safety MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug-report fixes plus adversarial-audit follow-ups. Bumps full 2.0.1->2.0.2 and lite 1.1.0->1.1.1. i18n / fonts: - Rebuild the NotoSansCJK subset with the glyphs the FAQ back-fill introduced (什/么/门/做); Chinese FAQ titles rendered as "??" on zh (and ja/ko). Transaction history (enumeration gaps): - Surface outgoing shielded z->z sends via z_listsentbyaddress (listtransactions and z_listreceivedbyaddress never report them). - Surface z->t deshields by parsing z_listsentbyaddress transparentSends (which z_viewtransaction does not expose); dedupe t->t against listtransactions. - Key send-row dedup on address so equal-value multi-output sends aren't dropped. Lite wallet (key safety): - The async create/restore/open path the UI uses now applies the passphrase: encrypt a new/restored wallet, unlock an existing one. It previously discarded the passphrase, storing the seed/keys in PLAINTEXT (create/restore) or leaving an encrypted wallet locked (open). Migrate-to-seed (fund safety): - Persist a sweep-submitted marker at broadcast so a resumed migration can't treat a ~0 balance (an unconfirmed in-flight sweep) as an empty wallet and adopt (swap wallet.dat) before the sweep confirms; best-effort locate routes to the confirm gate. Sync / threading: - Gate isSynced() on a peer-derived tip (tip_known) so a peerless node isn't reported synced (Send against a stale balance); add a "no peers" status banner. - Guard daemon_status_ with a mutex (monitor-thread write vs UI-thread read). Tests: extends tests/test_phase4.cpp across all of the above; suite green. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01BwycPDQSfEKtrTS2uC4JeT --- CMakeLists.txt | 4 +- res/fonts/NotoSansCJK-Subset.ttf | Bin 683312 -> 761920 bytes src/app.cpp | 76 +++++-- src/app.h | 11 +- src/app_network.cpp | 76 +++++++ src/config/settings.cpp | 2 + src/config/settings.h | 7 + src/data/seed_migration_resume.h | 8 + src/data/wallet_state.h | 8 +- src/services/network_refresh_service.cpp | 97 +++++++- src/services/network_refresh_service.h | 6 + src/ui/node_status_banner.h | 29 ++- src/util/i18n.cpp | 4 + src/wallet/lite_wallet_controller.cpp | 63 +++++- src/wallet/lite_wallet_controller.h | 14 ++ tests/test_phase4.cpp | 275 ++++++++++++++++++++++- 16 files changed, 646 insertions(+), 34 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 70df660..8ac50ff 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,7 +15,7 @@ if(APPLE) endif() project(ObsidianDragon - VERSION 2.0.1 + VERSION 2.0.2 LANGUAGES C CXX DESCRIPTION "DragonX Cryptocurrency Wallet" ) @@ -26,7 +26,7 @@ set(DRAGONX_VERSION_SUFFIX "") # ObsidianDragonLite is versioned INDEPENDENTLY of the full-node app above. The active variant's # version flows to the generated header, the Windows .rc/manifest, and build.sh's release names via # DRAGONX_APP_VERSION* (resolved in the lite/full block below). -set(DRAGONX_LITE_VERSION "1.1.0") +set(DRAGONX_LITE_VERSION "1.1.1") set(DRAGONX_LITE_VERSION_SUFFIX "") # C++17 standard diff --git a/res/fonts/NotoSansCJK-Subset.ttf b/res/fonts/NotoSansCJK-Subset.ttf index f751d672ab944f10d3655de73ffd8b44ee057bb5..d8acc07bfce89121396866b6516cdb7717e79c07 100644 GIT binary patch delta 105103 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 z zG=t}i?a*wppxNGmmhUyR{7;}2tPQQu7-)ruK`Zhzv|>%66<-CdqzbK61!$!QK`YY= zT3IJ(b~T`tD??sdpq2ju&Hg?#hqKTeDXhXXXikryRs0OCvM01EKG3QLL94b1TJW+X`uN$=b1E9HtL38C|yri{;5zrb30nr|;? ze!0*((TV^n)VT_@ATAiZ5Lze|=~5J0Sb1pS9M?4jTDLCHy3d2wBL`Z~-=Xy?39a{7 zXnlTy*7qSaOTQ1$2DE@Sa3Hin%b^YCB13jV8^$n1Hi0&LDzp){&_>RHHj0eqqS3dY zjp5|6^jwS=wDEnQ#mHKRFacT;t)IoP z%w7(QHrE51Sr}R}W4NFJw1pVDQKSqwEa5aao${L2dLmdhUCyeXoorP@B(N@IG%61HQuCZ z$NG~`&`uO1^wLRs<+Mb&&KY{+?0#tHxPPHBw2PK$&@M4SE+;^{as%2mYIS`Gv>V%? z{qzyqt&Y%cbJ4pLb}tp$&vfI1U!Xm#2JI1(@-Z!W!o+$?>z-YM_WUih-_xPJ%z*Za zq55Mcv_HAV8_s`w7~1;?XdgB}`^d0=78$NDrJ;R|g7&Q^bS!`_T!t>Lg)ZemSE@i) zFG1JFLDw5WH{zh%lq5X1Rmn8ye95He+YP-yGC%iN554e6=tTmc7aa(_*f!|J_dqXk z75o+jdg)%!%N&Pp=Lx;sL+IsWVA1VcL%06|-Qg5;M=o078gwTv%JQmLE)KoQZ0J?* zL$Ag~otr?f(F1zTW6*1Jd>t-Q*A{v`3-tQGLwDu)27RG790I)&m2dO}x?4SxK)7Jz zwq!H(Cf?)*^rkI2u_-lgdK-E(H?jhHbCFCW)VjrFashhFx?~n~{^e9}wSgbC&;s}6 z>L2u2r^)LLesv)7$ZO%b&TZ?evw(p>O`4JlBbsc&;3$<%EmE48i zz8o1w&O+}{olJ-BO*eZ-k_XT`29y2JeHsvY%C|gO4Bf8)NhD99`)5P%#JQc8Ko6)$ zsAK>w4{S!(K<~^jbf$MY{{cOSp0@<8gC1OlP>J9-&_mLphc+eC2nThUM;=2Diy^ci zobC>1U-${=UAb`A0c1X5$htWZTGs6h^zQa#D53Q|+7l|;;{)`b%(I^KXaqwUk-_-) za)sV2iQIpI`p=~1*WiUXpk&Mhdzn!oW!K$UpDp0^wQ)Rgz=uz zkuVo{ccf2U0eu?VX@d!si)VP^d2YHRSw3Xxt2jAc2Nk5{7e!3n$tUo=AKP zJt>~>5}R3?FupS%K%Z5aM3WoPXH$XMqsT_`4*Hxxatiufe?r&K{RVv=`{r%oXJ5q# zW1n1>(6Hp=(C5>`^TS9kSPvleq*Ai$3kCSj_iWIhC5gV!a zM!I(smEH6S`sSYG3iK`Q$vNnmOy10=(6c;=g)YiwQf1SXIU?yxPC(z8fM2?${&f#|J~_>lXdQV(2I96DoBo z0s85ZWH$6ODoKQXwh`gt=a>WM-atS9GxQ7Np#ws+=3XGPTSGIaNtANOeCeOiB? zSJTg2=;s{h55l1H23G&&H1vm^p+DlhM+cz)%JaW2h5ndpJWgi(e=AHV@JRzg0Z$qG zr-z_FV_2T0L4Qt5pD&00f{MIg`#Z1k-#0^lS(kA9D_%9P?nD2hE4cyv&q>f$e7zH_@;9h>VS(j{uQMf+Y38P3M5>1}KDB6!4f>Epn zSp%bZX<~sB~r zl{lty4Qg3=Ciw(|f7LdsoP<%e4%q~wS|f5FM)hE_8U~;54d>zHI*b|=P-7{KnpB|X zSds^$Ruw{PYkh!GI}Qe)3yeB6q;4~E2}ZpkFzR!B{VW(R^tx*xj0P53-hc-UX?4S+ zFd9X`a4Sf-2;Tu1jqkx|!t+fVliy%8TLPmwL(+oEwV=?JsW4hqC0xjzeeSf>qb}JF z!;@`mwrx(p@ah4hoj;8BRJOx%7~XSXbX?E)`*wukKMh7F#xj751kz=l>Czy^I_M<~ z-WwRfQDi@ikg}vJIR_(@aSV+iOr|dMOqbHcmrzz1kHhGJuyk^Ye1Q>Oiwq>|$XUX$ zcIDsyjIQ;#(~BgNZG`ddR*G~W)UZ1fuKRfyJxoHSdOU^ElZo1sO7*1GJ#UkDFe2C& z(UPz)g5!J9E4>)DUe96lW(a%VfzhWZ$%WC^lh6ZwpELgbXjQ*fgz@ReLH&1-uP_EM z6a(_eM;N@pFb2|n19uW8)*yyxkQd>6J~bJGzQGvWn$WtzTyV%JauCMQ_GBnw7>2%r zF{}@vCx?B85y^asw6x=o{v?5HCdbHq7{i(A!;ip-aw4>RgiIK!kxb5!BgrNhqg29> zjT%9If-$;0nMj_%hz=qPVT>t2x)FM1%q zh$pDngm@SevtdkP|KwdTro4bLbuEl(6dX^X@$|&>sW4`Qz(`Kn{2ezu+jU%Vq^4%jd2P#Hho}YTNyU_O2a0< zJ!}euz^0%RYzk3W;bO2U(t^BzP0=f`DdtL+!lrm*vIjOLhLiWODY+FkrPjfwbTn+r zoQ6%=*RZimfK9n9*w`0>jl)>jI8wGF$5yxr8>dVQY%1o$rqXrTR1Sbm6@y%WO*I`h z)rZ2y`7Uf~oPbTui?FH1#cMx>O`ZS4*mnT7RbKnw?_Nn)y3*C&*50yYO-r(6$(FU{ zZD;Sj6UYDpAw-Y?Nq~eT1u;SyX$lkwVH!FDVTaiQZ78Eq%BDP^uPrS_H~({GI|N?) z-v2tEY%8+;o!>d%+2`thj0#_YQ4!d1bPh(fF2bmo1{VBWdPTQzY7nE+*J4!X)fm-P!Ke%gs|vg5hFy2pF)G^&tAHaLV^rVG7}XEI1F9y}zycVq02Bvz zVAK$N9(o$1hT;E5EUOEipsK04FJaU?_?h`luol7sW*MN*k2IjVlgb?jwLjt=K1Tcb8p|?b9dp0?Vh6; z!cy>xZzo=na3?gX#h&fkJ=gY_kKSWDQn)|4({O()fpwqyC-GApnk_IA6JnUcs1$ zi1p0JUnuoWv+{(5ADbl>3lAeM$I=;-wy1hMvhIYyEKa6$_$WMU?vS*Id>UDITwtc# z;mL!&C_IV>FE5ubufJ<|Q38(4C&dvZLciBW^!CAuG3jzG4`(h)h|Ge-CEGi+t$OEx zF{RCzyDp56KUaU65MF-y=#dP%j#~|#dI-qL0%Mw4jK%oajuiX*d*L>hJz z`1iZvvU`Gkush=6q!ae@i~jJo1HF`u?%8+Y$GPCVL2ZdL%MQGH)$CRS#ZmG#*??}| z1=aTc4Szo$CoCl*ZS22!f4k61#jd#4$%JifmuwDuG;yOyF5>gJ96BWBM!l-J+iw_g z9c2Et%o|=XnlGY9ND~(eow}R&1yRNlSO?aH$yU6i`dpSM~0e2&;bBZMeAkK-k{z9|;B_Cj+Y){ZHO zdq+6~s7}tuF~_N+u-!Z+!J1e&K^$dv*|_uh7SZPgin}pH1!)V3; z5;K1pU_M4#BCm?Ah}0+H?Y$jzhz@VPy2KpqVMOS!Jr7MqBiyB$Q}+>15k>eLCJb|0 z**5=};2mi|6uVctd3`c2KHmP}83if0rimQJ&wn=7b=d9}jKFB7XZ zYnRn$;%)uybdU~jyJmE77P?7N#W6XM^z#Nu$p%RcipEJ&d`}ldK^H_pZT%aq0Vk%^ zk}AWI;pwUHoPiG$ny}VAU}dfso23svXT9$!LysQ#^pX`_!mM(jZc!|7Ueem$u54?X zz8D}`zq~#NkJ>u*V%Xiq`|CRbVzbOU-RWKsD{U;EO)V*IEN$dkW0!X7g32~yrDyc> zmFP)cRe9=u;t9}aCM*cMZ=A6xt36b9cx1&0w#*7s0C!JtYn(RpM#MF_PwW3X^Vk3S zFVs}ui0mJK%={fq*A@pqDXLnH<)(xYsEN)SCe*E}SX>h+zg7CS1pnSDYhtZ=b?fp- zwLXGpoLM6ShHe@tog z$U0=_R=Jq+)a@WqP>I5%m#$c2Y5E? zJqsNs`;s0*V2NF)>&~6Rj)_FaN~Pz@%++XB=F-g(kyX&Vazg_5vM_%S<$aClO14IKX|#St9K<>t>{^ zcH$S;FZSbpM#n)6C6NZ~h~{hTq-bvYoVHnAbS%hsh};1USwuJL=FVj)diflH(}OE?2Xb*s4)j&Yl)$eh7@(2d5~ zvzbZg6G|-cTX7MoaqvX5TW3V4q*7C+?3{tWrHQCyy0q@E>AhF)?WBy{uDw@Zoj$FbWGx|kTF0f0h{-x}?gU|$Y62L>>VBjxsbl6+<5*CU*#r3e z*3N^skD%W$K_vHiDHm;V$+=S>Jj7gwb~>j`V@A-I0*S_F9U2pXry|X~+c=3yNR1?U}lW7xwTCjlm!={Is>of)d zpAZ9`!gYrZ-F4UTQ=2wz!Z;v?_Ylv62}@#~(8&$m#tj0awa1_OIA!36`;GRQNzx?jUSKyk zg{mg``?No*HJp+~|}{ zQFm#=E%KGSg&}p?(kJpNQqg_$sCcQ;PV*%Mv+|~Pb<9HQg_gKl?GQWjVnP@&vOdqUEY48`=e5++c(&X|N9YoCHDT6M_kzMNfwdlUVPF_kF(Kgm zhO&&uonw%^u!`)c{x&bRD|0fZlB1=PbI`mknZxDs;Rhf5-S0BD-@a$hmMwSQx#idj zxR_p!f=^q;e2ZJQ^`N@NT(tLyK+285^_S#Dp=7IsFToG>?!F+R?XdC9!k&=MF9VNM z?vSjdq(n&(UL30D@Tj?6CyvmSmyCmhmnL_Y`cebw3ob|xr25)Mk^?(O;DLEoXr~B= zhE5Lovy`c4PZFnA-JK7&$`S1mE8|grUMDn?t+QS8I+VVOY4K%Ty7s$W=*ub(Gk^|e z&-hp9l1K7*Za56;NFoxC!E>xb z{QaYV9=RcRD|0bCtOzO`1~S5zF?WjZI8jDb*&lK7r${N$sQZ$*i0>~6Mf{iM3gk?n zAR6sjScy|*< z_K}0RTbPU42T|$7d${-$q&Sn-D*wgV0=d91w;TCEj+oEfCH~oolFimp_NU4!zlPUD z1*d(F`a{kCo?K0pFSko2rczcX7dsX1oOlOwJzAPU|D`&2b$t=;H76zz;5QWB<8bH| zqrcgW>JkZo`QQ*d1}e}5_G_SKWpG37SJxs6SAZ5g1(uc#dEnH8ABGR~%8aDl>l7$e zd|cEl;iv*W(i`%SEnx*yAK;nu7tG`7>FH}a^p~+Ny+oVPqdk^_EfrZlvneaGDYr0x znoiuPiz=88&@MEWc}hcTdwMAucgk#sBG`;#BRiH4=*4cKx_kP1Jbpnblwal-TT(ig zbM|bPU*4=HMFOIq<6jvpfrFoDcoBUNd_w2AVmuha*}bZ)Jcx(J`Q)xyXA+9q{8tIG zN=}&LgczT2q^=>Pq{ObELun)P8L@;>%~*g=cIVQ;MN^w{YR8o_GwJlZ1xlp=7x|2ER^VK@;> zo_Y{iRAL>AaUy{s;OxO8tFBmLS!(|;a<3s|e1)e;l6KTlK{?U%y$O7luF6RX)ka&=p+3xKN)CMUPD?68|U!!qN%Go47_5W1C0Qg zT(?=;-&(e0Z3ca-zMNbUP+HPDhkfc)kJD>(k<8Coa&wfF(?8Y5vNad^DZ7q3S>xQWw<(jEneiVY`wv5^Uj+#s}e$wB0<#(>xctJ#1O{HvJL3TW_n zs5D8S6n-sgRkRwA=Ys|j9!ja?gE0W61)3W~1Y4E23Y_KhX8{qDS9h8D57T-v-%Kyv zU-+tmsQRyv+;9JvI!g3PrDiE%we?z=$N#DpDNPd+HJ5mq17s@boeIr2AKj^43ZbYtcl{S8J#80VJ0-W{$O(7pY)E?xA;wryx-k~i#ooL;G z=^Ez0#U=?PP6}sF?nMHPU;Zz3*mP1LBnCM`0a8y;X79H#Ct5$z(8^RwAmx+>O!5}b zrvp2d*(5HZVe;zCS3-?YY!L)jpZ%gXWi+(UzsEa05SZ@0XHjCLZp({hiTou|SQgU%YIwMGM6S{FcMxK}zlRnH z-g}Rk{{d=xLZlH19CT(qT6g5g5xj1;4fh+Jrol;dk9X}w9gen*>q7W%jB$N9b)Cv; z<=AZfwx8u+_a3x3_iQ>coO}F){ z_hs^mMPWll6V&o;!k!(-{H;PHi^VAkH&bfzBp=a7md>!yUdoW~bAP(2dkv2wgf=0m z)Jx*Diho}BrVM&p<>_-U_kOEsmJ2OZqy`Rx7E(RWfRShhKe_)u5DKZxM&Qm8Ho;a_ z24rV2mxEpg`kEW`;Uc$`qwv^yHn)w};bWfhu35@6P;=*6SK#`;P0;gq<`blOb5_=! zsAxkvzE#+BDYAa66tXl0#)j9Ifn$q_juq|Rq=o#~jrfo~OU zU#_-`ZY6If1*o4SQ76efN$I6S_#5>%@m=4(=5$a3uEYn@CZO?udKBTXGN6q5qs+&u zL)F0Q2iic~mFl$$nMrCfA#AM!FM(TqADO=7-4G9+=> zrdWLHwszc7{#R^;p>6!gx(hbOk)!^>EW8?G)*+Y}#rhzgdk9=R zBWB0kunTq*pmhL+|G0_Y=XP)Mb(N3JL3=OhdmIUkQ7`48y+IAhY(VN4E0wMIJ0goy z(P0S74)@f*#s#UJmv2i5ExdSj$0hAZRsZ<_7w=2yvnM1!0TP1u9>Nd5-+d^1>7~1OXYUer z=SicetzEc%4?;V3FrO3I{`%D3Ui@eEspyk(7s_qfvZeE~%*!ul*7=1dzI}Q)Beh6N zoqzfE>R|9({OD+J6i1|sgV{iLK1+0CGB}h)R(mEAaNfqV39<%{Pz`Z74mRPys|~#c zSqFK-j;xL3f^FBvjU@;vCH*&M@cTZ5I>-x@f;kr*61DPm;vyZ=&Fl!Aou!TZ>{-F^ z+BB&Xb*u{qOM20iTTi%b(9SFdCAq0=s6oX z{PcBsLcVUa6e??kPN_cH9|`y9X&a^O4R_V1-1zleftf$}Hqze4ykFVc7T*fu<^XN; z43Prcp9V(_OVrl&;Hnz`ZmzD@V--lMtOGNa9lA+`K_c?bk<~F#i*Gz1cEqemWQ|Q5rX6zrgb!DmG>(=(n|z7 zF`8Za_~R0D%vZ2>>+x4I2RaGG`bSE>tXk+4YYRSqQBT{Ag2>d?P5GU_AK@Fx{x^{3 zj>PiFoZIt>O9x{IileO>H0Oy zi^hTrHn(QY1>#A!=kfie0ZH-3eNk2Hz|Clz!a3cdvdpZDq8DAZt&Q$Ka$BzSQ-O{~ zbH8q?61r_X$Y9?}`;1Aka$w02$v4!N!2g^&h{py=mU*PR`;iQD5mWH`mm~dE0xgZY3m<$?VBTuZ*t(%p60$x=iC0IHWgPC_g|OJE zkHKgZfUF4%eA-~5=Z4ThW-(g&?dNC?vkF~J6qu)anDsq9Xb;>Q0Fo(38*(53z2=0- zw2>1D;NztI2=%5T>uqA2P+5v-+qE{6 z&_G7FrR@nNZJ}Jl%7}{A36-*2-`zVXQ_FB1A&FN^Ad;d7fArDeT%5VLD~&d2;UpyQ zm?dqGS@^~17UuZLlYi2x6#tyzb4PsSHXY2QaSv7+Ce-<15^f2F=CQ(Y|%TF*Z? zjZ_O4O;rSmm_Dht#^gV9%xWWz!Z};a6@f~0jW}fA^@}xJt+2d3p-wWQ z5?eIt42!TcEv6-;faddv-_j;&^`%28n(~N)1ES@4!A;v`=Caq3HwpD*9Q5>9L}oH_Nt%|~WQ2UnU8_xEr%0OgX~OumGJQ;M;Xn9~ z8z#w=GMqrT!~@_${3z<#^Ulr2gqFFsgszeEb!6ME9OkpnJ{!$W!`JYeIZ!~l2Mkaz zwhUW^t;Nn6mtgSOKtqm+DMVGQ3uRqE=xUdba0OMUhqBop_?VFA2EMEgCRrWJW`kJq z9?KE2bu$RGNtFh^K$qn}P;ucO!k1-5dcHYh`ILbRc`kYPU71!l)Cp}Ek$#%yGgrAM zsoNm>Zi@X8pTtZ}CLxv?xhBv}X{w^jL>jW>dCJ+XXTD1A>xaTd*LjiS2zPFN{PBBb zk3SCdC>3^D+TWw3EFz&-D7Iyr!bjrw9ZUu2l4$uuWj( z`?vL}q!x*yStjYywV68RScTw(bbHgy%ysdFF}ZzD3XAgqw0j}$h7l}mIvo6UaP&8^ zZWTZVgf~+7{~kbF6^{1=INw7_)A(U4z{X}^8hE-~EMljj`!ZsysA7up;iM2;kdHo% zT-7Vm@y%)DKN<+ScYX4SRHNx@V-pc2k%+86g!eGF=DLeKonYvymiDa$LdmaIcDBEd z{60@qW+MGa92_xyD$>$nNb+1w>eWv@b+7WNr=AKmtJ*XnO^Rq$Dx9)(bGL-D(tNGh z8TYsOQjtt*uHiI{q+oU`<3gO6i?Rzm8+$z68lj6)cQvPddvuDBLaplrogkF5sJ6U9 zwk?jUEqha=N9N&AC}_5Q=U%|k0gfqFTTOvqELo4!)dm=B7ODY+su3N=zBAvD=6~7k z>@hR*(Pz?da9a*t!>m*p3^Vc^1(lpFl~BwYbfCRkAE3NPjVbw{CZ<2wThEoRmjpFk z#)Qc70_8=sUnN0Y5!diUVLca3?2 z(L7+3T81I>n}T5HUSb&Bj{>WD&Z2ml|KF{@;GgYgR2VVd?mw-g$e4*)B z33swPSK6mL^|qqvnQhyiF_sJlbH7nxnLK*d27H>XnmhF?XhkuCHMX(6JG2=E}ro^-^X_C^>BUt{h5N$RvX*oM~kzx%F2ubWnYj6`%AO;hy;@m?m08|qKv zJL~7+srviBaXMx?eA-m%^F`(N0A&FONu|xOTfXp*Wt5-Q!R$>f9DV!>{48FY#9F|n zKMo14G2c!B(aKb8J!{*?juua1EIaUKf zW^e3=!J{4%CM#H3Xo`U({e(aPkwa0}pfaWyKovBT`RUlI|H-(zU|+a?!*Umw#yzV$ z-g--_)qsi;lfixw&9B$+70K1E!3}OlpT}q^+qT~pt$L?w65?m%H8}?tI>d-br!HjcNrc!5 z6#~SgHl06jJz_387TwmL($0>&O#HD7@0DMA52LwL+;W z3OV~@XCOHPJNJLXQ5`)EB;sq#qq<3!%bA`BLb^CMANwwNR)&PY2M6`f8*E?CwLfI644PQKC z#>F9nPogWhy|%1rJN`(C*3-8!Z|c5aYMdg9X?#Bk1g3^UKlA&^ zm&himLCoQEMbJXWLFv>v4TL2CH+UfHwHZB&rnEWLf#gQHy}KoDi_^8#-y~3_0CZnN z{z$^(kSn4}4uH#ty3d1~kjIH{jPNh@WO*wH+<#)du zgH+59^;KyRzkUZkjal648dN1f)L$FJ@EzS}jP0m``DPqL$xp<_`niORM`>xgRln_F zc+cq}*9wtAZPgEN97$ciGeOdLbn~|KtgX|*Rd#W8RNF$sY`GohO#uFuRGm;Nl^ylx zI#pgj~W6bW0K(tN#f#7-zn39nm(lU$KU zjB{~mpoO(DcG9vu#~%$Iz~AFlIhg&_oy2p58RBRa1V^o)lgGmWGGQXv)>JkG0ucta z0N{PsE|O5ORgQuqx^B76WnH@NDpzXfrRVyZJ?CuSxdFfJTKCTBw1vN5zmK_;XNS&D zrzo5>3I-4^M!6*Ig!WWLv&Z{shsfdeDRasezYG5(^Xpaj1u2)f{grBd|IT*WNw%K9 zWmD{|v!l0;O0UOnMLrG$FSil*Kn{_WARQ#NF*bWhu&NC{pOHmW)GqpvXRe|U-d=xZ z9zGM=@V=_g;;UO6!e+7EtK{9lk&@t43fv*}Gx%f7Qrn;hZB=-c-G+2rRv=ZvZTGB6 zo6xg4G;;p=-QW1Su0D96Q=sK`?7nJus{W#|MU3kdR3z`pW)sVrHI~k%CfBUVW*by; z1<-*B)A*3@9DV2p+{~*|(6MloI3Lnl1QF#={zW8K>+W&d2o-1rK}4Mrss6c|8;yy{p}qB4XytPUmJ8&Ua`}!;^EcLF44iU zzrOyX8>M>QFKL62c?4}|HnXdO>-ykP|4i{^#sLrb+mk21CNYbs>0Q^{aG;aY@H%z@ z!N~CfUuqIF-=H3;Ln?7f>tA+s1ln4eH;rcWSSMU7Q*gnk0-NMLpU?yO5;)*t=;cgT zv2Lu==&<@h3mhAv{YTPSjre~Nj?N9Lyi{1=MY*utBlF2Ti~@N5`hHSD1zzH7TAWlf zw4^jEdH9z%#yorKd+!wM`HzAB$J~Qx0W2yKr$p+I)W1rl0&P*|4U+};-v$?)CKiT^ z#$l)`AVHOE6ch&UnY{x->;NbseKP%-E+0O~~qjh0bGZm#gs1Hz(Hvtl>K!F9}ZwCXEO1O`OLB~JF!f-b_06yO;W*9e>m^FFX$;_MR0kyV6 zt=Dvh&qTqfqyGm8VsVp*`4=in98wmB|LVN0BXE#;L#xNVx55Q7U<4Q*9ewK-e5$N! zhM50hh&wl})dwpZbfgP?&}G6oxDz~TKr z$xyRRHqe$r0VQK}5VdZH3H$J_5}XYv9cK zhYen+6Ac;~B(TXLAY`HlHKu}CH8jRVVCw+UUx7Aps|2_aBWS+2ODH?p^JE3l%!v}J z9{C!&R*i>CM=B5Wgby96KY17(e`eRpUmMKO4J5ULy+cU6pZP8FL7Vl;T__8UM43g4 zR;++eH{U#Z&4c( zh8bY`(fy;J{Q_UtJ(CA5x<4a+4Rvq|lS2@HHZ;;*0>5h9)ImGscp98*m64!)Px}rD z))GPk(U_|-Mm3nn#8~4fJX^Egz+tl`WC(Mo-p1K^7G2H)w1kIIhr7C3V7$Xql73qF7Izdwt>16W}CYCD+9DKHBc5 za}v$-Eh4YoPzef+{N}vAeGXS&r8p{S#DPRSy_XdIq_guUa2?8;yg9cUsG z(ju}``h4X*_3#aa(xExmFlDuCQAcL~AjGdiH>DE`_2Q;3j0=`5Mx?V3=x&6g%0fk zZS6XB2c&0MzJx&!j6ejr3A^P?&L|opI}U5S_W6Yz$~ev&R+`1cX@JLsn_Js^kT9cY z?Vf`~SM|w)WnPHX3Ji(R#kiP9YiWLybn(TMoGg)Y!IeLAd#r^_X znKhJJM2Y#lLmG41#0qbVQ`kaBcAl8#p`FqenVZ&{@($Xr_=1_CHI0w5h%S^_#o_QG zFQw=DmoD?6VN>z?p3>pj&?Er8z}Zc|nU<(=eoPofXV-BNN5KUz;TQ}FuswX9hKsPE zcKj6HQB|s=NlqhfX?WiDW)6fdphF;s;P26R1(8%tq}5Q+OC;fF!+t>`pWw%m>TJ~6 zn^7d>ojoX>ah2P}1v*izAIZtP5?!$jybT7rsy^T3m-a1K*e_GJ#iS4w$Yum&LrWHw zsHueu(XKCEP}0)aKwKcoPHR&Yclte^i|`vR zEM9HYA`UL~(i|dDJWzz%Q_HF6AxF{=MRhN@xoK>WrAb|T%tY|69)%VS*7;$L3w*Hc z3y_nN_ceuu9hELgQSyB%>QlC%oA}y)KCKo@qftu2>B{TnEnY-h^<7edT~jhZBUuKAx6E;fmHH2&ZDST1_=tgfu~)ppBKxb``2Md3-FV z%(iv5=C}3>R6=|fGYy@`>_&T;bsBGJc#$^Tc%z}FcIdci@L_87vVRHa+eqz4K0fP}V&_`%Mu03Re2{jsD8 z`{`cV8>;Wi3XN8IUST)x?1O>^J|9{vwlZ5_YHo?dH_OnfNaxLVebqiG08VIS-RrX- zP}7oNh!S&H0$V%}s64Z$TJ)CbbAx*Ol*v|$d3Z9uPp%7TveP?fbx$`JbumRNq=M1z z+&Z7oB1k_1!AAJhQ{cfF#?=Trl9OjAJ8NG^iBJ^+N_N%=Jfy^!k9Im-M3!EyPte+# zpmkay!IGto1_a4sk{|ET%#OH*N}8CeIE3<9Q(FLrq&AQTh*=VSS)TbxTa6Fn{88PwQ0Sxr&=B6k?GL% zk$QqPgvY9I!HM$$myVwnz3JHeGlVZOUNix>^K`=%7!q zES1e}Q>7Buq}SQ~mL*edT~j+Ii={>}Z5DLiR1#awgw>okGe1^@G%>l?iT0_YI$apN zq2^PMLMeAbLQ_zeUjSi6RK;o+A{wc^cnYUj^=W|NDL9DV(G7TveT*N$Mk6YV0I$~& z5~f&%suxa(E9jn15Z8?Ensx*9zNKkMLzlDKfRic;94++=yA`pfg3@LE)sS3;)S{>? zpgB=rjt|sd!~OMVY%aiM+M=DEC@Qy0m=BbJ2M;Jj;$TQHb~JtNdmNhfcCA;FOmXzq zzOvD1=ozpW{bzZs`fLGzP-3=>)6e3fZqm{gLGqeA8yJk96cS64b{v{I9&WU>M;lF< z3AXYKUd4Dnlnna$YU}E_mXMJ|ZC@{8ucE!L1?Ku8uiW?JD}tHkF>)p8%?X)011vw!* zP20TccDV(HM_mOk^NK~$RLoEcPVY3QDjr03GfqBNeA-^{B$$uPHb~5iy%4hSDMUCb ze1-7MB9u8;N*)KpK1NCO`(30p+2_bW$m|nTTz8kd-vv|*9^M~~MKT<&ZLeEi=EjDD}neF#^=_5OSeE zjqQJI`_dooe>7PnX7aTJ=7cH}>z#NYVZ}CdkHvR2^d+vBuzocgy%S&v;M~=I(*uK7 zTUJ7&K77ai2fnoDtFLYnhs-PTiLJe~fq!sqW`94_(<@h`W0OpT_4Pmhd9A)D6q{tI zZ|^z#?6av0#@Hez?MX_pQEe)DF1ds#-&m{>s%3|}{M|~COA=TVE@`0KUfI>se)$k8 z)QZd>vgd?@=j73&&lR7wR0c{lb5H~^4 zCxzYzD4sbJGIsxL&^Ol7RjYmtBk(|ut?KP}wJx=E2G%7mlNMkcPlq}rDn&K|sdiTf zCHq;K$rV?wTu7~!DCDUSqDf0kAUj`i`R2`Qn(%p)w!NvgCj`^`4_7#ea*Xfqo;~e%bZoMv`rA0Kh5r%8$+uQ!50aI29-SP z0;(Kokx;^=DB^Xkmx`w2v`z9E1|62Tp{?i zBmD%KL$mQY^+N|xoOuq`R&;_%d{)`C380X&P~dQWIs`(&cC}^Dw`Y&Jh<+yL98+?o?Zc{d@PKPo8`d`HD9&NADAL|y1$?UTRifA7Z9^KAv#CJ>k~wViDeMB0;$!(7cx<#~)voR(wA02! z`70FL#0;YgnH}hv`e&b3&O7e{!PfC@fFuCE)U~0 zZ{*00@n~Re8u6Kb{HB+=vr<7*m^+!r%jcYPo}emID7qs^%(o{6l;B*|?k!u8^X}3S zlIGG55v8}tQ(~R)=cT)uy-x3%C-CKsxGS#VTZW?vWLc z1PuMLbFTmuTl(PEc4Y|@TgDx8lh~|XC&3m*P8-qa*r|fSczBA9yJ+-m{V(|R`t5jc zyz_>sFxGV|x|Nv=V;KWbtmU`#i-Z(258c8XMzaQQ?m>L+)Q65jrOkIE&X?Lu`|pSz_eE)_Y%LR-z8Ht0KnU^@`}3<2+u^KS@dHAF*1f%=Kk zd+o+7LCH>ozDfBjS%<|Al{H@Drz|m^>;Jd85FGpA?t3i5KbfvXoAWp1EW~61eeu0UPdJ*eDo;v zQbL z`g@(k(23Gr6_V@w0Y-)ULu+}}+0|A@^^&Nu|>>m@Bc69LbhaWyhP7*f>cj3yQ zpYMdCFBGG!j2(T!yeBqG-T{XtWJtA2q7v10k3Rah`WtR^b$zfV+ev6*f@K}E1f94( zZ5(>&NLi@ocI@19cQZEh$FC#p zf0I~baaTp!&JmK;r=R|^{xm{QJ(bCV3>+XzCyLKma!%%>HW}+AvhIf*A$!^JZjsp} zbkn$;rqZtT6?-mi7if9O%l7W-K=C*8DN|2Us&Gj)jv00XUqJ~LrOr;L&rZ$dl3euh zf(4^1_YfPn#U{|PzX5Hd00(n2HXDGN@buZizljoEbiBtY!OqB>wj^$#xe=qm6v1pl zG2#1j8}{Y9=4lu~{;+iAm8Hju)oSsv`b*^n3+4%8^ZK+!=i;?-O3k-)TbSeB6)-A> znd9=!2mK?zOl}$QL2UMNY%U51CVh>bEHcj&3aFidp|)k&Y15`m$u3*A^hEIvW5zN( zYoR~BXh26hB{H8b*`9EVZ6b-0dCCIaQ-E8u6wb4wxdP(;XmQTuA=f14@65Aq|4Mih zltu&9vmhvKFq1rj&BWkcnc-et6Be^UXM@lhnhta}+<@_VR|4XPak=^)U<8<0Xk0$s zK@K3Combfwt&Kqi!H_mFPa3-{JhiZ2qLimYP(xs&hb5ZL=bvAmIdiIT#lPkf8~EhFZxAE76Dn3qbWDvB^K;VX0ob5Bk~*~Aj3vb{s?mhp9zjd=yMS3^u7 zmXITHOKiqik%qNxEvwMZ@XD_gI zh;0kkgef`KoU<`6n7b`pa4nRF+l)i z=s*vple}E~+SF~ycU;B%!r@qaD;}Q1{;ttC4iI^>kAU3FShq9@)q98*(L_mbOev0$ z6=jupE%P|xtg<~D3dpkI({NeL>5fY4zAGPoxc7kvcB{LmYI_B08jbX-a;?6z3Xm4c zuJVPqPU0G7SNJ6-WWCBPaMQmJWL1ERP1e1RzR=q zxs5{GWwm*C6E@aHL0zqOOD{)L6F$Kcz`P6)0Z#+>;0xA>jp_DACq3Jn!rrb3o@HN_ zuuE|Evm0(s(WNgnNCgdBmnfuS)x3BAvt~I$xNE<@1aTnu_d$!!F9->I zGs+@4f_tbyi3-i&^$&tWGie4_LWztjMaqn*2CNbuz{)&N(!5-S%YsU=pcZ?A`ViGJ z8!8DONw1^=hq-Ty&?}Ol#p0)r75OR}SrYONzjJP~!9crvVJB%8=9jz7)sS3g;5%q% zTQmBDMwijS9i-59aWs_Ewx|sJySoAdmab5omP-g0R~L=rJOSTs_ODL^sD6o&7BrD< zH!~WozNpuk`$Gqt@|*YVdt=sZ<-+0l5DPTe_S~e-#a5PWbCU6yPRcFQ|JHf(AG9<3P%a5p` zlOZuN@-oh!(~UTAtQ$8;VH-JN0r%j?Aq_F!_r~%DY<<-Y&UA85L9h}_Ydg&_=6@Og1_p;OteNEPlK zf&0n*IJotyoWv$fU3@`gBy|4vJeZ-uYq$Dg1a>#U55j!1G7k4|oAPZ>|MgpaBA3*E z(Sg2t9evP$`FfAUE%a=?X>!l#=s$>mIvRTFD6t>(l)2F*#-5)!wLcOZvqr-j(L^J~ z2#$1vn{c9{*L1_QKC5WiehO=YSE}fP#dFS~EOrC0g>op1+O(Ccp-k-|@uo|jMUmk8 zm_1W;6phRU=vhh7xh&zci1yHOdAkpMX=9j_94_2cDBJ`W+I1K%qI|sQ69=NrR9soI z)xSm`4PF_P`DoAiySs~vn_>zNc)7}mKEtv5r&!f0W&IBZ3wu8@4~?YTUV7;@wEe8J zShIB2=siZxS!62?;rmfAu@&&LSnp!2GXaQW31TfIyC#$`2p{N-Xw7^*=@<*@K_xgF zl@9hbNaOnE7AmO8=)=O+3ricz7r2yux?dGk9(;*oHowpO2z4+|p&v0v0~@zxV8%4J zdvmxqbkWW%X{5Whp4Y;RqAQu-pdfS8o1`8sW?q*Xc&5)N+JFy|8S}7BZJTUT-VYaT znO3r?Tm!}o>IijKl06wmXHX1P6-8<=@WyX_eI0P`8j&Q}Y0vZ|E1jBzNz<<85o&mk zN4WjTQ36J`!KF}vPMlYh^mEnF&dXYp|3tNwJ#YZ*=?@5)PJf(+z{*0fxZSa0LyT+^ zwXR!&l!4HiCAOe-@fv1VlKJ{pVSvSkYTaDgSh~Qa8pGxW=AXH$%r5;7p4GiDFFg*# z?@0sPmFOucB;FCkB=$8;O;NW92Al;ZH{4dj(*2Gx#xIEE@t6vPU&>AwXEAvnjkYn!%-)MT&s zu19XW?e^QBXO-s?}Q8pNgz=|1_46C z7BeJ;3`p1kQz(R4pry1>Xc?stXv?pqKnJCa_~C!jy-7j(4i$|Qp`iGABzow`yV=R;NZb$=&5CeSH-i5 zGV{Hc9;D=}u`UQ@ma9#><*e?vgX?nAkiR1vAp7KGi%A%62?%1EKKt;O=*{uhHA~z= z|0R2iWqR!Z!BHqYicWP*b%tZhG=2p{d?MApPEC}qVK$-Hw9$}js1|0w!yTkRDDGq3 z3338}MRIJ!6* zsHxeWWoZSTx}r%yQO-3E!cavkzI&>pV;ejuv=g4sw%l+->egF#E;IBwNd*~@JcBqw znJn%D4}~(eYBI)(ozw_?6O4-72!uA`LVV9NFDzKse8UEB#esQ*Zx(#RV8uLg;BetBWLDx# z@PMg{OxCk4wb#C5>7g7A(q2LnPs+Bk#Ru#HI1xNKO&?C)bI(=#yi;{HWrsa!?y-?7 zzGt#!{S}Apm&3Dij(@?LtFF2VLiw!c+lgj_K@tLtNUp}t6~07^a%0BubOtrs&((@^ zim;BO=If(g;q2K+15oZlq1bJZ${}bf^UpbuN<{)E*_w>fBMYl9yWXBx2sEUk=r|ep zTe%!B)143*S&q8O8_VTO=r@NSWnFms^`L~rKr&!PfI%E^ywT|pMG_%FTv@Qt0(7W+ ziC+>B`#0_B6n74-PO$G3LE6Ryhxez%SD>{f)W-i6SqPble10xmABq%RPP;uZi z0!x!g){55L%>xP>&l?K>G!-8T4Im{CVW4n2OaiLYmwbU<@~}9fUv=vo=vC~iM(y(> z5DtOXx#Z-6;yKnP`d6Ks>9tSl&7nC?wVluv!ZW>pv#W4YQ^x6=;!>rWi4BCk?1-~u zcYgk(sK~{4tm-!v4GO!IGV!BZuPSzT)6aUQE8}5Z))-y_&|FSPx?fF zuiipvnS}%2>x@t1e-*Ll;YKQ1IkI{NP>WgpH-KpKgg9b?)dPabSXliepVTe~&L<}3 z<q(00PwIhBcaRU%3(clEjn`CWg#Ei85ktaFQ9wlQ|4 zTSl4rk*!yEy{itWEdACwux>SUnfw~?2*3(p*3Ed5%rRutJ?L$G&hSC3t+ey?{6xK~ zh)dLDe1djufRBQdF7`+{p$;c$^;*#Zl$*pyS$rc5n}3o_W31Jhmvm-*>64{JhZMAZbRAX zHMHx+YI3h8>m;pIsz-e>y7XCK*8E{rN>0;A@<+No)5Lls}!pab71P`(>8R@m261Op-wQ+-r}-_VtRkSu z7RURnrJN!N-|=^-- zkOP=nfiWcx@thKWOvmdOxM>%QU(?;dwoE37l)YO&>uMk6mL`t8y+t~FijWC~c9Fy# z)MOwp8=mGjmC?}*XjkcZ zCh+w!AKxwV0z43lof5mDhF0eD(%(VPyhECox}CJdy=;AJZ0zwazQ`lw8chU;uQf?B zB8?!sI$l+8+eNe^2bq717f7%PEpBB_y<*&cxT7HhNfgYK? zmuH9GSioKqN%EUKa4kQ`g83{U7Z|H007WrvO!xO0=!rU3X_y%4^CN3xXG4Por@=w2 z=y0w9)We851KpKSRgZ6D=0CWrfsI(O>X_4GLxx;h5tgU&#elJ+O$qYTf$p$r(^8u> zZLsUoQinA4ODnuvq^%a4u2pK6riZ5jR-LlVSh2lAj0Mc3s_*otVEJT%9I*xb!{hBo z^MCrBP{pErmk0;%%jg-FAR%!(#W{(`_gzy$%qs>Xk*;dX`_*>K%J;nYHBoA}8NZSG9VQWEu2u2}8hZGujTFNze5MCOmNh zLX_OLJ=a~8`)D4F>S^#Y**Ie^u%45UrGd_cT7CjYgG>Ef4Tb7y*gEyl27nYcJ~PSv z|3_ZxGYNS_){^NA81k)(kUX82VotTnLlHGvq_4Q;mYhXvf%-IsRh~!fHe#XJRZ*nRgZjG9iRS-g=f812NZ=AX14>}$%Pc`q>hnliz|W#X5|2yVBJui$zs2lw zx2#*f0;#}f66f$n-a+iljQ5?1b#)KE5>->hQH#h>OQ- z(;0ocF4asw?^PvcU%NzT;Z41CW*eZHhR?fZIcXIx+&iO1NoblwjJWhPtcYDvg7{W{@a<-6M2Gt=-mUC$Vvyx+SS?*Tlp0+o9JVgP_2{pQrVMw^Y}W zX7T3RCOJqn0dd8|7Ikdpe3#QXdvVkfSu*G{x#ut0_s}Onp-&Qeu{OE3DF|mjyz|J( zSC`E_IRjP}gO&Zbv9b{MVjJ8W39>Z+rM|y9R>#P4CWnW!-AbQRFd(pb(L`8-H2lhg zxM1vsv|;Y8Ye}wzgwF0tjzav-{O{z8|{y>!j#1wWm2{1439p1o$`br(UmA70y2 zzZ}|k8~Y_~H4B#NF($ACF)e6aKjT9)3K?4+Shv7`phj#kAOL^H7OW$SuKBelu7?)N z>;g+KWt1EqKxz6V1oZ*~^!XCBbu8L7X1J^W^nQDrjt>CI*Ns&e(}tZpEBm`~#~Be_ zu;ABMR<8u(r@n?h@JjH$)dO0%1fKbKAU`~MO^hMbSQ7q)UUUs*7FU!m5n6@+ISJbT z8^9Qc>gePQV~<*Y-vA0gqts6N;sJUd@xCUaP(aVuiIpqqm+x}qb<+CE*h~T%QJI!6O!|mpl(MO6PhSJ&SWY)rR>ivadNg?e*~P7@ zW=%EDT|EbHEl?IRaq-s8$+4Hu z46cPK9lW83-eDg`ab8mx8VjC;s{FcZY;CFo_m65>#_ll-UiCPNjawUdkFB3WY-)85 zl+x z(S)_xHmK)uqQ~h)=t*&Bwv=D#=K*5di}Ymr!&2!z{0vR~9b3b%l1;6^(jj0euSp2i zg-RGL;KbUbUI~DsclE3|XCyj}&k17-vFb>p4Os+4yYbEGHH%&CEq4rG&)RT$_xtE2 zI^B3}>hvsK72}(Qu~l=IpN7Ku^`+17Fg{m7^_-a1YM>$^yZYOPsrMyzq4kT{*n)l7H2DJf$dk;ga7l{lO}{>S6k7wEB^A7cGNDCrrgL zAy~9T*k_|e-Vkwm;@L${99~-aZ8jTitQvz&z6wS9_3Azx+tERDbQ6wk;r2irW55}6 z=&Ybik>-bMD`mC;KOuf;Lj*U-keXO<6gTOu=m%@vMiKjqGRtVcN$ zJ@X2sm%68*;cI_=_0{xWuO~JQ7bg<$Y}`n{a>+X_wt}{GnqJ_RW-MStOWfsO3^P5uvL7a4*l#6 z$|UzqH#fWI_?>eg2gqA@wQJdfh=m3Q6S|wPW1csW^7T%_e#sLXHx7>#xhc|SAwH(N zy?qXS!I-eL>k<=mV3xOja6SDLjkPESgO!ceglK7e!J(LcV2o4H4UGv4zGV?+qQ4e(;}rJAAxFedx|fkf7XS*hQ*#)D?wILg=}A6 zwvei{(Zk!fZ_m@443H?mFePB`?fxx@F{Yzl{)k~Q>U7le%|Ad#-A`6I2=M*~*K&q+j_EE1;5-~ay3JAjJ)@aY#{{NRJ9>0hBJeK~qctW*?WsiZw6kkY>b zFw`hIz3*37TtR<+9kF(}Zz`*2d?ueNX0ydqK10v$+d;3!h((P-O_C{hA_oj;!V5kG zs<8C$NPfAp(t^`ZSb3@HN{ZpRRKthqRj`!Fo`d+Wub*zvWIY+O3$oesR zthE_8)!h$QTixN%fXe|@StHe0OnV~Zo+%(S3gJ9Ja<1)j5T5E+orXk!s!+c8$9=l- z&2Ygs_o^Xxb}Itd+^K=wr2YGafvA@k;ye9{ZtEB?7iuj!v#P@n`X_mTqlH}?f1wM^ zpXD?==Pc}$O}@E>XC$M0pvVc@n7e6^iZ-?4!LmXa&ooo*^47coCa65#zc(WFiJeO> z61JA_gtAa@LHRoK&?=Ji<08OiVbq8dtp0||Pyk%#Jdo{%)|J6K#-(KhbyRSCF2ylH zaQ9%GGo;105Gm8txy zf-$RBtSq}l{_t22Kdb4n19r`r!bU#?8LYEbCdzB|MYFHR1WEShtNLYXKR|A@s*T28 z+n6-uh$Fi}zA)iKqX>^P;0g0<3qNGXWsOV#nAXfrYErf7V>y-|ruCcd|3+c3OiFnW z9(ya2hIZQKiPOTSjI>#jL5I8-s-t=!Fgl>_U5tHf{E(vzm}5Z?d}zI-oLeNtaCSk`;7Wby(X8x)%>Q`zfS@3{Zc> zAhJVNWG|SDb+y$9AHnT_gJkuZEPzB9t+HwZvsq+q?J%K{jf?;pc~{g<_JMcE^y~r4 zM%$Y>rIJieI&$)4;pwOEzWW!yDE#6VN6>3%B|e2M(C7fI`vUU!Dkndbic$=IJ#MIq z`1$dCj2B6$>6J?hOX<(x%Wd6a85;kt%*ZS9-KsI_2qX!_P@MNDrDPX5T5&a?45|UR zIXs{(V{zge!{|h&wWXysLw}shTyQ}q2i^K_7;uTW3|EMA`5)7Hvs{${WyS97w?Opv zR1uzao$#Qa$?fKXIv`i8Gdg1o2IK*ni%j8#>an=il2G&Xq-RZ50sei?ic#mnx>Ui(HLx)~|IV%gG04f4% zLRTEKK2K5Hhahn7i-nkR2x2#|KIr2`(-DY2tLRlrI;soke=g}*@NHn3Dxw42{7|>D zixQ#8Te_q)E^P%&&M`@WmG8TtT_G`xbIO<^s7hv{^r!Lcjvd)JQbaQlZCXOV32cYf z6s9PYVfz53j=hHe1hv1K6&70sYaN`1H!6i zxDCIHYQOB_sB53Mab&gkyExqH{l9^y3(cYb*oD-8g{K7R+t*6S^8tP;GL?ED4Y#tt zBBc2A@93NALiYsVvKKipu)`Y$Squ%JyBKZQ{kja2OZWUTH>Vm1O+i{oPkkSMl{0v*f zn_oIgD}syND)-VXRk=J$%Gu#+mDrpS>8bJ+h3*x{m+;t=%aoQ}KVBob0};g=4KvhhtjA@sq7|7t(JnxkydFxO@yK237*tUKT#zzyKv z5!X@DfQd|mT7j8IsyMY%yE%s1n0*{Y_2nENy3LVfODE4L6?la*?Wbt~#cbo6w?l4H zsO`J$s#vmj5V2Vg-OIQ5bz*=mwTsv)kEOZfqaHpAwP$@ZW13G%!Z*=JH$!2x%%u~> z%-Ng)jAE|HBo)T90I@qIv?1s!Brfha##8WHr6wrd11z&?)fE~2Nh-cRC<<<9r8;*+ zxMFsAbHCg_-=T6Yq(u&>=CcwHQwBJT8hQVoLV4}cnf>D_C8u|iea%Zd=z}egrPte( z(8ztsK2pf}=c`mu%yt^YZ9bCEWl;e!%dD1+U6wRe_Ow!kF3*%BC5<+sR#aoGv&lv1#$S|#YBR7(21l-$a*nE3P1 za$y`WqBn^`x@lS8f~?TU%g*-&2ICNEOwJDki+YZ|lo8m(dXF?f+UbvqHd}4$6&zKy zY~L@?3KJ@iiQCE&aL+5=j)WufxdnY4zv`>O%vF=aWN^_UdYQ~Q)zN?J_3hV{*}TN1 z9W2GB=leokYKYkCy2GKeUg+cjutxr-$>Z-m(#zM9)}meFT<`$lkK4w8#QGQe4bA8K zzzB%U@|q&c76*A=O-QM>_En5Jkwwa-XQDg!AhWpi?0Jw`+(w~?a+nECm(|v6=a_42 z9^n{kTgMRsDIn{|8yV#k2Bxna+|CKYybm$!1!$@x3V=tru_2Li&7YD|6Pe)}SD<@k ze#IhDSTjE1oe>k7dCk*3f%ys2Mx_@ATsgh8kQDfZ*}Q>0c$Cf4kBPb46!$>lX}HM9 zB_O<-T620vE{#$sK2Uerrv%rl{qpvfiwnnjRXIN+F=@F4g2$P*Jga|_YS|bRnfk~1 z&q!vk%stDhw9O_Kb=C?j#p*{Yp>8c?7WF-$Xs-b}6!m@4*xLN1Z%qb=VbaG)d&k;k zQ+`xRZ0$?k)I&QM*))oB-g9KLya#r-9Jafs9W84$5tRaXs@DQf8Q~FDw0%|t>gU5lwN5^^98wMJh>n#1)TdV9M`GK zTlK`@BcKef1aIyHS|Ez!=?CJ7vM1)8)&lbbVpAMr=aWX^{E4pCs!~)c@dKjH4h^vY z{pe*`v(1)xQW#zB6ZzJpsltv3M+AqxTjrSoES=R&1tn~UQPk$;^0{pIz%pqhW0oe( zG0a9rziV9)i(S~5ogiEFXm_}xfr4s%cW9O&4L(YI zasHZdH~njnZy_g~Lh3u|KNc@ZCN3UQp3hkgt*H!L)b(L9Jer$Te8Q;Zp{n|`mC-o*NOw`X~* zMdnf%v$}5wyoz{>I;2j*u7xL?C<|X#AtY1bxq1pbZS+CHQqYTyBJUKJE5038^k;GB4@AbeKUZk3_dl8Zc}=dNoP;HA|)_LX&Q#~RbnuF+5;X7cS*Hu zqmYQBEd;c)Gv%pK&6EZY5{aDAl#MGe6S^l53&mAB4HeqWjDkDo*MK%a6-H%7Nl4HszNPa_FBnut^SQX!|7`i>?RI zDdBo}9>S~GZo#oWD^x#h2lt)1i9rmoU@$kas}X`?>erG<<_8S~Q}FI<=x$QSYhc?m zW{i=!C2G2z9A!SY`Q)x$!rQElBNFo+Jy6VcKpc^JRri(YCk$a>uBRZz{eL1i#?%3S z4P)vg@;YcB5a$4&zBag%laH)^nTjB$c>0Bs33<3kZt4A{-$*vb9GV zUhY_vng+(Ql;KrgweNg-sx+|3C3i1Q2`amzTp5_T#FSx9 zSClBD9Q|CEMUrTXQCUS>r;+{}@^tU=1MrY(%KWIa_R8HHeeH`lUlHSh+4?g0=QtZ7 zZBjQNh{iYV_)nrefD5X!d1`;(%h5r1Jf<2k_Nl)L#sF&}L$JhRq+d#DJz$o)uIxrX z8~*H*1Yb&|kz{$Rz#;>xmBydNWiT6#H2>*P=YtPQVyPfMOa)?^PRm$7v>;h>3Hct{ z4>BXQtEr$qmeMf-q!UqrsJv+GbuUypC57<$*;_~%FDcfmIMuXd?A#VDu&CvHkJ8Z7 zi1^$18euPyzM*FiKR}Dp3k~LkDl*+$Vs_ zdMrseM2@x4!lq?StD4p_{!{JslN^N@DvcJv+T7bYwgFJoiL+6sBs4lEQ}|-T znI7a#DNUR#+jy!SJ5WxL9hR9G;L)}p9Ibfg0};w2nS9fk|wL`;Qu5U(_D{6oRzI7vtSqHka%qY>nM*#Gj4|~G-@r56u%D=~p(U+j ztV(P@C6p!WINwsGM_Pzp&~_Vu2eyhda#ofaJ1?PqQh=3FxVpKG!pcZgS4PV`pp1ys zip!Rk48WT9!1H zuL1!oft_@BmlB|}#P+e>21QONIirIxIwp=$NScNQ4{&;^DZHj2Tx+iafBBF#JlJ%k z=~&bKO%FE>osF?GRTIp`#)_v#?l>m1496C8KFTGTaTg7f2tpOLgC7!V4D10-Y0UnR zSkb9#Z5IIcrR$7^l&aCsa@AFr1G`onxJ%&gNu?U^#HK`G_GDAq0H(yHMfb~!~qTBarHp~U&LXN#JBt$Ps%G1!$-`mdf)Jm1T)Fy!a=G?&SS3-EaWuv{Qf(MaJ>$Hee2u_<&Ln+B>zd%j;)^|$P0cBfe&|{B_VSJWr zjIBjU+4=HjOLmOZB;p1NVVhWOOsNDazOCEhfM4K)(wou?BO?}=QCKN2)L?*MOP*}ozh0|sK?%|0Lr|xIA|6;Ib?rwUt z>B*+&e`xzXj1Z)th|7kpKR{)NO|AbKlTZdxz?+Rx|6>EF14% z4KOy?1+3P~!=UL9iw{|U)4)ggePTnxKYJ?P{Xu$1$u?ys-u;f9P>#(Jn`J^k;Gy5- z2-tn&DVW;9w+sDcRbC1?u?Hk_)JK*31gVK`W7hn*DQgT(xMwojsPrhJ3;>$i_a%%6 z=}oXDRbH@@qIk8E20dV0>ic-_@%ITsF6|iMO`>RfMX*a961BO-UY{Z;5Bgj5 z`dB~;<1dw&`=C_=aY=SnZBrV%T+s_*swq!JS|&#&wzyikEjlf1ks5`h*es0&Dke2x zia1q?l*X^KNp}OLE;NccnMN@#e;X;l_NrIJF_JdOF9SBkuSj>?e*|`Uy^fYk;(rb66Ia9YazpceBbwg`FDJw-MljC^peC8(d{4}!|8>H?Rp(kyyM5d_W;Qq{2|3xZ*d$fw1GVxd=sxZvcGJRyfepW^V@5QRD` zeF{i^-iK3A;}Dte1T?1R<|)pwMX0MwiM)dYzN)G$PddDFQ68kjG(l(FSD%q$rDsLb9Tg}AO7gt0#sg!j!=^+hXd{>q@Tkv07nfHo+Z_>vF)z0iQLV7%4zyd@x2;)!{FgAJ>yZ4M}suRh6hc zc7OwwFNs%R_ULr_h<)vY8;6I|N(Yz(_IKL_Rwl%3LaEO_{B!HyKl#^FzI7QWyK7tT zD+e}r@{Pj6M&Bd!jh)~ZLtM#Y(}X3Rx{Sf3F(no4#!}h74XV_WN{4}yVez?{&YOw9 zEn1X%V-8G}T>7ajxYVukEKdtoRW6T!)g7ru$G-*7`1a%+J%G*FE-VaxTfERJvmGnOpghI$N@e?tYeiS!PzXcAQ4D9caKHjju^&%=1mzOO4-o;9%v^~5gyAKSeBsQ3;t%%F&oe|TZ z)-M>2fANfOJ z5bT7=+kaTy*l6mqJEz`Lmh0mhu>*`Inb0hyBzZom9Kd@pkWVepgNWPKcYJrcbOEym z3p0@S!CZavKe&ofa@RKPVmNC*m$Qt|SLa%zptAPCZcem%E}wB7^dk+fE<3~3mw~Hf zl4C%!QCy{e)Zv)nNcmBUKTXL4!^e0F)bo3a<%8_`%I(>mj7A0Xa+p5~j zk8(o zx#qRm_YqonNvDN}*kX>%r4_f?J4z;WK3m1BQo8>A`(jDgq%gsI;27jY^bq?OIz$|m z#~SbQQA5WOhcz%5*Zqqm{8M}hu}|FrxXb7X7&5}L>qMEcGeXvJ`#Z$?7QK7??VUb} zi&E*O0OP|JdPFR{LDCoFQzXq2x&@@wLi_|LKXTCLdDGVNx?{6V0~06}r*E=j-7j|1 z`=MZ6sJAOcgbcDDBF;aCUU-1Bg&)F`VD1Om#N$EJ(2aQw5_ZdiSSfA#|s{Zu;2~_`0?SQmoA55D%EJ`VxGK={B zNNlz*c|j}S>a|^zQrbl~3igo#qD=6?Fz3^6Y8|ohbdgBj13g9-E0>};h;oZrGCQQ3 zy=2lcmkXCJ#~|z*52H%ypR=S~pIqaey;~3v6Kp7|m7AEe2n_|Mk6ysn?Y-(Z5 z1ks;_IIL}l9$n%#eXz%3ai9<`D;U?BjOaWPK!^}I)z)60q#(8w?ADyg(r&3F^f~&K zj<@6jaWKRavy*LFSIb)yE}m)*MtKln-YFO zu9omQ5dUF04U$TP6p%xmk8^fmP!1c!%$|gp8QJJk>}uM;$Z0L`5JzfqeJ^MRjU>!n z)QAT%akFY880aZIyMUbiL@x)LU&JOE6eb0vW#_cWPMKAli?3_@5TV7Ji)J2yGi+EO z0Zxd>ZQa_jb?_0nOZg-ra~xWY#Vo=L!?g{ToVrz{dLh4aB$4RVU+YX^C+^T z9y8182~F;o3Qbtk2%G)b&VLfC z$74(Y0LI0|8#|irz;RT_`$A9A87v2E;(?tAh5ESD2ySQi+$fKd)t(_?A_8=LHUek3 z*pi7%HbSo;%FJx%vyGiGJhcP6r1%s-tl&)N2_Ec)n?B)3h>nSkY7R_R)4$Gqm*gn* z^x=%iC?0b_-jWD_xfILj3%2n-v>``Vzsv#}EzepiXzqIn4@?zsJNIo1`RtP;Hi?yf zH!jq26I{*SRjX19Z(DU};ldxACIl{f!7NMpFb-#w&m%2u#%GNK-d!(><+4^ER5zI8 zJSo7G5Q`<2o}x~n@9MP+&6T1~sw?%}s4(aCFks1T;bkSzt606^y6{vhy*bp1E~5aU zLy_*ZXRl?G1Z5Gh#R{s0C+6O48cT?DIi*b1KKvR^5F{Zoo;OWxMk@=aKTSw^Y}}%Y zkvygLlBwM=A&7tQxe*Xo0WeL30f#Jk1Ke~N>g^BZJH53ONKrIJD%i>DH(HjS4BuoLE3+t+Lwn+_0A0s>X-+s0vF4SSB zqb+fNzSP(9Z_BKBGkUOa`YR*g zLb1huresBmq`~Q)l4(XPfDRN+e{DL8IEkc4?Vsd=#sv*rru!jwIDmorkR~~F*_)jA z2?_3(!AK!RIG7HMGQNp4RGEo~cmp_$hlGm~@p&9oHpQHpk%ldJCwlW5uZ7#mHF=)z zo-#!axG71fdV=%~CHV87hy7Oz6y$3J`beOm{w0tAybtt5EA3pWJJFjM*N%>;9pn1V zIsmmtCqF31hd;B{VXrWKJRu=3+sqYvcelM_9)27qLBNa*7)v4~h~X~?A@e#hwD2v? zLJIH}ZE*Ab4Cus(EDw|^O$PC-?e69nrC|3nzG58OSxkO^8PY(- zSsnmiFyx^8TGHkKZ{2R;bpiS)+KY<4pM7?!u&-o(LO_R z>6;&ZI6E>m+^1tXPJe81LJ5{QfYIH)u08lSma+>?-e1|vQY|>rIWqt$%I3L|9=+Hj zRF^{O9euK{y)X}c`~2p0ozRYm2Q6V5+)Y8K`51#~Xf&N-yk>3H(+KL;m;L?HLD(^< zO@Qr!0|#XaOoVQ09Y`dS%Q7JlXmgAw;O^mCtxzRkGm)iTyAayFs1D?;SaPJp53T)fad>v5tPx`% z=fJu{42<(-^P(4)&@HF`VzdhynA#-&%jpkScxra#*nPdoarp2jX?@0W3~Dd%5T<7s zazl_BnKeQz-XFd645`c(AzSU@e{l@e6xKL^?*oK^vPb~2M78gePp%V3jC1pIE`{CM zIWMuH0P&jqx|Y}&qw{CK|NSg&DHQ3|4SSGG0_QbG2m#AgAm6Cx+8sOxWrdotur49J zieQ=geL-=0gvz5Glf0Bi9GMX=X<);Y`9P+6)r2})^@Yt5hR#2M(gZpIE(Ww_Xhve@ zPx=LBZsqP{JzEWi4(-?U2hQ;UX;{C&r0)faZEzTW&gyCi>39%Ps7Zt<2LnUEoW394 z!iE$mYaidpR@UgZNp9`QpMx%4h8$K!ItUnRrYMh3-p&Qd^)QlKt08nVYOTEcEoQcvy?YI5dnPU=#f?D9qtm_1hESvTRY2^yg7-nK(OWc9Lvv;<^oUwd|NQ&anI6M zoBcf>Oh;%5~PDoDKG%LtpeG#T&oQz0Fmpu>MCO24d|zU6Q>VSnQ~5K zHi=jOuFch_6TDC+8zftDI<$Gy6Bl31@}K^**#!qr0tZj=<%}U1PR*&@dw(x-A3F4x zw)2l6BW+01fAH%qW4fVhf5P0JfHs##^N@{xAK6B&-oyZa;_BU*|FL=o)pXPWV?;4k z8{Q2=ght3g4XO0N(L0BUu4u24V#?fhhF}Oqf_X(aLFHMJpiZB(1gd=B;lHvhA)cR znirrG8V(nCdiYh;M7dB?xlH#|(CXn=w{3e4d?Kkd=0@{{JQ#c> z-pPS{y^AvG$mh19ONj~M9cv@g907p*Z<*nS(rov*orwGGe31Vg6n{W2fAN9WRMlUiE>(>3HGHZoW=#}cXu1w6C zV1=epV`XBn@|VBJUGl-BN9R=KPB{^qS;9f1(#)A4uoycHcbiafd;0-?CkICkcSzL7 zed2|~Yos>~kNZ8Bf`&?AR&QehXDX&B9S{QJt5*%$+_qUOSFh|xt;^^>#q3!RTn#tg z)&tWO^p@2?$$JE*`}@jt&<-%dYz8eJ@rCHu7inBQj^Xdn zkBrSKn1q(RZ43R!Hl)4%b^?v-nh8(WxN)#O8nB#)zz>dISh zonGGkhd*>LCwyJB&&EvpW-7#in%`Wg=_?J>=q^l*PeUbcKSNDZZ)c^_+1p#}>lHx4g;||vI`?_9PEL^!>A1?iagVSsL?Rz1b&8fKlbcK zZ6N2IZ3d4GkO5Fnf$y+FP~K{+ew!7kWvm=as@nMZ=eNWGG%Z0z>3v<=Qoozj@u5;O zTBucSzPVD91+-o2poTP19UIVxNOc|*@P9hbp#7Er6j?nrGTv`{Va{B4061u>Ot=FZI$mN0ACN_)UudOVI zHE>BeiP#7rQMp=fHK5yEJbbu#{q_IzAKdFKxthCFK{ZHRVLkCxgJTTCrVN;I@V?YX zADw&4T93d;+86AeM*j(7cTiC+U?ES8*n@dV%=|$Xj;##8hh74i<4^%%;^v_{t=tt! zIE(nQ`Z>$h=qkel7ZSJ;6{Bq67pw6C`x2Z1+@(>6`o*R7b(X^Y>_E6B1Kgv@$VI=) z#vqey{9OcHNM9L7o{*nHKRzspO(}^@Jm~(EZbPb60*bPxk?(e**O6l%)EAEO6ZUk_TIg?|M*=U&=AfKvvf3#h~aOJ9D#G<1E}Ix zM|Fo`u&-S0;8>^)fJU9hazP=ES}v>>h`|en3(cmQfY*v&KD3tJyQWnW5at9y6hp^v zmA6f4g|^yRt4(`YnwGG@DWd++3GmB4A${V=C&)^FK^ylg`e&31u4^Xrq6WMPu{-b6I(RSJ(bBy^7xH37r3mntm^g6i1JO z8jdilINMrSA4`2k5WolkL8zH#aj9DVA})`Q%>Ym=jfKscfqe9Dk(Yieh?dbeuFYut zqMR^*e)q#m^P}KjIy;Y^%edBamgk~>7e%VWhaa8Iy))jyhBe{>`({{e6nc>%2BBzZ z!p7|eYh+BQM)nqKeTdVg@Ld7lC~CGa%K{bRj)iGW*elFY?hyK)UTEIs)BorcF0`ndp3>$qE-#`z-fqaN>zA_(#WfLIR00mX zdt9KP5_@1~sLOL+%g~Rm;eJTM0NxO2wv%wy-3m|6D;{7Zzxj~hAQe>XwvX?0NEfgs;bw% zx0WN}mZwHkKi@&cOa)U?SiDEmEFJ@`VoNp&6yWNTLTF9(O4)LkmGlRADc5vfaQ-YP z^O`i(#jUb9VVtW?s9W0}di2rZTP1!WsduT-m>zodIH{LvDY%rxLC0LL-ZSVh1y+Z& z!I@tAS^7gIB}*h#0x73ZG5RyeGUw)XZrmg~Z^wk8?Kg9O#YzC@49? z7Iq#a0XV~Or*_~t9D9iL0xL#G%HhC|m=@m>O*BNa@ax`8a^+md*zgVXlNG>GXy)6+ zvE_3`o@T^Sx+VXd02R)EhtX#d-FL%Fq)r74ww-qsx_b8P8o=!ZXU?RX{Bplf?$>uz zY>*X~K)wYgHWIrGQGoQ-?vg@76AU@)Fb#i$UWfUsVhgYBrlI~j0TRki!MNgkVO;S@ zvpS}vAN<^@aKjG*IyXd|ZaR*=!Md#mMN%qQKbOQs(<6jHMFQjlQfG?upacp&;r&lw z#6tK#699w>W;y(ktzKD?N`8mF1ROu+^*~#C%|hq6=q>b5i@s&6#ddB(r;9Tgf0U* z$>C}|JSoXX_I!S2d;8n@JiXH^aX2Jib+W^v_B%vi3<>l$mN|8MQci}ODH%cEj@r2t zjKC2nnk^!>eY!UdWn8x>wPvxQDoQR(!}EGyAK=-`AznxFVH_1B03+J%^p z3OC(Z=iT51mSd1GVmODHYXPT}Qkr!4j=QO)HGY7O@^_IW>LPfD8_axya_G^sY_fT_ zl1H)0QQUmgq6xS}aNSczyVN73S=|1 zF`Rs>s|z~Dlp+p6u*snbN4Yz>I$XtfFLY_*(MuR{K*C^^6q;t%w%)~cvRRN0sh$53 zOA~g%cZ`Nn;X0d!Xw?qL=mZ)bV@Q?wuAdCp%G0n4J%>PWA)v(N6m6-$hJQm`ID94A zZ)#n;z~xBYNuS90R;-J$b^PWP3m$Jdf4(i6y1oZFR~I+({62_PI2{zaDShEmM?7Ie+lfoK>5G)As{ud< zzgZE1LnY^J5q?0Nit$Ne?^DLJFdY_iXPKoC*f^yaro1`%r00)|8$o?Kl`Gc zCq+gwFEofIZriYlWx#x-LQyqwA+awnw^J+WpX?PFmu%hhyg()A6hu|4c<{4>#Q91~H~fmBZTOe_proqHC<)B*oXc12ZBH29H;pI%U7bQpeO({g>7jWJ$;0i?vzvobx|t%pDiM#uvH+Z|Av`* zKK%@4>V{oUH<&tY*Cks8?N`-^vvb(03eX4YJF6`AKs1JzU{XfSC z8^lgK^68EAfH*(}SR%17S+ulsH0BAD?MU3QHqsK8A{NOO2BmDaB9)AE>nR%-V|$_V}@&JyZFj4`aTP3<^}HX?9my#4Wk^iIo-FTq@K)(36Hx^`@R%=^gL|=37|adFSh|drqJJ z-~#}dd#8s!#p3t@N=_HiwY8-$?dkbbtuIcR3VkJb7$6h+?Hn@Y(&chGM}L&d3=YEH z`C*5Jz%26rYbQK`1*jJhh8 zT`bqomMvE}b@`mmtIHSY>(3=9k*E_iZd?^65%B*q@pCb-R2{wz+*p>b-(F9J8j{bgA1+p%`rlFMzU4uo*R2^ zp)bQO8^DG_eBIZ<{d-q+WrvK7Ie?`-5IiA->`69|&UK9b=Y2L7;2C2nR zh+cS>(%-ftb6dKlW#+~cCk$=$hwW_>Cfo{g3t~%QvE|k-mXXe(yL{wZygrP~fR!K) zg4Z!WTD}+481ZP~-36HnoOl5enB$8-*2|k9mjswqXLVu-t%XrQ4>w*bj&XQw`io-% zhcc`6^NdGjUTQ$*ls!tHGJZu`7KW<1HxWNWIaKDz?btrr)`^{6F0hTLU#M+C@50H zlm&y%X**l2QJJB~9r5(puOx<@T0m~}YqEKI&!kD01D&#o6I+!*WxFP)Gw2E$t*O^k zRU2D%?dpJn_M$83ZKPhJmx5Lm)rC`Kk~>S0oI5K-DV(1+!kva|E9$O<63X^))Oi%? zj3EG3XH4zO5APepfip5$`}!Cm!XjyI1+b+e{bc<$+}`TwR%S$jD6-wb6>{i*9~C$i zS(TsXJ#m&3a_OgZkbb!KfSXJ}ycLL?JQ)*-ydzrmw*VmBuE}V%y1YgKqeMkiRAg_y-`tx5e$W5?zW03zliZ!1J9FloGiT1soH^$|adL!k&K#lh zewr&}l#**lmyW8C!;6B->}8|MCr>UPg@@ph5UFBRsdCa71P(0@#`Koae{^63N6Usc>BKq-Blh{uCij;P2Lh1?pKF4V9B0%fOg^`<r1bZG~?@PmS!pbX^552eJKA`6xJ(@A&f9es9zpP2Va(MM$Gc5B4 z;w}C!sR0sD5iv*&8Wu;roT3|=%ws&Q<#A3+g`Ap0dK-FrNXWiibByU;svZzy$@g_MPwC|ytZhk&kS!OsjzOeZG#km$BCFea+HB)7aY0GH8pGJHJnK^0|P zC3K$Ox4iqZ0>K*L(K8*`6M7t~&fo$+k{?KhNHD6P-a+AR zb>Psm(0k^zQl4!YFJ-U<&0R2~>BT| zr9s(&ft0h#LxS5%>kMVqdKf?(VHIecNZufIhuoW+nu^-5Zfe@39QM5r?PIW)IjRv0 zjeF~cP{gjq5cH66)TmhNDjj6F%y>apsewnTH#&pO@UUI3q7@&X`m|tHY*tXEBR+I) z7y3ABs`>Q~$*Xf=IR}3+LgXE$$d3oRM8p(xu*IdC5LOwDqH&B~6ciTbuI9{!+%`XxQTe1n`MQD3 z;_9#Uxsy`p`D`{g*Wky_eB%vYIp>F=HwiUvSGRrqpra1y1@uH>(8h3Rkl`%RQyx*0 zZ>loo7FUiM1*g#I!TAx9Q%168v1GEpSyHevNQ$)Agk^=>Tw8nv+_Lk(B&UMR#6N?E zD<+aZ`*Qc&%Y<26rjD6_wy%SUF%CZA*gvqfpBP^TI{_tWDKL3-+?jp;201xJG^O!*)}X}veYQC*QIM-4HSI-bq8s*~_r=B= zMV&wPM)42#BcAUrUMZYr8+fJ#$05g6?|K24xv_55d|pGM4enOJS+jOti(xpJnXFwO zxIprG-@ZZ!@vvXl*Z(pypwfoJo$87JPm?)z{Jdf;IP0%1N?ScfjP`4oU*|jhhg1|% z8{^Cf36rA(0y2Ui`hpN~C`d3#B*z(16tty*9D{>@^G)`v*Oek}Q{l2zWo(R`Rk3hf zl$2=7+ScB*v%)XIn0MKgI3TIoTv5FsH@zw#R5U;wBBBlzhrCWq4$YWZVO?F&op_n> ztX%B|eP0B9HN7(t79-f~CnXSwRH&%3oE`(I(Ze-i14gEzUcIp5t&KyBKoq8No(2Vf zh$i^;13Xy(xj%3%2y&ma+nl?1-=lB1sp=}~0*0o#n_=@eI-cn3@l4*cW?o{If`#-9 z(ND(Jlw^M{FeyeKn;NSxO?G9ro&V*1DIf7)3gd_+$Qb7D=fNSlK{Q7ogsIU$;tFuc zwAw*V`jPRMRqaeEx1{(h-_Dp(HMustIKL2HV8v^T&VK-1wH3PR3tTmfpenHcqFX=% zzj_a^6Vk)L57aPb0DD37BvC_@?Fg*(R78;hnXRF*WB|FXpGrBwh2hr3z@Xq5Q(}NY z42Y5gWyzRi4M{VvuO(Iv{2W$)-+g!S?C42_VZ@i8yP`l25>nGeS=JL-#Z#=9mnW8` z)Mrgl3ilcU`L?t_8WBE^^6cEjocCsC>f#7e=k zY)q>N%MRS&AZ3U;F9p}%cH3@T=E5b!Fk)&rdQ=M1r=+S#KdLd|R_UCW_f$0sDW(6@dG-Fn!bxpzf56D~onSOjg zI2pChc6+iZQL@0xKY9Af+G{oJn7PvGu(UFq;93Ifhw#$<*(Tw~TK{a|<|sUT`(u|r zld*YQFe7To9U&;0UVUX6hIY1 zIxzvw)Vo252iS)-`nIMfWzDEy*GNO6$yOamZdCRrPAavwzglNWHj?=V6DF12`#Jmy ztbS+g`1h!1j0G>Xt*Adp?l(qZ3{53>F10S0nJy!KZu_`Q@@&rN*qG6;Si&m8Y~_I&fw@c& z&O_;VpspIQ93&t9)SLVAmZZ6Rrixg(-e!8$?f&WScGB;Ah&;aelH0TGiSxF!I?Kxs z`KI*T{I;{Z>cd10KM-t z+odyml}su?>hF+oz9k31x`o7nkMH?$K%fK-F?EHI`qbh-dxI6U2G zrcmn)f@0%I?H7H7WJOQ3el-0&d0EN0PQzXj_)+rs*3!xC!nLHzebrTNC9;h*`0inU z+Va^rtNr%buE`JYa#}}sFT4Q(!IcxmZ#lvBg}BFFg&9#|W{1E6j!ynVh$6#nmK4%M zHcGriwxDT2OUr^LCHU1aht1wrtK0COCk(OGR!4PBC|SUzD)*9w=U0*sln(Z%DK9-g zCT-5C%bg?cxc9D+Db0^P+0k8fMtFq;5V(9h%+2H?aQehRJOp{K%wj{^1_TGXP0cS7 z$+sj%2wn^18GL=%uN?><6aWznpLP?)_c3)bKcJ zH$fC+bNtmF$4CxmT0|Hkj(X7VmcHbz6SkjY!M?)I(jxY-Y; zr#~Epyt~c;9tg_-V&H_%6HT7#$`#un!DZyN7AQn+Inp#p;{v%WZAG zCB|{DzB8S#y~^gcUlHUc)0GCU{QN31#f6g_soE&>$`fP+^wPesT-^!%LN{sRn0WxJ zP_2<37^&#e1~dm`!6-yU7JCfrr+2dl0b3*T8&4>T-t^g7A=WP)-}V2CR; z@TMNI%fO?`#{gu7cgin9%BbWEt2x@%_TkpM?%KNb?z^|nUa?~K>`O0I!iOuY@O>pr zZg)?f>~5dzEVCAsmls*fKBqgtYW3Y^MBK?u;h0(d&vT#xV}d)iD4-Ik1E_!%#$lK= zsYIvYawtk?k=vBVo7XL?Fx0Nv+~EXH(33N6R)j!0?NpBH{3NI3rz64VeDk3dfBc@a z`*mLQ5Z43TbS?zM0#0CV0}|0z27l;qxsF}h=KDfsXZn6(Bb1-WqVsQ(MarL_Dkpws zFLylPobPbV@2=B}GkAm*E$Dv+^@UM+>iv8t{2mR|=DmG`7!oeB<9&a!0lwSVn_Fte zIUM6&8*k$#IInSZ-)9sb;Bm3K0Yak?LW9}|$KoDb88nR68n?5)zVVIzPr{BR|=H#uD7&rzgwpr*R`ubodG199Ck#_PYJW zA20}uAyI9rh?5`!tV3c>HQGpqU>kMYTplRv2Qh3p~RD?;_T~ewioQ{h{0(0 z)B8^C9#cD+wX>qJLX@~^?@YCME6w5y^EjpxoWBBGME#Vf1lfZAA%)q zSB^D!E^`QVS8l>VaFuf8cjfrmN7#k0`hFz3FV9~IA6HIE=81k4xVHmCF-PT_?#2-D zNj9*f|5eTk7y{vkFo_PG0Pn&GG13Nch{ij}Cgsi6RkMo?OO0;jl<~1lksz->4y=Do|)F zk0Jh~N00*{Yt&A&z0J)Sd!^rwxV}x8aBEB1xBnsYR;*aj+`Iy_+Db%OKK3dx`Z=)_ zXnS1w=r-p^=bt3?-JeE?Va6yPRD4|3RkW>=WVK82bdS{HHC-=8I6KK+D3HyH@`tL6 z1hOsXb*-L;SlfyT5h-`hh#lo=<@IBz30yLu%mpBlXJ>g@^9abWcQnH_nXQL_DSXJ zeacgWf1dqo&*#3szJ91XJwocvEu1! z7bH+w4L3^E3Fp=}@^qW;4*bTA;VHg5Sd-!)&-kw4^23zvZ3tx*GIy@>?}snBggvOd z`@_F)bXWlY+r#$3 z{Z5ziB{|?*QT>s#`@0M=f%80}l1{4%x`GNa9SZ;%p{c<&*zeL-^@#vHad>Rtye6@; zt-no~)Ye}>LfVcUrPTHPOj6&gnb??W$m*EWP<-+O&b)D>vgEa^uU0-*wvimR^JUlj z1#eE|4A7DcTn}+~^Eo0e%T9*Y(5mvRg{pxTv`#xj&&TPm0wfj=gftBXd!+;V!olB2 zg4_L;GTZ%I+gs;4+PAdExzV?W9=hR%!^*Sa)w5lG zHrd)zRM=81Bi@XyH3vM<)}5IrMiNA89D|Mj2o(;2au2r{|^3^{vA8f?fZk2y*1P;HrETsgvyb>$y3XhpY(lu-FR|v=gtY;u6*&dK95dI zhq;x2mr_u>{WMI&L`uUF74iV>*}H@lgFFp&v+B;&&AOs)X5BA!Grg^g7q_a`mK@up z=-4RVjhy+4E0ndz)~|nV;O`GFx!zIk*fYld{xk=6LGm&C&*Wk*Xq1k6ts#|h1{zhL zq0wKUV6APw-qWO~U@;W9 zOxlNk=;GdO`^AtZvbUpn!m_HU5gX@3N6pzdBC2}nM62@0e-(+v@fjn_Q-m5rUPB)K z8)}4<@{t+m|Kn}7QHAQw@iuQ*ow%-=>fuj-eeyuZC>YVG$|Kdl>3V>!PzJ|0oCW+7 zDrf=g2U1ie zBLpf&%D@CX(7EB|mueG9MJm{r3I|iDj<+e->==WW$oH)uU$KK^5n6dQ>H9#TfV&Q(WvUe>>^u$ zKLYy{RQdcr}Q_kvIhoiRIS(zN4kW9a0pFQU4$skWx z=FJ%wSs68ok@uuor#soym2;Lx4A()#fE2Ml*OYT zI*`INr~gfB`1v?pWOZDXt7!JXp;b4 zdqAAh#W3Iom50w7LR8)(>awz2X=!w& zTq8c^c0VcBbX7U#R#naYRe83b4M?$NW;&9S9hsT7lo2^?#ozl*eOKbnDebDQokmK% z_73rC%ty6gCddmcv_ffwSCViM44jn;SNvH1D=;M8(V6DTR>9c7HbS^#7pSvxTJ6Q~ zu*X_!Gi=Gpwv0?$vah<|`LvT8o!wIWwa53@XT@zfCDUqZrj=mCP+%$mj10k>HwX;y z@js&j0MZ`~ND5A;!w!(Ls-^)aZEa79HPfmb^C~On1&}j}%+@I;>~Y-6_B2*jH8xg7 zrBx?7Ga3tzD@D&0HD*~SRaQ+bI{(cb0B(iMVd3ZtTWH9>?W(Q)%LnFWmZaZR#tLK zma@-IV4+rNH1Rj>;Mf>)+;^;`)9hFgQ61_oDIOmlHr{G&52+5cyR*!Kxnyx-;*#R- z-Z|nS8J0tJm>K9ME(cvE7MB(;KPDyyK}HU26VZK7b>s1v_ou~TsOuiKMbWjb8x><; zH8CRPt{E|-*0u6~V+#D6yltIQT|LFB{jO`Ou4!tjscus0M&!4bOEpp+mKGBNYNYb^ z{O(*z29q<#Ss?Y!R~@4U41fq3w%nV%ELw`l$QVO7R+k zZdxIv>2^3JPBv5KZH*A4;e0B{yJMwj@n)?SMemK3V!%k>=j5in6FRc0#)O7V!1Ugh zxsRM|UfPWRe~)Qw?Cfl898+7An_EN^YjvizU6W87F|NFJPIPTV=ls&NwV$8%eeiov z_p;UE8lyiypg~2dowRtj-z5gmq>>He^TLfd?8`$}I^5>li{HL$>B0x=kx}e!J!He=JMn5JvS>6GtD_?$V)J&!C}L<*g2*OJ4Iyp|%-Zpa^YcO|yy<_vsIR}99qm?x#qO71DVbqUYg`r`eQ8U& zV`ka$adi|Y7DZ6Pq&S{B*GTlHowy1SoBNXFW{U)ZR^~-WSf{g z{et}j5~Nj{F?uN~ZO*u$QYB8^ALp!>I>O54wglV5XFBQ^MA<{y7gvWl)+pb(l;_Le za6L*XeuIQBdG^;f&t*FS~+ma-Pv?}kT z`LbEI#O4(-(QDkvj@jkMkKb!AEpa$XO6}!|(b0)Cu_xYe9dSd&lFasCnQUvWu4YT8 zI?TE4HRjqNcbQ{as4dVnrP%D~eqyUQf(1^?RE4x$#p&>DrQ);+dWE*iNIan`q;buA z_hz+~rrIWkg-&#&m9=Kxbr;FeSn3Pqogdhp^BqR(jHZy9h$)q}+0oTu?s;WK$E+`Z z@xA~1Y47>##Z!K{P#i0`8pKjcU~;{`TqnlNp$i#nA-PD{Q*>9(wI{V+78kvy4gIYE z4Igc5d+agT1YaQRxtht2;QY>7bg#40J|(m^&^5^#>=>`4JmK)&efwD32UG1>Oi|@O z!u#x2F>kci+MC16ePtBdDxYmnf#MXsvL($ii}K5(Zug^)Vr%BN16h4#WBzK{rlZtZ4-ay1E|}vm!OnZs=WeddUxC;hIZ2H>cgAEn@D6EE~9_P zkJ;KDS-)XMROE~e4Urw!j^UN>I|f%VZZ^iZjs2*r%U;xGHPjk1T{$_fOgvcIij)@} zk32E~$2@QE6sw5e;8rp{_{gV-jPh38Ec$zN>bVQ{XfoT59x0wyod}MNTHcaaJGJ=e zi)W;ch?U=u4L9h5?4zmopOwn zvTQB8>int`KxG!B>^v}W7*we2P7# ztS!6QR#R=`s#|K}OVbQhCR=Xd2>&W$W>sR1vdM-G^KzriUB89J6~}LV_my;*E9VAR z2Q?Ptw}*tZLAV6@Rr{BXN;RAFXU4?N%H{QV7?A;tX67E{+2q~Y_6l@Eh>14ll5aD*PV<=}sEA)nAhPU+^NNzg*U38<~ua(@@<~xMnN*CIwoa;zwSsfR%x-|_t zJ#9nr%fRl=-p7|LVNc9;aHZ2nS!|(QRG8U=Tjo`A-79VpPqWxub5XWeXGFSV8A}%| zbh`&TmR{vnF|_7gc7*RK@;hRk-A&x?7hm)}KyG~b-S~w!W+ z8~w8o?wJ(8`%X!05=+lA^_KPL-#DYZ{>wvTS94XkDt#YkeRXfBf8@}D$Mh>~@hS(wB)$CN!$oYPd*kq!%6u9pE>m+7&*lcBQ z_p)WAAjJ_IUS~#h8=Yfh#r2aN-fQ-V9Z;8D*qPAIQs^N*7@Ha#1mV1LpurNlI{@sd zOz<$-{o-eF%Wlq4PMXL#i(Ywps=8A~k5$;lM-<;kN7C%t5GO9RD=F{MKGi|=S)n9w zh$919sMQY(y=LmxVBnaR_O1lYAoP}S9X zY09oWowk!*cCPdO@2&Qzv1Mp9Bd`0@J>q74m$vPZt|AhRmRy8LO|u^SO<}oU>ORX( z^PQpBvsnwG<1EJU$U?<;uCr4)c28Fq`;x7>y2IA#aCCa7?Gs%CaQ?(ZcD~#9op~5k->qblqf`0$1bdggy>`5)J29B6Ua|p_6psE=bv1%?VdaDqz5Wn z>j-(-=~Ns#p~__9vFTc_BS)04b~wL>`nT33uYp(Jw>ggq^E;S}P;-a5E<8+>JlHxOGx@Q~^jjaD8P#8h<7J-VuT)Tj?teAXE+>F&$<5Sp8JTHDFoyJdG!2B zQ>|&!P_7fkThyfp=I(*k*{@n!l#J4j{QQp6;xPytP+VZQ7ZlpFADoT)s70|3Vp z^i+UdV4YZ1HOXq7RE5P$U1LRMV`F7SBWc3fz*8<#x1oF6Q{px@obHKhw;Sk=IF_)3 zqQyJnu$U^}jPqW1xYsY_^*6 zTAj<@9e!AB6T|6_JKf8n*RcIjH@&^8?&o-qV5!prK0~MQ+ZwO#>yTTn{nx*)rD?|e z`7@A|1=>!J-i?EYbmAnLcCvGX(=l?S!#ScdJtZYAEhQygnZ3usd8X9XdgmPzFZWRA zv-`Qb&?PF5Y3Mb2e<~SXBG!TI0m2LFm8xP9?~ybf3pc9RA+e!xQXm`01?)|d zEQkvh1d|Kq`P$kI_($@$;NS9+J$tsVjvf_n2o-AlU&1*~fVnsYLFL8B>WGpkN1T8F zg)LD_H{WuL(o{EjvLQAhTAyxA391Vzi4{T((ZyNviYv&NY1y-h!C+1^%jS$&k6%bK z!iieKH2qN-4sJYYMfXTSQ@DmlcSAWbN!QHJifzKr#!M8|8Q$79xMD~>U9ze z^lfSXt^p5^)WUkeOjmVG1r#O^pbVd>0}DtF4fUzu z5K5|n+us~4iNQ6h;RpSSAgNZMs`SFJye<{71k&`8B-)eXALEze7p)w8rt}46R&889 z@y3OmCux!MO8nvsslM0z!u_K8(7ZGRD^Jb}*0ZO4Qwb@YbUoVv8?a@{bu_tHW+Z-- zd8WYZ0RO=31VpV5Fa_pEF2TRO2#+0?Xf5gt7h!rTKFc|q`q z7@Vm0-Z6>~OViwdfZf1!gWRXRrJC~5=r|?t6_V_Ydzg##nhtQ1w`Lcw^Hj56Wrt^r zxyFpKBWVQo(aJ6am>}cyBkFlw%NU)>=ocMiDUXA3V_dN;`|JD+XZ1GGUvM_)1lDMg z;Mz8KL_=;wo-unWr(;k1Z#U-Lr~6hSADgmc<1+o8truaPl2~ae&rX?K;TLNt8JE|t5Kk>bNOlWJ=6OZ@`;VST}x zU~tXqjs7+Hx}s|uY6_-hnxjo(q)@g`2?~pJMTEyT=IxGjUcO_;7vt_8;k5c1tvR|l)C24JqAhzc+i5YZ8u1i_DDamfjL{Z2 zynmc|`oBi3xiCbhJ|z`{U5i zXGhiKQ`H!t2pNAtKqK`6k5Gv4M&Nxxwa~>>hWBYDXnRoF3|e=?fng%GDEuGdUmf}_ zDY+}FFfhdu?8y$2qWlq)CLl@>jEI}^qfjpUIU9^oFm6w?S!^^smQ{IB3^0(9gdf?U zFRS6jocM+WguNNF;tf@xzGgX4Ky^CMQF^kWk<@Pkww9X zK@(ROlSG4FMTe$Z3ap{oA&zjvp(jO_3$2ktriHrv!mbvAjo}Mjh6p)?T%m^3THI6@ zBPE#v%TsLWYA~(%@YiaD;zRKxD93`{Ahj?yZ+1^%a}8URBn!bicAKB!*rSK{Ov?yy__a zg}p`OrAwEx!IpvY9nVfqm=aJ@O-7KZ%3dw`zKovlOq{-S*_;%eADgIrypbJEXiBct#00EGy@4 zH$?eY1tu9xcbF5{ZCU2rPy~D1l_@7Ane)vlNy_=Gtd8vL`?9h|?8r=mwBlK?4dJRP z*F@9su%={KvBIt6WtEjM-_FPm&x@$>PY;=1rc`8w#n>YAESVXc_yNW=1%%F; zs({fn7dm<3q>iJunUF~&qdp|XkW*S92L~HjIn|#J$;l9{wM8O4>XW_IiS?BM4s&^( z?-ggTv$VR%^;XPTw!zoT9`wCxNevd`{H9->*cuQ_0^~wZKu+j8>(?v0(wjo_11swe zlBB>Slk%e_kR<%4OD`DQ4*xHWDwm?X8 zOc2$3MwsGbx%gOD?2VN-S#mu^p#hXV)oIxy1SGI57%*PUadQp z=uA^Sjf3NE9qz5*N|FVC>cwEWr>?so!LXi^Ev5fHT+yk+0~B(zZ}a)c#=y3 zRIvbRfi574HYw2GI87Dcwhm9K`>G*xcIY{HWo$;QvjQQ>D;%xqan34B7M={@6;%=C zp`~S!#5%sXWYN@w%Cs59db2UHKD;PxTxwKjjbEHtH>uR@epDFqkc(`xR960LU#lUa zDASN+O3Muu8X9(b@0iRdIGq;O9OxIF5KtBv2an~M(e|j~_`1kRJ2H$ZX6yZ*tP4<@ z(yK$W1GufY9hGW6Sb4p7M~#g5>Km@v0As^ob4phCqiKfw`68XF(HvJ5K{EtmGv-#t zLOZ51C=8D1?Fv=vqsN&bQ%R_I3dbgzic3%gfgLGkl1se#xrPWe%5lepT4Pce0z^f` zvUk5q4v79rc}Wl~LgNfG$vU}^w-}pES)uKv=~GMPC~;w1&TNDXmMi9EXWGN{3C=RO za4U!_6cX65--9dD(reCt?ud`~uG^vu_S7ZZZbVSU;M8b+8E4dwcicu~iH0E!lng#B9R@`uQSw{vmD;wygx*UAYONGVWmRL6pUZz}ys(R9@1*>e?jcqF5)v zbk-SOj=I(w zP>z5Yp1MRhQLX%;JQT4}VrnXaz5COek`YF)a{Ws3QADIYUW_n?7i5`|_kM-DqmEpH zPz$&xDa4LUM2)E2msV&BO#?9|F3e^kdP85 zR+!SWE!SP2-5$Fowy7;iIGi;m-r$K4(ii3#YwFJb6qBglx}!G@1Bo?na*6EHWeq_C_a4MgrjN*1|Pg@sgnX-=voU^M~QgU zD2rIC@wf(h0jq%SSnIOS|M=s-3L^4lI7=2wInKaIyu|A`%xnRngyZ!7reOF3lW~nt zul!Le#V23^6dPk$c%lDb#&zqh@W=nw#ZH31CYDZr1pGjJP9dej7`Vd2`8mgh#x+Gp zM7JpKDW62?dAN6xa$-$FOr?Z)mx5@B$sm!}70sQR4z(|7%)~5~xxlG#!Urx~0a@cR z{nK-Bo>OU#p^;B;?V9=)J_x}~U9cV*@cyIi4s=%)8@zm;4o6cDa|b~Mx@AmPt4&xE z4B?Q5K~}v^(rJc{Lht;OB%FGmRK3npL5{7HL9CM25y`U_6&Mrz3NM*j%o1O}mX!Kd zG0;dRajajI#S|A}8J$TCLVCR^%7{4c%3FnYcGWXh_^BKjH(|Ebs$`fGErvML^et^s zQH2@)Qj$@QOD8pB$L<(AmLzm`7LZDR#Hk4t%c}g00`v`z283Y+eN4EmO!2d1!)awe zW_D0I`N%I^_CqwFTe{yZH=N`PUczAtou<;kZ1BcC=psiL@XZ9R;h+APyxVypHW(~6 z?2HXeNS8q+-T<2+DEu#S28YX0VUk3Q5k_51T5d7oL@Hxjy*8U6GMKzL;#U3L`qZ|1 zF-9IYT6$P&TR^;5+6}>h-);Trz_gA^zbM1x(Gwp0)vMg){nBNK4k3=mq7oe`)oYg* z$nl1%8%Cv#Fo)%I-%(>&W5RSB52q@_TMsi3^X-VukpF+>%Yt^q>uEGZ{hv6rKwpX` zy#4>np9S3;q5CEp5hCF+t9sz3{jC2+3`lLP1ig?!F(dzX+!nbDqN}WCE8JAo=Eu~Q z1QeJ{YQEFl0lB=3UlgNC*AKb^`mYX*OgaAPnYi4Pm3cVNKQ+Tr1m*P%Tp20vd3$1o z=t$kLL!5v5GU|?OGHhlGgdr-yjW?{>BB0Y&F%t(nNrU-nhud^;sRPWrzfg#;{r~WT zGqxbQy4Y+D8{eKE>nIN_3MjRcD~8?S_`Xniu7-^JU%SQO{Br%%GJ*=DosZ_5(lRW? zL7C}K54p&>M&-h=qyK}aobEeX42K@-9;X<_a20O_G)2755Qc$(H^Qcazzjzszj6OL zABJZ=+*Fr=$Q8`pn9>PK7@5lcQalm)G4BuX80HDViAV%Y?qYCmn}iC|m`qGJt&zP zGcyZ;6$90KwL!R$y#g()Vm4!gbqliX^bA(XtjFgKNE?wZBn|EPA#~^`Ij?j7J{m z$!=x=8oUa~+=R3l=|dF#3Q2oqus=P})n@^%vyqkoE6b5Cd<)+C)qeLux4RY%-wJ9c z00_zlz34_Sa{C5yfgcYtD1V-SJD?a0L4Uwx25ed-)H-0`wE>8Gu`t#s-pe%O-A<%` zvg$nKU4pb2eO`jbmLgq_v=-?Kq-)XIHl#h^BToV-MY)l}r#6jh&r-%2Xs`!kfqp~B z!7QZNNW%@#anXYYKnUd1fwdB8&Bd4;0-cglW7Z*%Y5(c)nu;_HsS9a3(qI$A!%iF5 zD*^w7=-%4jJA^Ab7JA{}r4PXNq1Has+J{>EP-`D*?Hgjmd3e4=9Yr3EM@1m9597U$ zS%@k;_`442O0~#YDGB__Q8c3|I(8|^=HvSXNCfgS2xPhxxhsZ%>0yY9Rmu$-emS+~ zMQVvYlwXKC7coZ90Q7yT>^cM#9zs@q2($7eb zU62RDBzh?k^%zuOMQwjYM zu<l|&?*vIV0HxxJT zAZcw3lB5SD@$^Ez_hL%uWqv?XX@b9hps?06N{$oiG)pI;1I)wtmR9+QT3zo&=>7ry zJB8AHDo(WC6XqZ~VI080KJ=S{LCK_o0sRLLpwkfK6vecuLnwOof__oKLnog>zW4}$ ze2nzT5CQt2fesSjRW$M1P*cZH@)$}UV}1g7r`5@sk__`df8iX5fkekQ<!m-Cz9SlG7L0GC*1*%k)x?t6qkdb(xxw*ia(_E*Lp!^Ti<}@Ltl6py`fFh z;iQcPt;BGT9tMfDcQsK;n^rYF3Z{xXDvDKVBS&jioBcinXj+{IJZScS3bm#rQr2LZ zXYuqFq=VmU^cXLyWgsD20~=|JT!(*CFxpyBKV z{5^_9hun)u+ORu|v2Ye+LF=q%m?t%Ur#wujOv-3fNqz!7>_!?S2`CQ9K<9n>L{X@5 zJ;em2>qn?~K>X5Pqa4kpBZ+p8>Vg!9Cve80zUjg1gYfmJFrEO4dV!)|ph)BVVGvKK zqF7@hRVb?rraf)I+mWbL8?ehz?{cK|=)?x3jYtn6Q68dN^${eBpGFNqCxD<6%$vw~ z3rQ1`bQ;kRbs^0thfuMspgfB;B9GPM3akY67_yA_tpH=hjq4hdygvjOjUIf(^v|ci zfd&DIMR6UFW{@ZX31m6_&8WGw6bo9isEU@2G-;nz7Hm|zMhnSH6W=B;P9UW|qje%Xc!%<-6s3nANi9Uip4z zjeJl($aomu_*Z|Q@m&82z-RCYe?PfY6u< z;(se)l+)BwJ-}fMlLinFZwE~eeo_Han>~b951@@hvgdKgU>zzQ?9(Bq1?xzzYJoR;^~Ci8RU5S&!SKtvU-r! zqZS&>=|v6%9;&A~lqf^5sYIbU&kX>jk%Y+{3hOm6V zE4>2^Q7ptDSK|PhODob|qg7J`?nBNED073_7{wpO6y=FyY7WH-Me=Us98hy8CtaL# zh|+(MxwO}4)q^<@5V8T7F#2;tN&`|~may82Bmc1$`X$Yra z4+5j%fnreW+9K4j7}^V((FzZuNMkbE@Y#Wev5Z0!v@)%6{lH)xWHK#xupTwmZS1&4}M{00T7262}K9t+lQjP@)U5qR=h_mj?Y?YjZm~S zWkZd0gB+sq{6=-O3=?#?90i+_n3B&kNOf%xo=_pXka8L}2CLRkJYEHO_Yh0ofQt6X zSly!=yBT4idTo5nL{Ar?JeJY_?OUwg9@4c$K+iD{TZyDQjsFG zcc292ehno0z9tE@n@oa5)dH<_3A(K;{3WsD?f|BnK%v{Hlh~jq0!)CdXho zjrWH?5nuq5?f{9k5db_xd&Bf$9QtBVUJVNj+Ke{Q48A;AW}ixcVQG1v+S^`r2o8!h zO@0ou?jULDsGtP#U7d5DkD-#%f;1Rtkz}pbSbgisoV9X#Z##ZRpZ6!{E{p zM8BtRXsu9o)i5$1Ezp@_6UM*18ELQzItT}0A4b<;kF`l@5ZK*v*9G()9Aui%qMaHZ z{c&g^9%(Gfk3#~hs@ji{@miydv8ExF=QP!By1Sx`3pkGjOJD{&8F373lZYOhKhUum z4*ENOh6eq2EeXgPsd3hgMFStX~*>Cj}drACjMD z-j$EZFES_PH|00Y%m<(mw9@{!@t4R)y%Xb&PtK4gO0%Td(jw_s8F3cn0Jv6-k`v`5 zIYmyDGvrJ;Th5WopexqOBjnL?qdZZbD$kVX%Jbz*T0e2csrTHyWiL-JGd zVfh95rGb{{kr0vBdRHYFYty7jQkSfkW!VJ%bCf(sUM^oF-zD#t4-Az4Q!PEi`$d8= zDM}V)v)m=`kZ+K0RKI7jacu{Y`aulk^qx@FUT!1P-KzYUpV-p6Cf0 zFI_hDv}W+hBV9iDD6JiOx&lvH)t;fJbwf{A4n3_OdfI>|t&xpGPge~+Z5n#oJoI$6 z`sAT-T{D!qW$5WzJl%~nfDh?&`~V-)=jcfk@;Mxh**|ejb-u0pTml2 z`GGg}7_9Vtx*pT=u%U}J7AJineJS;VdPH4u(UqEvW$JLLKt?ZphJFl}(^hKOL;6Qi zmIb(+#HvfNU$JsBWBp{p`Y8}As35GSWUQ%-kVph;so$lu(jU^FkQ}G+^Otl^`dd0r z7gka~7FGnyDo*CHtkPr1h!7@5Spr-8fwBF;v=~AjNR=QtSPqdxu^&txq6z{A ztVWrQo>8+{anaL~5S&u$aPFOr+)9`SM8fvwE}UKO!(r|+9OfFBcX1XT%e;@%>jVZn zZ`j28P%4xPnU8Qn-pYK86Kyy1iPRx=FrRvGWWJ2~OuAIMmeDx)PaJlC%bdf3_bHqg z4{|V0<;f;|B2wN3dBy!HL!YSBO?3L^_>{fOgyMx`y z?qY9e?_lp|A7US6pJbnApJ!iYkF#&EZ?W&NC)p3!FW6r88}>i!DfTD!7xq{7clHnV zFZOTN2hq-QJg4W3oFC`USvXG!7YTpX@vza&fFFfIu7s=RYPpeI6W0#cqm#KV++&>0 z&E+oP7II6t<#33znp?xIM|gtG+%<6Pzm2<xevGxxlg#yxi7e0?rZKp+$ru1_Z#;YcizJ(*eWvce!PVb=cD*|K8erZ zv-n)TfVc9cd=+2IJNP=jfp6rS`Br`mKY^djPvyJ#8T@R1EokbU{3)jWzQgrFMOkIvHUstFr)>Z0kI;U==Zj`Q3*P?6Fwd*={ z6LnK{UAnot`MQO=WxAERHM+IB^|~#(>vTJHx9D!y?bYqmJ*eBSdsO#??kU~Vx@UAR z=w8vit~;T7U-zNz6WwRJFFm@ibl>UvbU*9P=ziDzrTbf_=o!6UFYAN!h$^a2)o1BT z^%eRmeU09(cj-s!N9h~&&H7foTi>DY)Q{6o)KAt=)pzM<=x6EY=;!I@>lf*l=$Gqn z(BGu@>UZh)>hIM*sNb)DTK}T{W&LaV6Z*IG@9IC+f2IFUe_HSPRex6hr~aJ&yj~Gl z!6^74lt-u#E<_5^LaY!k;ChmfDx?dULbi}AV;fQcdcv*N`I4Sf99}Ay( zgkIqr;d`M^_*pn3{2`nZ&I|n_e6olJxZ*U6L1L&FDI!{|i2G1tzE~|f#SvnQ*ddM; z$BUE2Y2r+Awm45*C@v9~iz~zp;%4z0@p|zFai4g<_>j1h_3FPTU$R}CTbHe?8IlQf z+9Md882^>TMjW_E(@NSAW?~ZQLKK;~qfxU^{&F*3EWFKH3W*=jpVh^)N*jLzB*%R#B?7QsyY!CY-`xW~w`#syo z{>+|Xe`C+Gf3oM;^Q^+b9F)^>0w-}M&dddIo=`4|i{ldDWh)b2Xp6W~t_I#t>$zsG zgPR2RwA0~%dk$P2%;y$yOL5_LC3hKjIk$nkio2TI!d;J`BD=WTxjVSKxqaMy+=Gbz z{1|Q#J`H34=ivhW6*7xpSP)!}aroH}WPv zh>zf-`2;?h&*ZcDJid@G=F9kM-o`um5&S5=iErWC_)dNzKZT#hPv>XybNG4u0)8pK z{Qqg~%mZsW_Bh^i?rfQfqNos>R6>G~5Tr#>MIutJH6chyEVVRw*0l8;ZB=!pRT*1L zb+NW+5v?s~YcEx`m8MToyJ%G@t@r!Qy_Xx?^ICuWa?Y7MXU?3NGoSPQPVPO23?omI z5o9EJj*KQ_$XGIkOe53DY_fnXC7+X(B%iD!Uy}7?C)rK*k^|%*IZRHFU&uM){7x>C zOXLb=RMKL!I4wy_)3UTYeUL`bYBY*Q)7rESjiZmyrnEV2Nt0-6+LpGb9cc>fMtjf< z+K&#TS#$`^p(E%>`aFGwzD_67$#gn>m(HRe(S>v|&7+^w6?7F{M>o=~bQ|4G_t68i zfF5+{A$pXaqJ{KVdY=A4FT*#-EclvmF?g+b5UUJ7j;z5RW(`>*)`UIATCkQZiM3{K z){b>xomdL%!n(0E)`MlRUaSv$g7s$uSvDKQhO!APmrY{R*t={t`-m-ME7(_T1KYy3 z!5fHovi^EamDZDC3p}o&nxf;cqp&L!+8}R$*b`wUX#c0 z+Pn_0#~w_7HPv_jmuFp#)HKiiU%lh60MvAk0#Gwh0jPIx zuL0Cdj|Nch-Jk;YyM9z3cvOJ;(DBFsQ~_$%^(sKkzB3&lf4MUqpyuA70~`I;8m;P3_NOP;5`QtSB|wLxuEo785tMQ!CjsIS#F zwOxH9Pw-#W4*rMQslHRY)Nb{yC?-m(J!-Gohsr>nQQ!Ni1Gn0Lqc)JggEn|Uyr>Sy z3qEbYj)5w0+FvgUY!}SA-91gwC*@NM)#B&!Dc zAod?91ZNVP0wUoZkO?!{2e*|9cT*`0^=Sp?{}Bs!tQEpUq@P~6omy~;$OVuJC={-f z3HAUh7U#*WBrDnK126J@3VyxdSYxc0U`8Mp<^txzT)-#RGArL&XKk=HS=%kp0oH!& zxOLjPXk7vKA7q!eE7&1+m|e$?vm4q8b_(3)yW4{udxZUhooi3AXM>+#V1EKmezUy| zocuofko~iL+&*O&+E-k{Rnisass|5?YYLC(>&c@kT}wSIE_hDl0cG3 zd(x4lkS?Sfd6Hz49C!`TOJo9>L_Sb+$b7O8O!OLa^W03fl5M1b93jWZNm59DBbVV@ z9`I5)s=z-7(O?=zE7Q8P0c}Jdqn&6fO{1B#Kh37Y=u_08uhMZemrjFw;cD>0-#GMJ z`W@Xve^QI+F?yDsqrWqFnJ5!XvU^!6c0Vh}DzFDwC<|j%SXEY?)r3cN*JgECJyxGR z%9^o6mdc)FgV`|l7JHjbXCJaTY##fVEn7+j{Pznxe$|`8V@+_v&1z+JvU-CF%`q1A1+bvw!G6AH&GA~# z)y8;k1mo$s$s7aIc>(MuvCG=!>|kRx@t)EE947k8G;ov8*rUNePPS(m_qfP*e8zFF zpJn_D3}OlJgz#zw@Pckv3b?=lu2;b8O>}AF_t5Ko1YQrFUOv3X4t?GZK=}{g?oOe% z152j}JRa4Uy6RqN*Eer z$nFD>RR)wj>}u$0>RhE+4!poN*EJbv=FhT?uC1=^UXq=5op)U{94m1H#}H#J0b>ZU zC&?%SulLA@WDZ#bxH6yuMBx-)Fih%0&XgqIW2_EHPFn0DXK-+O;BB%ngY%^1~gRwH4hbm z&2ZNQFKql#yaaJ@rUw!72+ZhQgGY`Z zI6U(=D*zvx3_K1Pe3%KF+p+_RF$kt%a1y2{Cy)|eMmV>C#Jzr)@J+5DA12>bLJSfi zK0tWzP-uQ}_}VoEq&vZL9c*=3k7y#p=or^gYfRp>CNhs$FKKsk8C*zf_A!>Ld z3zir)A)eHunoF2Mht*g;ySa?DBK4l;vauDVOSh2Kb%Pet4eMiD$Oww%gzNb&WSHLE zLUyQ%}b=6kVsK%%ksaEALQ}RF;0F zpA6Jz9+yGpz}0eF%jl9=8w@PPtm~-O@_XI5vlN=QkukVa2_4-=23n!o?Uunh8b3;H zBZC6)q_D+H>-@IRyiOZgLF+bhY#B_%FlP-*Y?!JO+@;IvI5%{`i&&&7cY#}eVYSlP zZDp!n-&W4oS?y$y&U4Gsc$gAan9jySE@&s4mCS)cdV2#Ky{0Z~C&!tttfoh_m(#1( zf>i7>JS;XXRss6}5AW-nSXhtMF&$*8>66NOYzO&XG5i#p9rvuJV>-&X%D$v(kc5rn zYalGxcV3ZtKu74iTKZ&1d7>g-X&9Iz)Ng6l_5Mz>e@QG9TNw-Rw05&(xWp4-Gg0?V zmhr`~78SiYs_6O2GPtfUv#&ml8LMT4~8zLB@q7F)t zspXAyBVpwc9S8dL&J4wXSf!xBti);&EQaeIV-U{B$h42Xv*W{m(t1kIh;zo(JzKhnTXo%7miN z^n??f+7pJpuO&z8Y&>vuCLFVPCgP(idVDXaoT<%E88XVRkqgqHY&dAB&Vt(L#=W4xj7&I* z=Q9CWsaW*#OzHOQ8`1-~bO}2MsuZr1d&zLCqt59i<7;4#Vte2cF9u&=nrR>J(xmI1 zy<|iU?7|obylyZH?utwQ>j(!J?h>tk>;Weo+gnCb9AI5^C={WGLp3VK!*&}8>~4(b zAs&ao?o865nKBxCQs{!-vO>AOkcgL3TL`S1FKX$)J~Fl>Zu)Ms&0sxJC-jkV0XT^9 zqewlfj|>jLl5s4BC)j%z?AQya*hN%Slp-M2-YZ~35k2+-*d_|7h$wat5Y$-EvG?9} z9P2ptGBY~%;k%jgywBVI-*^4Z^*c#+_Fj9fd+qF#oCFUZY>;)k_L5RIJ2V1t*WKI8 z*I{|wE7%D|fvVlzef>M!znD4#cGW_Gil4oF8#Wp6!ytb^=*Rl~29E9@UM!&5bD+U^ zp!UY0qb3j8vSxKVU~V*w$UDOZ_a9Wpc$v$(UrDoJEGSs4R1n)cbHDnq(c>q@mHjly z#)5-Dz3fq81N*xyY_t*xtVpB%M)#i-ULe7nb%j8FsAKw%9&BH&Wi=Roa9`LJ9yV@# z-dr?+9q2F|Cgyjry~+dFwa>3#eIh&Pbg%L|?nmd_hrm)%P@U@rc=s*R`aJ~cDN73B zPS&EP+`!NK%WXCtgl4+kVZa*&63jlr%$%o!C7Y>gS@V1HgX(A&&>ETTwAQ9gb1+l2 zKr>HsC|8-LYWz((uB(%xq?XxH4>0n#97u8VsNURY$gQfRu36R?YR)no%rtVr7;n1U zg`4~AI+{{}9p?819Lz@r`kC$p9nHvsWrc=jQo(9QMYgL=s+d;`HaANYYGis8@-sIT znr_xDT+#F`?4a25Xhm~+VS6*Ju(#5Ldri!OMeK7L7SRRMt!PPeV9|PJYSF8vtyuN? z`IYm@cQ@a`e7Ewus7b2)z0NPs-*zxx7PA*BnhwS7O}FC9j9PymuW7z3K2*tXS1ejA>TQ};J67^x%^=d7MVNF|+EFF^|;oGb_|}ES2w1ehm5R zIwZe_vl(8umRX=)!P@zyZluHC>y})*kQRTF-?9_wV*XUOuF%nRsMpTOkNvOs$JeWC zrq-)x+Sky{AL==ndG*Q~f9k~=PNrl1RzhcURQ(D$X8mEJnb)w9>C~ufp+5&7W&bn% zX1_+&%z2H9R>~iA`BeJ*$UlSj&$W59kylQ&#$IB{+TQ_o4mv+X2Urdz9V$80H0L|n zn_Haf*p7Lg_B`)--SejBW6zhKA3VQ!e)B5pRo<(SS8K1%UY=gQUjAM|UZGz7yvBLi zykfmpd9C%@F5q3xySjI4?@;gY-YdK}d!O^Z;8VxP)yK`JtB;4z zV4q<=qkJNK#`#40MElJ3S?IIEC&y=}kL7dR=Yr2ApZluM&puyzYG$}|1#_Wu`C{(A z-F&@#{e1&{`};=tj`y8w<~r9l>iRYGYvI?;ZeQ))>)AvQ+_x%L3pKCd@ZnM(02M!oIVB~;^0h0$z8!&r7 z%z)(s5(gv?*f1b%z_|g}2Rs(dLhw_684XQY(<)FYp>jz~FIydO& z!3_sD8{A@W+reGTgUwnLc{k+aPz*JO78+V%sQu7-X2Irl%WWTaaM;n|6^G9perEX1 z5miR?9a(i`+mVAujyHpw*Dz-^uT!pMSZLV6@IB#&!hZ;V6;UOkW<;}yp%H7wb}>&k zZ(8j1#G4ZzO?*A^-NcWPLS%u+_GWpP-ohz!noI3!6Q-`5dSU9Nskf&-oz`I5rfHee za;EK>_WiV@(~eI&Iqkxoc#){O;zGj`n8%7G=#T zEt*xi7JE1LVeHSbuVUZCeu(`NC&g8bbByaBw>d5|E;sII+zIn`ix$eB#kq?QnH5{s zFg%y|FB!gM(h{5L*|MBDyk*5g*~_0Vf4%(IE+TZriZ7GPoI=NJ$+VsTzX3Smh`Oj{pm;3 z@2CHk{yzQNHf;0WHfr1WZE@S~Y`eehCE$S^V*WjJTJWVmK@%IKbvn{hMKIdgaB z$*hi9-Lm|$dS@kOC1s_ULt2$-K54s|EoU#uUY4Dhy&?Nl_O0xnvY%$Z%zmH!DF-=z zIb(7na*pPl&AFCyYe&%?Wp`BE;jp9j4sWxld)ex{itc)AO|{~zrB!tN}x3Rn6?i#!6?QUV(wXWapr@bzFTkmbZ*KhBjz2SSu@13)E-rg;H)A#P%=e*Bl z-@$z!bCFvpw@7Zu+|s#ab1UZ7&TX7KJ9mEW^4yf%^|_nP*{!Ra8Lcb1yBrEUH0)6L zp@oN*99nj0?cs8VD;{<{y#8?d;hl$nJbeG~Pltaw`u6C%qaTk6$2uPCa;)32?#KL& z1s>~jtpBlyV^fbUJ+|Z6?qkPIhc?wpO*$TZeA)3W$Fq*_IDY8(k>h8NUp#)*3~f`R zz|s@TP9&a4IkDlyHgicEdvkl6vdtaOx}0r$w&U5vvq@*u&Tc!qHlBSKeLudNuUw^s5W6$=3>AYkjTbwZLnk z*ZN%>er@!%@z)ky+i>mVwVT)F>&32@y6$ql$94bff!9N?55FFFea-dF*N z)zib%)3dKa)W@lhOP@}C{QHbEi@LV@ zA7lgKW=KqgdAVc##@Awh!Dgu*JfT3&R&iE}XM4#(dtQQn4|M zr!1bjc-G>ji_OJ<&Cw$|)i8SeIY&owj!x=SuEm;V8TyyWCu!KXu^WvV12QOZ{cJyH?cynRv~&UCY_#=LvaYUimzSygqq(=yC7$^Mrd3?oPZl{9?h21uhyF z<=eg&r5vZ6`Z;xhoLXEe=Tyikms2LESWaOsN{+|{*>AJ&XP*PI&t#wG=aKCF*}JpT zve#xOb1yMFk)N?-diIR$VO)H(yJolNehXW+OLntt=j_JW_Sq$~?Y4i}{`>Y{w%^`< zWBY;a`?qIrkKZ1%ea7|?+k0$pw!Or5HR~al1z8ia#%G0Rg=Ga~_2OlKPnHjm<&ou< z)h5d+tHPg4kt{t+&-|SEKJyWfc|P+Tmou5CGf!k5&peWOATu{JBXb*%*rsF-&m5B3 zFEc39H`6n-MP|{=f*Id{j5`_EGp=S_%s7*AD*q;)C7UvqWK7J6$Qa4(5g9{w#b)%$ z=$#S5y?_k=jIJ5(-0GOoE~9ZqgN*taH8M(V`v`2iy)9*1(zY4f+HSK?f0@%K-7YOD zZ6%ilX|vL%r%kq{1*Z9=bxrG-)-J6@T9Y(v{cdaTtsz@$Zuz?9!xnSPQZ9?P#B5ox zWh$^`%9f}tleUcC61c^Ei}RKyTZ(UfvpIKDIIzi$xNd6xm#dpqJa`ld=7Uu=BF z<;lj!8-L{T!^Z0yuWmfNao5J38`C##*|=fj>W#L<4L3II+E8snl?`Qp4dVJ|>mQ}J zUH@?X-SwB(A6dU?{l@i+^Np_WwZ7?k=k*QOH&|b7z5RM+y|`XTeV+O(^~cm3sh3jE zq@GSak$Q~Fk<$b0(vu@gvetQRgSB>P2ZMn5&*OpjYc&(B0 zH06HE&6H~?S5k6Qtdwmjb5mxdjN>vYWkgEflz^1BDJ_8%r+l|lilr1-^TV3+Yj&(j zT;mC>skf$d^2Ovc$)}S~B!8d0J=wNBc}wz|$y2zDN*?1^SiO7ohSgnHcji{x)vZ@ITHTOa^P}KQq!a+N%dEqS+#%FzEy))2`l%m%vd?ZwvwmS3jX@FqSlHM zi7$Y}t0XZoK5=5=m_(n%7Kx=2eouIx@bl_b3D*;5CrnSUB}64eCbWycl{72-6}vp4Wv?04SZ zRaiQ6X~myj-$$@rT847C%}1)8hM!FD*X4c;8}can9nX#RC>)EJ|Or zanXQ90gF5rbzIbbQQJk$7d2S;Wnu8bz=b^*x-M+Lu<$}D?nd06xW#cXam9IWaUnJ_ zc6n?}?7Y}nv7=)L#rntgiftb26k9L0V$3fwFJf-TY>i2dSs4=>GuIX~JSHfnS4^uI z=a_~ug<=XW_Oi5Xxl4W<*DzcKA3uU z>W@>Crp}%kHnnr~#prXbIzuQ7@vNMm>po z9Q9MwgQ$B^H=`~^osT*jbv)`=)PbmdQ9GjIqn1VmN0p8$9;HssV)|GE4=>FuOflXg#P5&1N7f23_@?<<9M(3hNm%``I%ArQ-ZeUCbi2`xqkbRuVOVy^)sV{}Cqj;e91b}c zGCE{L$k32}A$>x6g}8;Z4rvxrH>5_0%`W(3@Q2`Eg5LzE1kVYc7Cbb#Y;dWdKZ3pl zeGK{~=uFVbpkqM?g7yXN4C)+IH}GO$kHEG8M*`8M*c>|4pVOwX@9H}q`j6XJc* z`>^)`@7>-zy|;RA@lNqx=snVVn0HTaZ|@e~PTr2*syDnodhO!xKhwR!y^4Dk@ciic z-t$+_7rgyB;(5?>kLNDWEYA$jZJz0#D?B@Ux_R2#^Zu!_X9>@up6HR+<6e&+dtB%d z(8I6$x$g1Z7k8i4eP;J*-KTbs?jF^BO!r~kOLs5PUGw+c z@JRD;^r+!c)uVW~lx{1!jp#PC+n{d2-FkM>x+q;l_s{Mh+<$R@o(VIj@v}HQEowQ&E1^c9NlWUIk;7FQ{7}Yp~I&Rzjt`s;aZ2w z9WHb@-{EA310D8tNbj(q!`u#G9YQ+zcj(!{y+h{?Z9BB;;L@Q+hf*DE1zf+l{^I)D z^@ZzA*K4kqUC+3la6RO@(RID+TGu4km9D1iQrAVU^IT`SPIay8THCdztG#PQ*V663 zwtv?Cc>AO6ceKxJ-@bjD_AT2tXt%pvcDuB8o7&B3H>=(Bc9YwMwO!K2wN0%yW!sc& zQ>1ln>piX0T5oN=zO_&54z1g^Mype;j@VjRttPi}ZB?L^-11w?Ut8X2d9~%qmUCOq zYB{~-=$0WZeOp#+S*~U27O^e#7D@}z<%-J@mxC^QUD8}OxvX_DU3^?RxHNaE-Tdd~ zmz#$+yVUG_vop<-nk{d(uvtvA+0BMGE8X-<(>qOXH9gn#MAP)98=J0a+P-O}rp26B zIWKb_?A*uM)x)`qb0_Dv&aIrAI#+epoE|$}cS>_wy-D3*^Jky?U;GfQ8LOJtn^cS)ZgwbH9J_=_Tvnrx+D+~D ze>SY0=fU<4+LC{YwU~cx&CcJ__^Hj#H*D4nu2@{-g^U*ZSC#y=7HVz}3KcccJQSKP zs)1%i$aGP;&BA`763x9Oj`G`oEatD}pa0X|U6v4j-Z85L_AD&Nag+R6G@rhe7v=Dr zL!lES+aG_G3eANUf}hY!2o^$xk-}&pLKr7Z6ebB#!h9h{IA%9cxGy{szVhO~v{+88 zDpnV3ijHC(v7YE6wiG*yT|{rOr|2gR6bFkN#m(Yj@u+xGJS|=luZY*g8{$pzwqz$2 zlnP1ql7m!Lsv*^qdP%`HDM4BxU6HOyKS+0_d(s2xo%BI&F1L`|%N=De*<0=@_mTtU z5V?;WE{~PR%M0bj@=|%Z94{xxE9JFvs=P%`lfNlY)+-y;3Th>FnmS#bss5n}S}Cop zRzYi|IcqJnHd+_WLyOVkv?ZFUt<+X)d$fIa1v2fj>=ZlIPP5x?e(vvR6RC%oL5`FA zef)&soKK{Pw6_m zE2-O#k3p$UJ=(6{jC|l@*4IEoULrKMHRcDUYC_{qyu58%jC{}MwlvnfAQ=m|urrq> zKnq6G;sVez7HE|Kv>pMp*$=e+31}Auv}a@;=%CwBpd%gY$djZK>vv}4?%RPbyz}hZ ziF^jS9RfUfJ#Opa19upcI0eyL!)UQ9#pI6cY==cEI8@LG=G>34E4d(H|{RrC){>=OjnGFo> zKq%QT4#r_i2|XXqPKRFuMoc5zAL&gP=_ng8x-Bq9qk-w& z$u?j{7qSDGNi}AksYqnbO#+ zKwL7gkmF&IZ7Z<2IbnX5bS0E^XHebYf;GbfSFg9sJcQkhVdttAN4vh_TW z#vsx-Xw$2clf3CKO^)*CfTqlU1~)Pr5O$iyNU~OtJYc&!xyRo?I9ReR!YFd)06S=G z2gl0}O1je^bbjXnU>6+%+~}{~+K% zaq=_peJ4T>4+awMA6f_;X6}!aBaHOuM8b}bwIV!zoa5%ibg~&ZsS)P;ltdya}4IZLiPa{DiGUj;GzpT0$k$3OD}-S+`dA&t~Mf9fNMMpu2b3@LBP#Y zglgR40KHv?aQlbBz@0*59q?mczEeVH@1_IyIQs9~0}rUgLz&R_qZHt$2=WGaOebtV zF9n{cWE1c-1bEg0c-{_pLAhT12)tw@uUP(?lD|F=yg3fM^#gus2K?F__^m3r3B2RL zeD4qZ-j@6Ue4xW0DgP&tYy>_t4PSZIeC6o>#=-cS8-z-=NG^oR76f}QLgNnpXd*`w-!Wmgr+tVLNh-I%{B58LW=?rS_%+a(O~O#5ZXAAR}k8shtQ4=w4VaOwKs$g zbs)GEA zr3oV)R+l`7Fnknw0b#@n2qU{d80AcMKp4$H#xMnAc0&joOz2R!!2E{?a^n_+hzStJ zGV*bhd)zSyUJ``)9yWd)gs`9i;h>4(!IYCg8Tl2$>SPGX?Fh%g8hV;SgX>BXI+z*-VSN<{8x-;o!p2Y{HuHax6)rU=91xdl5Dt(ljOfZE z2v?b&YaDFXu0XiH6vB-R2)CL*xZMK656vLlVU%~iLHLnv?#4j4$DH2Jgz$h$JfupG zzCw6h7sAi8Aw0PY;aPWb1j2K+f6jnjj9>>Z8xltIik`h@<=2$^%^(PG%aHpJeqn^a z4u$YrEwUKGJEq{>MhNeFLil|Kgby6uAKyUubPB@fNf5qJ%`c}Re5F(0DElAm^bd}q zyp0esl&`{dg($i}lG>Aa?Ih-az#1 z3DGMJqEA7HJ)1)Gr2~E=A^Hy^jJ}r-!~iN1$bf@9AqLN~K@2$ov3D(qeR!~M6vTcF z2|Mi1Gz?&r0~z_C?;s8y2XRP0h(q^498TjS?m!&*GsIDJaP&=xW4PvwCPs9GICee6 zadc?>Ylst`L!5XIVx$RiQWuDm8$*mLKwdzcVp{<*nw>{e(y2EfPNSr@jS#2Pks0X_ zXHtRLbs)|OfjCzoe?XjH0pfx-5Mx-ziAs#4;|m``TpSH?$vcQk>CAUmATBEmad`)b z@sSV{i$YvMr&e+7tY)g$u>IOT>?@Vtu6KsGfe~$_XPann^H7LemO|W`1u>0=(hot* zC3%!7EK zKKTXW_un)B2YWy~DDnZ!M>C+I;+Ce3IW)FM=VyY!C4jrF-o{octZTz7vi_>5dYxmoL7q2zCsds zr!D*rNqh)N+6_r@gv8rxNqY@Re+G%S$x?yZkP5m(D%1y3;r)<`R)JKkJ*48JA(fON zmD&lZOe&;uBBb*DAXSKiRPhp|%0(gB6NgHWs(gl2?LMR$+aT3+CD$O?YAu1}I1*BA zHmc(WsqQF9_0~eF{~S`o){q*tgw%L7q$VFAIeS8CIv!H9E0A2cZdn;ptAUVOv*R{& zy3JllZ4W_ecNLOrNk|=hA-VO2)G-87r(uvfCqr`2fz)LWq^<#wx@oXU-M&HUUYM|P zkG_yRiPtVj-cKR<(5Mfc^WlC^JF*UvZzu8|lHUbL{tF=Wq6)o^Lkb9o6etlo5Ht-^ zus5U-dqQPHZ$s*R3sRp1NPUAK^{Yl0M1PwN(f~>|a5ofq$8vvM6G-Ekg7G&YO$dcFk!>TRAx&xl zX>xr?QOzMu=?W>@4boJqGmSTwl5GN{=^G)yo{nB<3+ZBY zNSD~jWgkdannJo-5z@72NY}SRxEsah?s*-9Sk9*!hEQ zkREP;^yoK8j~hbzxh140y&yg93+Wjpf6mkC1qbD;s*qkUg7oGjq_>A5{jwj@uQwpQ zlOerl=f5+h9}YqK$cR68hV&%}($`v$zVY}Ub0DJ}WWfQl*blN~+XPwO3R$rrs~M2B zjga-LknMUyE+{}Q^gZOl*C7}A1i4sW$i)vrE;$Tx=@R5H@jUZP|gIv8KdKJozJpxvCFJ@yAvYim zk3w#g4Y~0K$W6vTc8Y`SydQG2GGsU8=CdHX#6WIQk}=o{$b(oom>mwGcSC7xSOnzZOwWjAkVj61JZb^t(MutR z(NH+sM3|7rs)WjoW2EDsK%PK5k#u5GAmqtWkfZ3t6c5PJ#UW2^0C`$XN*)V&`mc~@ zvfeCP2;|upAkS?Id7eQi;{tXN;|4i)4dghcVo?{!iy6_Ti&fhZ8!rW(nk!?vU5IKwigjklF_FdX{foOJ_FSg}kK# z6p` zhEibyluE6jRNf86Ap%O(hES?4gTkw9rREqYwT?rnT^vdsTURJ`$3dz03QGNpP#PSA z(ufTk2SRCb1&R|ZIzNZf%mYgEXHZ%+g~Cf`rPXIBZCXHSy9)~c&PH+NF}FidI?;hn zze4GJ1B&|rC|&44*IX#w214EL@X4ZJ<6C7P{Ie36Hp=o2!H=o#+8LKz6+EIlxHFjOqvZPiqcHk1SPr| zc?o5z1!dYSddsICwhK_EXG57W70S$>gsGa91!eXcD0AYV%$)~i-eo8Y9H7Kd>e%^E z;;76bwq0}&%98R>mij~C6|%Cdw++hjlTZ@cK}k%7vVzjCtOaFN5|q{D$WfIr<%xqun2%|k!h^&CZ9~zZ2bmq(_C}(Nw9M>0`5ITF2?Jli>a(O$HE1k$=C|9Z2H9Bs) zJ{iglZro(fZk2;_yEc>`80{S=C_frd?rwu}pN$?cua8PV`H7M~J`d&RRZyOAWIv6D z@{GznXT&dP=j9A2uPFIzs`h3zl(!3^{4y8HudMTq?cYg7y#jtNshd+fnf{~1T40RME9L=&Z>?phd)Cgy&W7j|(_ZjN=6Hq76 z`H8!sMv_T%FscsJDczt((}}4UU{j|t8rxB*(<$%FYEWlwf;xxN%w;a;cZRxvUdPg* zxb09Evcttype~^!OIJhvZZlMq^_D$^y8I`o36wsu4b&CP?aH-KlO(9CscLc<)HSo9 zrksMh_8rvJT2R+B?He{T|C^YC&FN6LkgfZmrsYB1rb5l&pvr6wHH!mr`$?!d+}|+? z>P|*$ErYs;PVOrYHJ6SZ2!ML92Gm1;Kt00fjxvbjOz8>QKG_`VsqRoukAiw;9MrQ@ zp`MF@dj1&H3pN_NNI5UF(q%?-C6v&Ct7oBJON4r(DAb!vpx$%qk=0bf$^?qWyex~s!RN^Vuln2J}1?>I?3_tOxa#Kh)Pc`2p&ip;Z1Y zrFgp+>MyZSe_aaoHx9mc{h_|6x4(1y!xN~VoS=SYBwy&{*SS!?v2k8`Xcz!ZcnnR7 zfTlEsrtXBMeTHV#f>r>~3jPeOa1yj4o1qmw0j>B1XeFYcl^hPOR3B)i17Oq2&_KE6 z&?-1Vs~8Qf(rIXwuRybZ4y}qCw5m;@RT~Pe`UYquQcT?1M@ zFY**xeeT!40j)s*c>%3qEVRahp*c-~=G+Nd)9TQgwSv}sE;N@oXe~C`ptZaOt+g|> zHY=dDD+R6nE@-aZp>>eSWoT~nyyIPHoo++x{2p4DiqN{oL+i#4J!U}b(E*w#9rQX6 z&4=}SQVrkY(EQkm|L@QOSf9VJYeC~lF0|m1B#c~w7GiVZ$8~6-0|_JVU765W?<3Iq z)F$+@ZvoN|TE9XhgzSUXzdpGMZ2&tPz;**m5Y`>U_Jd-e4W=rC^VdH@8{$A1=#V^U zL%Wc*3|csy3->46phc7>OjQIsAImW^b|*A`&1>W6&^RhMo++6?6(*!J{}U+DL{^Fv zNg!d)A}Q@8N;GLBp`??Gl78eYv?x0gO7=sWQjm-ySD{6BBpJ}AQtqjn38S6nO-?e( zYJ}s)b{g9BT7*GQ{{n5sA<8nd3}GH;{sC>)c=8Lh*_3zodT4y6tIcUnsKy-TYi>=# z4mmk#^C16z5atK-i(~`hb zE8!WmL`Il6k=%r~f@jqVre_5mUCDvEl0mIvz^i5xDsM~TMiM(%&3vq8UXpn*c@jBH zC?Vep*48kmYgjoYgsdeWpsl4M>nQ)a?j)JKhL$>vq!BvF-xRg=BMIf-(2g8~wy^*i zM_9IrDcLli`QOZZZl(&GE%F)KmTrVbws3nZ%eS6_mR5t9gbt<8BWIv(t3v4Lwg=EM z7=6Y}@*G+wbDkMTn4+wDgpOu$Aa3^~yP;*5CQN(w3urmK{?~FILEAy;cHD)wlZ|(t zgSKllG|M1V#A4oew;}tW?P*L-L)**s-VM<9G2(qUpyjqCnb7t#s{M5SKo61w?R%!; z`y^-wE0FQzH)w|<$O&kN>k``{Xh&qS4BAmn8b_I*W1R_|IUY^e-~^pKIR@G(g)E15 zdJy>n?aWxh^;s%&t{6#xcAf#8KMw5z9k{^!Ut9t0QaE9dml@P$+Pks<+EoX#m-)Ze z3)*!yzRn!ppfooYLc2*vZhjB#R!2f3x9P|a!wEaP!_IzWy&su^yHw@w0ciK!2-9?* zYCNEm53->>90KhT?L4{y?Wdv89#;vtU2e|<4bs)4iZ1{#_;LRs!ZyC|sU!ncNv**{M#6~_q`)vTZ2JIc& zy}JbMJqPi7u74j(9zy%Tw0^h+?c+u^_{3A~(^6=k3lVnsg?aiak_hqw+BdHMpp$=a zjQsHmS{{w$y@igYUze)YeFeQ94b`tqc0h07 z3B4hmZ1^4YMiZeoZc5m`3Add}5eDVl2YS<5gtnXYBp+Xp;s-JKyTHX z(EHZxod0Eo-ewAU4ZZDf=&ZFjeO2baZyRoekp0m6Nrch$qtX6OWIFT#I-$%1#*<9w1FI2A zIglfMAX79*BQ;4EGK$P0YoHGRqzJO82G$0?K$KHb;cLDms#n2bg-Xf-R3EM1Xhu@_@=kJEP z`78A01);~sLr>s%vn6t1t)N6JmqTB*3wjbeUriN~NlIn%4*I(0&{I1@U(Y%lDCtHT z-W&mas{}pm8}x0AK7;wpWOP~4(6c#+b4Eem5deKB^S^5wbZaB@-IRS#4Zdv5LBEfR z=MG~2_pgP1fCj#2g@do4AEx(5W$4Fzp&y?J{lpsRr&Q>t+3*aVJIDGLc>Kb1=$9Oz zU!iJOX!wc;$$@^Aid=Ojo+Oe`k!wXrH^TjEN1$J4VAtcwdmHo{oVjk?g?`hHG$Y<* z1bGSl7Nxz#NN=qpSD^FP5&brEcbjtlupRmxs`TSP@)-JE+PO;y?=gt`Z=pZ<3jL8z z-avm$2Y#knPuTtm1AW>X`m?sspWFKJ;~exCVbEVTg8qsfzM{;p8xk6M^9cIeXV8DG zN@(~u8hb})e>VtEs}EG?Lmu>x%<(6x@@WqA&#ef@$(L!+zmA3e%@z6|%y-@a81RE3 zOo1WZh9SGbu*uJ0D2HLF8(?TrF!Z@FjCC;VDv>KN3ao-r@G^|TE-;F0fl<_iQ7j2Y z@!c>=?1WKrDvVOSV3ckMqf9LrW$Tj%Fv_uAc_(rZMuqJ#DsF{Q$sa~#{zpRg0t|<8 zFsclI&8XTJMzs>;5)9ts88xPn6EJF)BK^q=7`4&}k2?-0-(b}CA!}gNX-A&IsLTDj z`(V`TPM*N1zY#`*WEc&H!Duvs&_-jHHK|Kp!Eo{-TstqZ!Dz|~O`pSPmI|YJQ<4Y6 zWi^Zz5#%n6ma}2BV#C&Ugi*B_KxnY-BpB^llAAEvXToqTL^i|dK*u{I!f<07x5qF# z)+1D|Q*RiZ8FlA$8155cbm>BDdtr3tfo>9ElS8ba>F@SZ^4 z!0?#`qi02u1;dvL__D5FAMz50|93EYIg$%70veNTFap~U)(IL5gHK3|;0zcc1qp+& zg;Mg~RR}%p!(8=EfzfXeVSf8Ph0))igc0VvKcyW|h%nE**)#@qsH<{!AG(7j1iKGScPrE z5!HGFP_`QNkjC0s`$DAwEiG+@6sTK0 z*GV~|bz-Ar$=16`Cz|*Q~j@(lf#GIVjcFte}%pkLuRt*V0(Un)5+& zwYOH6kynY<*h+GxM%Is>@=wLB?gQlCt6SaY%cC{x-cosrVeMWm-%_o}P4c=z*4sVu z;VQfC%0nAkRf{Tqy4AJ3a-ytN#aRihVRZ>sHVD?LP^GkCg$!461Z%-)#nZvsw@BIT zX5GG_bWyC$_ms_*ttP5Ex{xK7Rx6jY`Zra#*;y|-sKEuSmqDt%Vg>h8+3l>6>KxG; z9Hwq7Yh}l(zlhfK#p<8})~yt^Yza%=sd|*K?wnF@h*p>LYDx)f=wo$cGb^Z)_N!{8 z)zH>CS?4POJsE__ev|I=1`wCh&$Li19TX8$| znq{qq=XDR}|C)YAPN@BZzS1gtM_;X2Ne}fCLc-mj^a<9*$9gRxA?%4h-WvT>Kd4y; z-|C)%mGP^-OGwChr}wuOzt>I4n)ykeCRp9R=zZm#vawZ87_Au35`I^WLsnJYa8s5v|%6jUIZ!mz%~E>&tE92X)h#pN$UI#oBgN?5t5u z?Z&E>(8lhbWX*B4o1t3uy4r1#tq?D}t*Z4Qz^pZ^J2cbYz!t;6sD%eV*G#^xyM@2ZGDyq{&ZQ~ca>UJIN_-ZPvD|zZ41-Vr~kd5cY z3*)VwWGCBovfFBxX1C3buQBr8QU0?=85BZcltLN47f=C}PzBXc1GP{Gb&X$*_r?di zns%M-((Us9aA&*S4mwPyt=aVpR1$36;l({4E;4+%DCmX0biY4;nJvl}s|TYvhGHa2 zVl>9n{zS~7{rQMNW5i)GoOutsl!2N^V6ZE&5^a&hr9G0lxMC~P&;k4(F3=H~Tsk2e zhtU~FaRmXmX2Wfozr$rT?&3bi;31a?JmE4H&+r`M@Di^v0l(lqCgCGKVJg1x;i`>N z0y70kkT6S71OS$=^0W9b2{JtU8aibDJ>Orr8` zvfK0Q<0$*!^RN6Pwm%R3NAXbrjXY%93$f;3 zwO_LKL)LuBnl};5l!cJ|7K>Q6gvY-7SNAvRSiW7`KZ3--@gKt+SA|$P|8NAy;aC!b z%6}g{|7W-IKTiIw7@z#G>Z#f4sIr-a9$Gi_$R`fj4;2GoBx=^bned}Gs(ZFCO`7s4CV$cKgiq2 zwCrVCHqyD94CE$k1^-$v^S%w=Bj}lJS&e=as2W<^R-5 z{5$TKkg$LqCor}1(@4cq&U5g?#AVbC?WH`o3rN}&0WR9Z9 zkRu(_HqZ044M$XxN|z>d5OSh{V=4|Rbtq$Xqfn{y|2}KK!{NI9y8nAW=kuI*ziY4c ztY^c&fxVw`dIQBUd=3Zr zw#)g*1?Vd$He)KXT@u+-INuB4!e8Qf7H0rjljFo>IN3`&${%1MOv67bP9%YP`YHZ>GIG1cWL4#@S@MeS{uAJwih;dmI1dzAO zh#F)T43fAOa#>h+)(1G|q8E8hFJb>rps5ESsz?|_YYmn1jMLZAXdzbOfswe+p^oc(OeII4Z6ts=gm-|@*L4Dr=mNV>vLLS&`gLM_7UFRpxEpdg;o7sX znyk2cc>_b&0p0^_2C^-HbYS=p@Cjfa;4{DhxSLFzI|w+0?>VEw34A{ZQJ?aGsT30~1IdGV%PUt|lTy@J?!19B#GBoKaE_QrG9eNl=>}*%3OE6~Liy z4*0Any4B7~oF$a2ExN4B*nI-)JBi+>EKkDKyUr>sLPvS99P1`BNKBRt((ta=lG_|r0EPzD&3;p{|11|W2k6h=Zu z<&+s|2)X4;=+9912~>{{AO{kVxRX64`$g=~Q6R{Br_p|PZ=7?{?xH8o>$h+eeh>6=Wlj^=ZaX#Y=)ZQ;Ys&rdoE98UnDbfDDp+*tH{@ptjMv* zcaanNU7e;kto`{;bG_^9tvcOt^ftZCVfrJD=k@ic8hWgcVQZEm`n102+@k-~*-mBS z8s=0n!U(6DQARn{O~mAOlFT(Gzf;@XY>GPdOmS1ux!2rb8aXXZGtZ@TGG+7 zI^L3rmNc|1M+>g+Ms_&m(2|9gGy0fQMStrQ(BA=$I|V>&0Wb^+EHI^`S1Ni9$2dHK zj~2M6oK3l+1Z1L9x`c8IoFVcE0Wp8IhR?r@JmSUW3Tib3 zeel>NM#j{7wMG`EPiCLT8RE8|SPRh^YGo>>Aw5k{P8=Yjhyw{SX)VM7kQpT`!!ng5 z9Fj*YCJvMCBsff@Wg1N!Mg?dG6Y`MRBy0MXZl@vy)ICHe@y+T1R&ge}3M3&>flNTZ zRMXLFQpj*3J4Q^%2jXD3wCW%#3F!%@2|LGF6e<}_d_Foc>Lg4-$im5YM2(T8OiE^3 z<9Kqmq$4{>SjPlY62v88F?BF`T^!%-<6k37ahhY$HAdfZbj>oQq48n&%&rwEJV+HqAKFKGth74%Cq>6xaTev7sgUC|dBB+Z%>qg~OKVOyU8C!XAs{IdL$jhi zNc%&QXng1i8bwLUxnf#SqMuJ_W|?A9;+qQ& zAOVVsIJ5xpnbNZzh^4U;F@KeC7&J{7Xs+yor}Tj?;Vp3~2W!iUi!S^|qvEQI^9d6r zkL%+SAvz@dS#`JuP-<#{Beku)hoq2)Cxsz4Cy1lK5w|@N{yv1?MQWX7*0idcj z`_WF2(!dnECYJIb!9jYnc6kHyH23p4`H&zL#h>d67s(TH8N82*eC8BLJrlu|)5s5z zADtT`=OX8vLXlrJa|-KRI+s&gYkiYbMioi|)a30ZH^rz0FdY9hgJgz_2pF2JELH&*M zq&})oIL{zEI_*5Cf6zZVDf+BF>kQGq=--^72HCzd$}l6H(Z)lLG#)upUT3N)V6Jy& zn43%?XEySs63$#x(v)@v)J5<7nq+nx0~CYCCH%sJDjDas;Tb0U}~5e&N5TW z)N)=ncba>h<)*%A=&Ur2kYK%u1goX9#qAI`5lqrkk_H zJdTv>1EgGioo%MSdB)jcl1;LcfxIil*@diesI%9MFe9CPW|Eogd~W8Nxp^JzYC>6Q zHg=`JO`VCs(k5nIcU?)>(zW%y`aQi<@6!i#rbax|r}P>9z5b{ENuSd{>ultT%H%QE znf&G!Q^u4xl}r^=$2@3Sm{z8P>0};57TC`WFuoaOCYUMFnNHZ5?h7_FHTBBr%DTGV zq4(;~^q(f5sc34O=H^K=&^#OMebM&5EhyE@lq#Y#^nO#$JYw3KcJ@53;^C9&OZtzX zdoxoZZ{&MjNLMu#Ol6Z~YMQ!hmo_s$y1qdU&hUp<=ItMDn&?;2hCjUmEtIDV2QSRy zUJo7J6+6-mar7d!X}oG5J!%v`!khBZwk9~DTVz7xHU8*Pv)GZZo5v0{wiSq;#-;$# z!L8y)c>h1z)+T<0*XE;b5927D1h3IYkMMGS^r&6@2ruVH+pseNj&PSrXlyAFZR{97 z!rS{m%}^m)e(0C+2Y%?6v63Qa=g!@@2i6pK*IGLdp@M0P8!{c7PPixbI_`zN>AdZ{ z;H6(JLT5s=;hU2C14Hy{^O9Wmrd|pTb#27m4KMT^V!!jO>qmPNkTB z50%qV)KA}_e)<*_)Nxc(M^ICJ36Jy{YN_>lgWjm$hv!&>zfF2G+{On~Sm_T@SZzmH z^|Ag0Wz`OtOa`3EZoLO;y$_20x!zAE>RNOfi&kC1AKEO(|0v=35qqb*s4zW?TX8sUnQI zG8|MDc&BQnI*h3X%&8X4vyQ0?|8$qR+tf4nn0w8A@Kg7j2TTLg5T@VAG&W64QUEJlkzJA9k-bn1yaSIv2dYNOMmM1uZWrN2Wz=0a zLuZOR<(+F$z}LZ*i6+hqXj_K6wF7Xs)^qkneu|uR_T%>SFAgrP;VRWZ>?x_7L%0QA z$~lZ%)@7V8bU9tl`O?Qd@duo*bVJ?A`NnGSMcmC^>HLN}+Hc}!a7@pk`)&%pxjMXh z5ZXghHg;W#Tf#L6j_%pqt_jb4R*k-0|*2cbYrLUF9qUB?Qt8(AS%l9gt+vhu7V ztL(EXtQxDqYO%VkK5M`lvu3O%Yt7oS_N*i8&U&z?SZ~&sJX*e14xZN=^=+u6r#2g_i)*@2&$ ze#L!t&J8cX3v>TwUV@k6Wq3J$8^4`b=2dxhUX$16_wf7p1H2(`!kh7SyaVsdyYlY5 zC-1}i^8q}C58=c4C_a`?=2Q6$KAX?s^Z0z8%9rqEd^vxOzrk1Wb$k>5fN$d;^Id!o z|BN5thxnH~i+{^c^6&YN{3m|i=fCqmae;^lA(V)SYeZggow#1yC<=+9qPVz4loCmz zrl=$C6OBYm(MCKX9u*x$XVF#k5&gvgF-Qy%!^J2uQA`oj#4NEuEEY?|i(;8rAy(n( znROyfY!sWtR`IdO5PQVuB2yd^Uy38*8*xnd$Hi&!llWPj7r%>a$z?8?SLT<6WKmg4 zmXYOUWm#3$khSGqvOXSNY9w39hh$s%sO%s+$;V_5`ILNG_LIr-S(ze-$dPiqoGxd{ z1u|8>Am5OyZ|&z0cxNc zq*BxnHB9+xq#CWpsflW`nyRL&*=nAeuNJ8^wNY(WAF4fSzsgjH)R*cTa^1~Hb!)kIx_7(xxlP^H(am?ey4~Ez-JW(6UfP0pushrx<&L%6@J@H< zy3e~y-Iv@~-IeZ}?izQ4`#$ibyF1*y?&t0S_n`Zw`!zP|{nkC@{@|W-FJQx7#&WZK zEWh2jw=nZ*=U&>kw-UR9Rb|!f{=Ijy`&mQQgf+K&__kvmSSLJ&*OT>PPqTh(5F5;f zvTQH7 z{lLz#i|jX_GcK?Za6w*#7maQNd@C={EAl&dHJ-$4@jCooUY|GM5AvqGIe(OQXAPdW4va~EKE66)! zHCa>Ek$21c<%6=ZY%L#_?PPn|QFfNyWKY>k_L2SNKsiVbmP6$zIYG{lv*kj$SiUI# zA>Wj5%Qf;nxk0`!H_H!Xy4)r|lK!W1r`#p?$bIs2nJLf8U*rY(yZloL6;apO4U|i$ zQmTx)RaH{eRV{U|x?eR=530thscNa(sSc`>dP4P7PpRIjkLstMQOW9A^_&{4hDNtu z9;3#qNotCkre>%)Do_hls#>q!S6kFJwbxe%)IoJveWkM0F?C%1tS+eE?QYG&yT-dN zx@U7S?-s9&cdJ*y?%rI(?%#ZmcfZ}kxw+TMYvXnC9`m|;{k&n`NN6u z200_6emsnf`U#4Rx`$7pQTGf3qkb}HXk=CNQ~w{4F-Kg~z5ma+=wA^S_0tp>bzg+W zcx2T5!pNxmQ)JZ7{B>;9{~8@<@Hu*57#;Pqmq$lG79RDWs|U#cJU;4_tHnn>m?ESe zk|RRCj0ovp79#bqs|LxxjFFM8e;FgY{<|otJIho(!j~^Z(kW2tkzt_Jqb?7WdNc(} zJth_>BikubMn2MGuZ)&@+|{CG5g-bmt7B2NREcWo}=gLd0rtM==pkqUKrWpRnm*RB%P`kd-c5f`gy%XFQsT1 z`B1-*h?x4tFk=p6S|9b(Gw&ah6%>QY`^rInj?B58Q z+Y@2)e-t&lNdMml&c$*G0_Tc<8#=ERIe(?tiKm<0zZ*aO|2BaBm+{kY?1cgJA@9-u zEQlh2_R@W>jGgQj_ZqsU>Nap&xSic6-M(%zD#)R(kNR;kYR6R6j?3IP+_&6!-1WMv z?{0B-x%=F2P%i%J{_g&POL3ePU^lQLtSn1H{Hu$4@KM&4^=AXwC^nu=LiM+py@2ZP zHTD*Jm#t&#+57BM_8I$w$T!>)gui$I>`+N&eM~$|hALL)~ulX^4oS)|B_%Hk-{|!6S8Iea66h%ZCQCU zs*=>5s+nq~+Ss~jteT)^s<~>R@)xTOYLiM=+x1lV`J?Kr`bAw-msGaLJn3EQ<@c`l zZt{wF#l4bVS?@OQcJB_ax>wt~%e&Wmz zy$WZ(+uP@DWCQ5>x~h}7AmQRk7UvrG0k^sDc0zU2$Ns#?5Y#qf@$({c-9@&lp_=CH zXibxWisoz7F&Ew6?9YxcTfr1Y@j@lbohVe=u`Vc1lG#X<-{!f90?T>YxqW6KQYbAdiE5%AYJ>);62hM<=^`E%J;akD#a0L7;p(P~+4#Yd zMPj*+(%?<;Hj0AKyJd-!r~xk6Uoa8T{J#V;|NCVV*-W-T&fi&fLAD>}`opm2*C;s# znf_Fp>(8^(RAh|E7ZczTssswWT3druO zAiu9`v-;LHqo=%`GWJ-$z7_fUF1VXx>ZiYzrqhqCB$D)py|zfrCm|`PlzfTzg15|D z?yc~VnePfSa{D$%msl$9Fzgy|)Vh+|)|n(+N$5z(jl2Rk0w-cU$T9Z}ya&fcrqFkU ze&aFtjnHe%V)NiRp0}PO;WvD89ADwW&?T0g!*$f)t>HS_=5QUu_((n`>Nr;5`p#Ou z{wiMM4EYTBi<_;lxLe#S?iWqr9YW8L3XibL`h-t$@rAs>ufAk4R{&2?-Ck;`FB_P7 z@CI$IKOk=~AnFaqMZLjXIp2DNmGA-Q%{H8X%4%eZms?$YxUOVV-_!VCT!k0YHhw2 zS$)Ff{iwy$RgC4Zce;W>w*IYq+P#GIi;^$0@`Qzl_MP+6!<82P!xc83#9Liq;;}0R z)3`s&ULsh-)2?dY^z%bHYSnjHt8NTC9xcd*7ul+wjQ10i@78QlwudDoZF(N z8LyLr;eAXgrauUl^)V&a?&@RS5d~{-+!qD(0h9sU1xN~(4lsp-O#@81;Ku=`a!@ka z)D5~NVj$b^eG|;q&RKSpufZKx=15F)S4u@rfO9M@hpvSZ5GoAju z{j5n2$`3OAf+d4Y{owc@^G@*6b2zd6Inz6+kz$Gj?NiK+ZpC0kiYXaM0uG|4WUwy9 z)Cex5fQU+iO`BlyU^61Pb_lxH7-EW-Xp24_0FMIbI488B_F&2o^GNU``mW7C)Z}u5 zvct@cLE~Xq%J^X>Gw3lKm{*N7d4oa2@o(R7Q$*g1fwu;^d{Z*0~HKwKj1 zB_bg1%GH zZP8S7DR^lbXgM&=Gz>~khopu~H}!7514F9bz_YXwLKbO9^`bOv+`I?OVS^NhmbgMj-0-Ga?{ zIwj~l4{Pg7Xh@ZY2gPTb@}e|$ZT+7)ok$b+W9!DF$Yukng#h? zmyyo~aoewrz8OGk*Yv*eN>C0b9+2a5;H{ z^#SB{G%)oG5OqTzrtsQ93r$ZJ98EQq zOH{*2((qn@ihvdXYK!SmwV>W&h^X6QFupUH%k0IbK%P?39;6=KgAI#e2Xt6%t$5zl GW&aOQM}+$T diff --git a/src/app.cpp b/src/app.cpp index 4e87716..b1ea337 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -11,6 +11,7 @@ #include "rpc/rpc_worker.h" #include "rpc/connection.h" #include "config/settings.h" +#include "data/seed_migration_resume.h" #include "wallet/lite_wallet_controller.h" #include "wallet/lite_wallet_server_selection_adapter.h" #include "wallet/lite_rollout_policy.h" @@ -2299,6 +2300,8 @@ void App::renderNodeStatusBanner() in.connection_status = connection_status_; in.daemon_last_error = daemon_controller_ ? daemon_controller_->lastError() : std::string(); in.lite_open_error = lite_open_error_; + in.tip_known = state_.sync.tip_known; + in.blocks = state_.sync.blocks; const ui::NodeBannerState banner = ui::evaluateNodeStatusBanner(in); if (!banner.show) return; @@ -2331,6 +2334,8 @@ void App::renderNodeStatusBanner() title = TR("node_banner_crashed_title"); icon = ICON_MD_ERROR; break; case ui::NodeBannerReason::LiteOpenFailed: title = TR("node_banner_lite_open_failed"); icon = ICON_MD_ERROR; break; + case ui::NodeBannerReason::NoPeers: + title = TR("node_banner_no_peers_title"); icon = ICON_MD_CLOUD_OFF; break; case ui::NodeBannerReason::FullNodeOffline: default: title = TR("node_banner_offline_title"); icon = ICON_MD_CLOUD_OFF; break; @@ -2817,7 +2822,8 @@ void App::renderStatusBar() ImGui::TextDisabled("%s", statusShown.c_str()); } occupiedX = statusX; - } else if (!daemon_status_.empty() && daemon_status_.find("Error") != std::string::npos) { + } else if (const std::string ds = getDaemonStatus(); + !ds.empty() && ds.find("Error") != std::string::npos) { const char* errText = TR("sb_daemon_not_found"); float statusW = ImGui::CalcTextSize(errText).x; float statusX = versionX - statusW - gap; @@ -4314,8 +4320,33 @@ void App::renderSeedMigrationDialog() break; } if (seed_migration_balance_ <= DRAGONX_DEFAULT_FEE) { - // Nothing to sweep — adopt the fresh seed wallet directly. Its wallet.dat replaces the - // current (empty) one; adopt still moves the legacy wallet aside to a timestamped backup. + const bool sweepSubmitted = settings_ && settings_->getSeedMigrationSweepSubmitted(); + if (!dragonx::sweepGateMayAdoptEmptyWallet(sweepSubmitted)) { + // A sweep was already broadcast but its txid was lost (its opid didn't survive a + // daemon/app restart). A ~0 balance is AMBIGUOUS — the sweep may still be unconfirmed + // in the mempool and could revert on a drop/reorg — so we must NOT adopt (swap + // wallet.dat) here. Try to locate the sweep tx to resume the confirmation gate (which + // requires >=1 conf); until then, wait/refresh. If the sweep was dropped the balance + // simply returns and the normal Sweep UI reappears — the user is never trapped. + ImGui::TextWrapped("%s", TR("mig_sweep_inflight")); + ImGui::Spacing(); + ImGui::TextColored(kMedium, "%s%s", TR("mig_checking_sweep"), ui::material::LoadingDots()); + ImGui::Spacing(); + if (ui::material::TactileButton(TR("mig_check_sweep"), ImVec2(190 * dp, 0))) + locateSweepTxid(); + ImGui::SameLine(); + if (ui::material::TactileButton(TR("refresh"), ImVec2(110 * dp, 0))) { + refreshSeedMigrationBalance(); + locateSweepTxid(); + } + ImGui::SameLine(); + if (ui::material::TactileButton(TR("mig_later"), ImVec2(100 * dp, 0))) + close(); + break; + } + // Nothing to sweep and no sweep was ever submitted (a genuinely empty wallet) — adopt the + // fresh seed wallet directly. Its wallet.dat replaces the current (empty) one; adopt still + // moves the legacy wallet aside to a timestamped backup. ImGui::TextWrapped("%s", TR("mig_no_funds")); ImGui::Spacing(); ImGui::TextWrapped("%s%s", TR("mig_to"), seed_migration_dest_.c_str()); @@ -5166,6 +5197,18 @@ void App::setCurrentTab(int tab) { setCurrentPage(kTabMap[tab]); } +std::string App::getDaemonStatus() const +{ + std::lock_guard lk(daemon_status_mutex_); + return daemon_status_; // a copy — safe to use after the lock is released +} + +void App::setDaemonStatus(std::string s) +{ + std::lock_guard lk(daemon_status_mutex_); + daemon_status_ = std::move(s); +} + bool App::startEmbeddedDaemon() { if (!supportsEmbeddedDaemon()) { @@ -5185,16 +5228,16 @@ bool App::startEmbeddedDaemon() // Try to extract embedded resources if available if (resources::hasEmbeddedResources()) { DEBUG_LOGF("Extracting embedded Sapling params...\n"); - daemon_status_ = TR("sb_extracting_sapling"); + setDaemonStatus(TR("sb_extracting_sapling")); if (!resources::extractEmbeddedResources()) { - daemon_status_ = TR("sb_daemon_extract_failed"); + setDaemonStatus(TR("sb_daemon_extract_failed")); DEBUG_LOGF("[ERROR] extractEmbeddedResources() failed — disk full or permission denied?\n"); return false; } // Check again after extraction if (!rpc::Connection::verifySaplingParams()) { - daemon_status_ = TR("sb_sapling_failed"); + setDaemonStatus(TR("sb_sapling_failed")); DEBUG_LOGF("Sapling params still not found after extraction!\n"); DEBUG_LOGF("Expected location: %s\n", rpc::Connection::getSaplingParamsDir().c_str()); return false; @@ -5211,7 +5254,7 @@ bool App::startEmbeddedDaemon() if (!exe_dir.empty()) { std::string dirErr; if (!util::Platform::ensureDirectory(daemon_dir, &dirErr)) { - daemon_status_ = dirErr; + setDaemonStatus(dirErr); DEBUG_LOGF("[ERROR] %s\n", dirErr.c_str()); return false; } @@ -5245,7 +5288,7 @@ bool App::startEmbeddedDaemon() if (copied && rpc::Connection::verifySaplingParams()) { DEBUG_LOGF("Sapling params copied from exe directory successfully\n"); } else { - daemon_status_ = TR("sb_sapling_not_found"); + setDaemonStatus(TR("sb_sapling_not_found")); DEBUG_LOGF("Sapling params not found and no embedded resources available!\n"); DEBUG_LOGF("Expected location: %s\n", rpc::Connection::getSaplingParamsDir().c_str()); return false; @@ -5262,7 +5305,7 @@ bool App::startEmbeddedDaemon() if (!exe_dir.empty()) { std::string dirErr; if (!util::Platform::ensureDirectory(daemon_dir, &dirErr)) { - daemon_status_ = dirErr; + setDaemonStatus(dirErr); DEBUG_LOGF("[ERROR] %s\n", dirErr.c_str()); return false; } @@ -5298,7 +5341,7 @@ bool App::startEmbeddedDaemon() if (copyFailed) { char buf[512]; snprintf(buf, sizeof(buf), TR("sb_daemon_files_failed"), daemon_dir.c_str()); - daemon_status_ = buf; + setDaemonStatus(buf); DEBUG_LOGF("[ERROR] One or more daemon files failed to copy to %s\n", daemon_dir.c_str()); return false; } @@ -5311,21 +5354,22 @@ bool App::startEmbeddedDaemon() // Set up state callback daemon_controller_->setStateCallback([this](daemon::EmbeddedDaemon::State state, const std::string& msg) { + // NB: this callback runs on EmbeddedDaemon's monitor thread — write via the locked setter. switch (state) { case daemon::EmbeddedDaemon::State::Starting: - daemon_status_ = TR("sb_starting_daemon"); + setDaemonStatus(TR("sb_starting_daemon")); break; case daemon::EmbeddedDaemon::State::Running: - daemon_status_ = TR("sb_dragonxd_running"); + setDaemonStatus(TR("sb_dragonxd_running")); break; case daemon::EmbeddedDaemon::State::Stopping: - daemon_status_ = TR("sb_dragonxd_stopping"); + setDaemonStatus(TR("sb_dragonxd_stopping")); break; case daemon::EmbeddedDaemon::State::Stopped: - daemon_status_ = TR("sb_dragonxd_stopped"); + setDaemonStatus(TR("sb_dragonxd_stopped")); break; case daemon::EmbeddedDaemon::State::Error: - daemon_status_ = "Error: " + msg; + setDaemonStatus("Error: " + msg); break; } }); @@ -6958,7 +7002,7 @@ std::string App::buildDiagnosticsReport() << (state_.warming_up ? " (warming up)" : "") << "\n"; #if !DRAGONX_LITE_BUILD - os << "daemon status: " << daemon_status_ << "\n"; + os << "daemon status: " << getDaemonStatus() << "\n"; if (daemon_controller_) { os << "daemon running: " << (daemon_controller_->isRunning() ? "yes" : "no") << ", crashes: " << daemon_controller_->crashCount() << "\n"; diff --git a/src/app.h b/src/app.h index dbb6794..6b2b289 100644 --- a/src/app.h +++ b/src/app.h @@ -267,7 +267,7 @@ public: bool isConnected() const { return state_.connected; } int getBlockHeight() const { return state_.sync.blocks; } const std::string& getConnectionStatus() const { return connection_status_; } - const std::string& getDaemonStatus() const { return daemon_status_; } + std::string getDaemonStatus() const; // thread-safe copy (daemon_status_ is written off-thread) // Balance info (convenience wrappers) double getShieldedBalance() const { return state_.shielded_balance; } @@ -884,6 +884,7 @@ private: std::function makeSweepCompletionCallback(bool resumed); void beginSweepToSeedWallet(); // z_mergetoaddress ["ANY_TADDR","ANY_ZADDR"] -> dest void pollSweepStatus(); // Confirming step: poll sweep confirmations + legacy balance + void locateSweepTxid(); // recover a lost sweep txid (send to dest) -> Confirming gate void beginAdoptSeedWallet(); // stop daemon -> swap wallet.dat -> restart with -rescan void provisionChatIdentityFromSecret(std::string secret); std::string chatReplyZaddr(); // a stable (persisted) wallet z-addr — chat IDENTITY (reply-to) @@ -1123,7 +1124,13 @@ private: // Embedded daemon state bool use_embedded_daemon_ = wallet::supportsEmbeddedDaemon(wallet::currentWalletCapabilities()); - std::string daemon_status_; + // Written by the DaemonController state callback (which fires on EmbeddedDaemon's monitor thread) + // AND read on the UI thread — so all access goes through set/getDaemonStatus() under this mutex. + // getDaemonStatus() returns a COPY: a returned reference could dangle if the monitor thread + // reassigned the string mid-read (data race / use-after-free). + mutable std::mutex daemon_status_mutex_; + std::string daemon_status_; // guarded by daemon_status_mutex_ — use set/getDaemonStatus() + void setDaemonStatus(std::string s); mutable std::string daemon_mem_diag_; // diagnostic info for daemon memory detection size_t daemon_output_offset_ = 0; // for incremental output parsing (rescan detection) diff --git a/src/app_network.cpp b/src/app_network.cpp index a987c54..ed58e89 100644 --- a/src/app_network.cpp +++ b/src/app_network.cpp @@ -4701,6 +4701,9 @@ void App::showSeedMigrationDialog() seed_migration_balance_loaded_ = false; seed_migration_nofunds_confirmed_ = false; refreshSeedMigrationBalance(); + // If a sweep was already broadcast (the marker survives a lost opid), try to locate its tx so + // we resume at the confirmation gate instead of risking a premature "no funds -> adopt". + if (settings_ && settings_->getSeedMigrationSweepSubmitted()) locateSweepTxid(); break; case MigrationResume::Intro: default: @@ -4768,6 +4771,12 @@ void App::beginSweepToSeedWallet() } seed_migration_step_ = SeedMigrationStep::Sweeping; seed_migration_status_ = "Building the sweep transaction…"; + // Mark the sweep as submitted BEFORE broadcasting (persisted synchronously). If the app or daemon + // then dies after the tx reaches the mempool but before its opid/txid is persisted, a resumed + // migration still knows a sweep is in flight and won't mistake the resulting ~0 legacy balance for + // an empty wallet and adopt (swap wallet.dat) prematurely — funds in an unconfirmed sweep can still + // revert on a drop/reorg. Cleared below only if the submit definitively fails (nothing broadcast). + if (settings_) { settings_->setSeedMigrationSweepSubmitted(true); settings_->save(); } const std::string dest = seed_migration_dest_; worker_->post([this, dest]() -> rpc::RPCWorker::MainCb { std::string opid, err; @@ -4780,6 +4789,8 @@ void App::beginSweepToSeedWallet() } catch (const std::exception& e) { err = e.what(); } return [this, opid, err]() { if (!err.empty() || opid.empty()) { + // Submit failed — nothing was broadcast, so drop the "sweep in flight" marker. + if (settings_) { settings_->setSeedMigrationSweepSubmitted(false); settings_->save(); } seed_migration_status_ = err.empty() ? "The sweep failed to start." : err; seed_migration_step_ = SeedMigrationStep::Error; return; @@ -4882,6 +4893,69 @@ void App::pollSweepStatus() }); } +// Best-effort recovery when a sweep was broadcast but its opid was lost (e.g. a daemon restart wiped +// the in-memory op queue) so we have no txid to confirm against. The sweep is an outgoing send to +// seed_migration_dest_; find it among the wallet's own sends (z_listsentbyaddress) and, if present, +// adopt its txid and resume at the confirmation gate (pollSweepStatus / Confirming). Read-only, and +// backstopped by that gate (confs >= 1 && legacy balance ~0), so a wrong match cannot lose funds — +// it only avoids the premature "no funds -> adopt" short-circuit while a sweep is unconfirmed. +void App::locateSweepTxid() +{ + if (capture_mode_) return; + if (!rpc_ || !worker_ || !state_.connected) return; + if (seed_migration_confirm_in_flight_ || !seed_migration_sweep_txid_.empty()) return; + if (seed_migration_dest_.empty()) return; + seed_migration_confirm_in_flight_ = true; + const std::string dest = seed_migration_dest_; + worker_->post([this, dest]() -> rpc::RPCWorker::MainCb { + std::string found; + rpc::RPCClient::TraceScope trace("Migrate / locate sweep"); + try { + // z_listsentbyaddress ignores its address argument and returns ALL wallet sends + // (oldest -> newest); the sweep is the latest send whose outputs include the destination. + auto sent = rpc_->call("z_listsentbyaddress", {dest, 0}); + if (sent.is_array()) { + for (const auto& e : sent) { + if (!e.is_object() || !e.contains("sends") || !e["sends"].is_object()) continue; + const auto& s = e["sends"]; + bool toDest = false; + for (const char* leg : {"saplingSends", "transparentSends"}) { + auto it = s.find(leg); + if (it == s.end() || !it->is_array()) continue; + for (const auto& o : *it) { + if (o.is_object() && o.value("address", std::string()) == dest) { + toDest = true; + break; + } + } + if (toDest) break; + } + if (toDest) { + auto t = e.value("txid", std::string()); + if (!t.empty()) found = t; // keep scanning; entries are oldest->newest + } + } + } + } catch (...) {} + return [this, found]() { + seed_migration_confirm_in_flight_ = false; + if (found.empty() || !seed_migration_sweep_txid_.empty()) return; + // Adopt the recovered txid and hand off to the confirmation gate (which alone permits adopt). + seed_migration_sweep_txid_ = found; + if (settings_) { + settings_->setSeedMigrationSweepTxid(found); + settings_->setSeedMigrationSweepOpid(""); + settings_->save(); + } + seed_migration_sweep_confs_ = 0; + seed_migration_legacy_remaining_ = -1.0; + seed_migration_poll_timer_ = 0.0f; + seed_migration_status_.clear(); + seed_migration_step_ = SeedMigrationStep::Confirming; + }; + }); +} + // Phase 2 step 2: adopt the new seed wallet as the primary wallet. Stop the daemon, move the // legacy wallet.dat aside to a timestamped backup (NEVER delete it), copy the new wallet in, and // restart with -rescan so the new keys pick up the swept funds. Runs on a background thread. @@ -5308,6 +5382,7 @@ void App::pumpSeedMigration() settings_->setSeedMigrationTempDir(""); settings_->setSeedMigrationSweepTxid(""); settings_->setSeedMigrationSweepOpid(""); // W3-3 + settings_->setSeedMigrationSweepSubmitted(false); settings_->save(); } seed_migration_status_ = err; // a non-empty warning here (e.g. restart hiccup) is shown on Done @@ -5355,6 +5430,7 @@ void App::pumpSeedMigration() // txid/opid (the resume block reads these whenever the migration is pending). settings_->setSeedMigrationSweepTxid(""); settings_->setSeedMigrationSweepOpid(""); + settings_->setSeedMigrationSweepSubmitted(false); settings_->save(); } } else { diff --git a/src/config/settings.cpp b/src/config/settings.cpp index e7973c2..8793d3d 100644 --- a/src/config/settings.cpp +++ b/src/config/settings.cpp @@ -267,6 +267,7 @@ bool Settings::load(const std::string& path) loadScalar(j, "seed_migration_temp_dir", seed_migration_temp_dir_); loadScalar(j, "seed_migration_sweep_txid", seed_migration_sweep_txid_); loadScalar(j, "seed_migration_sweep_opid", seed_migration_sweep_opid_); + loadScalar(j, "seed_migration_sweep_submitted", seed_migration_sweep_submitted_); loadScalar(j, "auto_lock_timeout", auto_lock_timeout_); loadScalar(j, "unlock_duration", unlock_duration_); loadScalar(j, "pin_enabled", pin_enabled_); @@ -559,6 +560,7 @@ bool Settings::save(const std::string& path) j["seed_migration_temp_dir"] = seed_migration_temp_dir_; j["seed_migration_sweep_txid"] = seed_migration_sweep_txid_; j["seed_migration_sweep_opid"] = seed_migration_sweep_opid_; + j["seed_migration_sweep_submitted"] = seed_migration_sweep_submitted_; j["auto_lock_timeout"] = auto_lock_timeout_; j["unlock_duration"] = unlock_duration_; j["pin_enabled"] = pin_enabled_; diff --git a/src/config/settings.h b/src/config/settings.h index e9bdd88..8ec5709 100644 --- a/src/config/settings.h +++ b/src/config/settings.h @@ -396,6 +396,12 @@ public: // in the same write that persists the txid, so the txid always outranks it (see [[decideSeedMigrationResume]]). std::string getSeedMigrationSweepOpid() const { return seed_migration_sweep_opid_; } void setSeedMigrationSweepOpid(const std::string& v) { seed_migration_sweep_opid_ = v; } + // Set TRUE synchronously when a sweep is broadcast; survives the stale-opid resume fallback so the + // Sweep step knows a ~0 legacy balance may be an UNCONFIRMED in-flight sweep (funds not yet safe) + // and must NOT short-circuit to adopt/swap wallet.dat. Cleared on confirmed-adopt / reset / a + // failed submit. See [[decideSeedMigrationResume]] and App::renderSeedMigrationDialog (fund-safety). + bool getSeedMigrationSweepSubmitted() const { return seed_migration_sweep_submitted_; } + void setSeedMigrationSweepSubmitted(bool v) { seed_migration_sweep_submitted_ = v; } // Security — auto-lock timeout (seconds; 0 = disabled) int getAutoLockTimeout() const { return auto_lock_timeout_; } @@ -640,6 +646,7 @@ private: std::string seed_migration_temp_dir_; std::string seed_migration_sweep_txid_; std::string seed_migration_sweep_opid_; + bool seed_migration_sweep_submitted_ = false; int auto_lock_timeout_ = 900; // 15 minutes int unlock_duration_ = 600; // 10 minutes bool pin_enabled_ = false; diff --git a/src/data/seed_migration_resume.h b/src/data/seed_migration_resume.h index fe138d9..9b29d77 100644 --- a/src/data/seed_migration_resume.h +++ b/src/data/seed_migration_resume.h @@ -34,4 +34,12 @@ inline MigrationResume decideSeedMigrationResume(bool pending, return MigrationResume::SweepGate; } +// At the Sweep step with a ~0 legacy balance, may we offer the direct "no funds -> adopt" short-cut +// (swap wallet.dat immediately)? Only when NO sweep has been submitted — a genuinely empty wallet. +// If a sweep WAS submitted, a ~0 balance is ambiguous: the sweep tx may be unconfirmed in the mempool +// and could revert on a drop/reorg, so adopting now can strand the funds in the moved-aside backup. +// In that case adopt must go through the confirmation-gated path (>=1 conf) instead — never swap +// wallet.dat on an unconfirmed sweep. (fund-safety: audit) +inline bool sweepGateMayAdoptEmptyWallet(bool sweepSubmitted) { return !sweepSubmitted; } + } // namespace dragonx diff --git a/src/data/wallet_state.h b/src/data/wallet_state.h index dc0a00f..058677a 100644 --- a/src/data/wallet_state.h +++ b/src/data/wallet_state.h @@ -149,7 +149,13 @@ struct SyncInfo { float witness_progress = 0.0f; // 0.0 - 1.0, within the current sub-phase int witness_remaining = 0; // blocks left in the cache walk (0 if unknown / phase 1) - bool isSynced() const { return !syncing && blocks > 0 && blocks >= headers - 2; } + // True only when the daemon reports a positive peer-derived tip (longestchain > 0). A node with + // no peers reports longestchain == 0, so `blocks` is then only the LOCAL best height and cannot be + // trusted as "at the network tip". isSynced() gates on this so a peerless / cold-started node is + // never reported synced (which would enable Send against a stale balance). (audit: peerless-synced) + bool tip_known = false; + + bool isSynced() const { return tip_known && !syncing && blocks > 0 && blocks >= headers - 2; } }; /** diff --git a/src/services/network_refresh_service.cpp b/src/services/network_refresh_service.cpp index 2d79484..964ecb9 100644 --- a/src/services/network_refresh_service.cpp +++ b/src/services/network_refresh_service.cpp @@ -853,8 +853,11 @@ void NetworkRefreshService::appendViewTransactionOutputs(std::vector& transactions, + const std::string& txid, + const nlohmann::json& sentEntry) +{ + if (!sentEntry.is_object() || !sentEntry.contains("sends") || !sentEntry["sends"].is_object()) + return; + const auto& sends = sentEntry["sends"]; + auto it = sends.find("transparentSends"); + if (it == sends.end() || !it->is_array()) return; + + // Per-tx metadata for the synthesized rows. + int confirmations = 0; + std::int64_t timestamp = 0; + if (auto v = readOptional(sentEntry, "confirmations")) confirmations = *v; + if (auto v = readOptional(sentEntry, "blocktime")) timestamp = *v; + if (timestamp == 0) { + if (auto v = readOptional(sentEntry, "time")) timestamp = *v; + } + + for (const auto& out : *it) { + auto amount = readOptional(out, "amount"); + if (!amount) continue; + const double debit = -std::abs(*amount); // the daemon reports -nValue; a send is a debit + std::string address; + if (auto a = readOptional(out, "address")) address = *a; + + // Dedup against rows already present — notably listtransactions-derived t->t sends, which + // this whole-wallet payload ALSO reports. Same txid+address+amount == the same output. + bool exists = false; + for (const auto& e : transactions) { + if (e.txid == txid && e.type == "send" && e.address == address && + std::abs(e.amount - debit) < 0.00000001) { + exists = true; + break; + } + } + if (exists) continue; + + TransactionInfo info; + info.txid = txid; + info.type = "send"; + info.address = address; + info.amount = debit; + info.confirmations = confirmations; + info.timestamp = timestamp; + transactions.push_back(std::move(info)); + } +} + void NetworkRefreshService::sortTransactionsNewestFirst(std::vector& transactions) { std::sort(transactions.begin(), transactions.end(), @@ -985,6 +1037,45 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectTr result.shieldedScanComplete = !pendingShieldedIndex.has_value(); result.nextShieldedScanStartIndex = pendingShieldedIndex.value_or(0); + // Discover outgoing shielded (z->z) sends. A pure z->z send moves no value in or out of the + // transparent pool (valueBalance == 0), so the daemon's `listtransactions` never reports it, + // and — being a send rather than a receive — it never shows up in `z_listreceivedbyaddress` + // either. Its txid therefore never reaches `knownTxids`, so the z_viewtransaction enrichment + // below is never run for it and the send is invisible in History (the reported bug). + // + // `z_listsentbyaddress` scans the whole wallet for sends — the address argument is NOT used to + // filter (the daemon iterates every wallet tx against "*"), so a single call yields all send + // txids. Two legs need handling: + // * SHIELDED (z->z) outgoing legs are surfaced by z_viewtransaction, so we fold the txid into + // `knownTxids` and let the z_viewtransaction / appendViewTransactionOutputs path build them. + // * TRANSPARENT outgoing legs (z->t deshields) are NOT surfaced by z_viewtransaction (it + // enumerates only Sapling outputs), and for a z-originated send listtransactions also omits + // the external t-vout (GetAmounts' isFromMyTaddr is false when inputs are shielded). So we + // build those rows directly from the payload's `sends.transparentSends[]` here. They are + // deduped (by txid+address+amount) against listtransactions-derived t->t sends above, so a + // purely-transparent (t->t) send — which the payload ALSO reports — is not double-counted. + // + // Gated on shieldedScanComplete so it adds no cost during the multi-cycle initial received + // scan: it is one more O(mapWallet) pass holding cs_main (comparable to the listtransactions + // pass), and the caller's adaptive throttle (txRefreshDue) already accounts for it via scanMs. + if (result.shieldedScanComplete && !snapshot.shieldedAddresses.empty()) { + try { + json sent = rpc.call("z_listsentbyaddress", + json::array({snapshot.shieldedAddresses.front(), 0})); + if (sent.is_array()) { + for (const auto& sentTx : sent) { + auto txid = readOptional(sentTx, "txid"); + if (!txid || txid->empty()) continue; + appendSentTransparentOutputs(result.transactions, *txid, sentTx); + knownTxids.insert(*txid); // shielded (z->z) legs enriched via z_viewtransaction + } + } + } catch (const std::exception& e) { + transactionRpcError = true; + DEBUG_LOGF("z_listsentbyaddress error: %s\n", e.what()); + } + } + for (const auto& txid : snapshot.sendTxids) { knownTxids.insert(txid); } @@ -1305,6 +1396,10 @@ void NetworkRefreshService::applyCoreRefreshResult(WalletState& state, if (result.headers) state.sync.headers = *result.headers; if (result.bestBlockHash) state.sync.best_blockhash = *result.bestBlockHash; if (result.verificationProgress) state.sync.verification_progress = *result.verificationProgress; + // A peerless daemon reports longestchain == 0 (no peer heights). Track that as "tip unknown" from + // THIS refresh's raw value (not the retained state, which keeps the last positive value) so a + // peerless node isn't reported synced against its own stale local height. (audit: peerless-synced) + state.sync.tip_known = (result.longestChain.has_value() && *result.longestChain > 0); if (result.longestChain && *result.longestChain > 0) state.longestchain = *result.longestChain; if (state.longestchain > 0 && state.sync.blocks > state.longestchain) state.longestchain = state.sync.blocks; if (state.longestchain > 0) diff --git a/src/services/network_refresh_service.h b/src/services/network_refresh_service.h index 426463e..ac94feb 100644 --- a/src/services/network_refresh_service.h +++ b/src/services/network_refresh_service.h @@ -310,6 +310,12 @@ public: static void appendViewTransactionOutputs(std::vector& transactions, const std::string& txid, const TransactionViewCacheEntry& entry); + // Build "send" rows from a z_listsentbyaddress entry's sends.transparentSends[] — the transparent + // (z->t deshield) outgoing outputs that z_viewtransaction does not surface. Deduped by + // txid+address+amount against rows already present (e.g. listtransactions-derived t->t sends). + static void appendSentTransparentOutputs(std::vector& transactions, + const std::string& txid, + const nlohmann::json& sentEntry); static void sortTransactionsNewestFirst(std::vector& transactions); static TransactionRefreshResult collectTransactionRefreshResult(RefreshRpcGateway& rpc, const TransactionRefreshSnapshot& snapshot, diff --git a/src/ui/node_status_banner.h b/src/ui/node_status_banner.h index 6233e5e..a4dbe2a 100644 --- a/src/ui/node_status_banner.h +++ b/src/ui/node_status_banner.h @@ -31,6 +31,7 @@ enum class NodeBannerReason { FullNodeOffline, // a reachable node was lost, or never came up; reconnect offered DaemonCrashed, // the embedded daemon crashed repeatedly and auto-restart stopped LiteOpenFailed, // lite build: the wallet failed to open + NoPeers, // connected to the daemon but no peer-derived tip (0 peers) — stalled, stale }; struct NodeBannerState { @@ -50,6 +51,13 @@ struct NodeBannerInputs { bool daemon_initializing = false; // daemon launching / block index loading bool connection_in_progress = false; // a connect attempt is actively running + // Peer connectivity: a full node connected to the daemon but with NO peer-derived tip + // (longestchain == 0 -> tip_known false) has no peers, so it silently stalls at a stale height + // while balance/sync look "done". `blocks` gates the check so we don't flag the brief window + // before the first chain-info refresh. (audit: no-peer-banner) + bool tip_known = true; // sync.tip_known — a peer-derived network tip is known + int blocks = 0; // sync.blocks — >0 once chain info has been fetched + // Full-node embedded-daemon crash signal. bool using_embedded_daemon = false; bool has_daemon_controller = false; @@ -81,13 +89,26 @@ inline NodeBannerState evaluateNodeStatusBanner(const NodeBannerInputs& in) { return s; } - // Full node. Connected, or in an expected startup phase → the loading/warmup overlay owns - // the screen, so no banner. An active connect attempt likewise shows progress, not an - // error — don't flicker a banner over it. - if (in.connected) return s; + // Full node. In an expected startup phase → the loading/warmup overlay owns the screen, so no + // banner. An active connect attempt likewise shows progress, not an error — don't flicker a + // banner over it. (These return even if `connected` has flipped early, matching prior behavior.) if (in.warming_up || in.daemon_initializing) return s; if (in.connection_in_progress) return s; + // Connected to the daemon (RPC works) but with no peer-derived tip → the node has no peers and + // cannot learn about new blocks; it silently stalls at a stale height while everything looks + // synced. Surface it as a recoverable warning (reconnect won't help, so no action button). + if (in.connected) { + if (!in.tip_known && in.blocks > 0) { + s.show = true; + s.severity = NodeBannerSeverity::Warning; + s.reason = NodeBannerReason::NoPeers; + s.action = NodeBannerAction::None; + s.detail = in.connection_status; + } + return s; + } + // Genuinely offline. Distinguish "the embedded daemon crashed and we stopped retrying" (a // hard fault needing a manual restart) from an ordinary lost/failed connection (retryable). if (in.using_embedded_daemon && in.has_daemon_controller && !in.daemon_running && diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index 8780f1d..8c3df39 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -425,6 +425,9 @@ void I18n::loadBuiltinEnglish() // Sweep step: balance still loading / no funds to move strings_["mig_checking_balance"] = "Checking your balance"; strings_["mig_no_funds"] = "Your wallet has no funds to migrate. You can adopt the new seed wallet directly — it replaces your current (empty) wallet with the seed-backed one. Your old wallet is still moved aside to a timestamped backup, just in case."; + strings_["mig_sweep_inflight"] = "A sweep was already started and hasn't confirmed yet. Your balance reads zero because the funds are in the pending sweep transaction — they are not lost. Wait for it to confirm before finishing; the wallet will not be swapped until the sweep is mined."; + strings_["mig_checking_sweep"] = "Looking for the sweep transaction"; + strings_["mig_check_sweep"] = "Check the sweep"; strings_["mig_adopt_now"] = "Adopt seed wallet"; strings_["mig_nofunds_confirm"] = "I understand this replaces my current wallet with the new seed wallet"; // Adopting step note (shown under the spinner while the node restarts + rescans) @@ -1459,6 +1462,7 @@ void I18n::loadBuiltinEnglish() strings_["sb_wallet_needs_recovery"] = "Wallet repair available"; // Persistent node-status banner (App::renderNodeStatusBanner). strings_["node_banner_offline_title"] = "Not connected to the DragonX node"; + strings_["node_banner_no_peers_title"] = "Connected, but no network peers — the node can't sync"; strings_["node_banner_crashed_title"] = "The node stopped unexpectedly"; strings_["node_banner_lite_open_failed"] = "Couldn't open your wallet"; strings_["node_banner_reconnect"] = "Reconnect"; diff --git a/src/wallet/lite_wallet_controller.cpp b/src/wallet/lite_wallet_controller.cpp index f5971e0..2818cf6 100644 --- a/src/wallet/lite_wallet_controller.cpp +++ b/src/wallet/lite_wallet_controller.cpp @@ -450,31 +450,47 @@ void LiteWalletController::pumpAsyncOpen() bool LiteWalletController::beginCreateWalletAsync(LiteWalletCreateRequest request) { auto req = std::make_shared(std::move(request)); - return beginLifecycleRequestAsync( + // Encrypt the brand-new wallet with the user's passphrase once it opens. The async worker + // can't (walletOpen_ isn't set until pumpLifecycleResult runs), so stash it and apply on the + // main-thread finalize — otherwise the passphrase is discarded and the seed stored in + // plaintext (audit C1). Mirrors the synchronous createWallet() wrapper. + stashPendingLifecycleSecurity(req->passphrase, PendingSecurityOp::Encrypt); + const bool started = beginLifecycleRequestAsync( "Create", [req](LiteWalletLifecycleService& svc, const std::string& url) { req->serverUrl = url; return svc.createWallet(*req); }, [req]() { secureWipeLiteSecret(req->passphrase); }); + if (!started) clearPendingLifecycleSecurity(); + return started; } bool LiteWalletController::beginOpenWalletAsync(LiteWalletOpenRequest request) { auto req = std::make_shared(std::move(request)); - return beginLifecycleRequestAsync( + // An existing wallet may be encrypted+locked — unlock it with the supplied passphrase on the + // main-thread finalize (the async worker can't; walletOpen_ isn't set yet). Without this an + // encrypted wallet opens but stays locked and the passphrase is never verified (audit M1). + stashPendingLifecycleSecurity(req->passphrase, PendingSecurityOp::Unlock); + const bool started = beginLifecycleRequestAsync( "Open", [req](LiteWalletLifecycleService& svc, const std::string& url) { req->serverUrl = url; return svc.openWallet(*req); }, [req]() { secureWipeLiteSecret(req->passphrase); }); + if (!started) clearPendingLifecycleSecurity(); + return started; } bool LiteWalletController::beginRestoreWalletAsync(LiteWalletRestoreRequest request) { auto req = std::make_shared(std::move(request)); - return beginLifecycleRequestAsync( + // Encrypt the restored wallet with the user's passphrase once it opens (same rationale and + // main-thread finalize as create — audit C1). + stashPendingLifecycleSecurity(req->passphrase, PendingSecurityOp::Encrypt); + const bool started = beginLifecycleRequestAsync( "Restore", [req](LiteWalletLifecycleService& svc, const std::string& url) { req->serverUrl = url; @@ -484,6 +500,8 @@ bool LiteWalletController::beginRestoreWalletAsync(LiteWalletRestoreRequest requ secureWipeLiteSecret(req->seedPhrase); secureWipeLiteSecret(req->passphrase); }); + if (!started) clearPendingLifecycleSecurity(); + return started; } bool LiteWalletController::beginLifecycleRequestAsync( @@ -585,15 +603,54 @@ void LiteWalletController::pumpLifecycleResult() // log the failure otherwise (shared with the synchronous lifecycle path). onLifecycleResult(out); if (out.walletReady) { + // Wallet is now open (walletOpen_ set by onLifecycleResult) — apply the stashed passphrase: + // encrypt a freshly created/restored wallet, or unlock an existing encrypted one (C1/M1). + applyPendingLifecycleSecurity(); lastOpenError_.clear(); lastOpenWarming_ = false; } else { + clearPendingLifecycleSecurity(); // wallet didn't open — drop the stashed passphrase lastOpenError_ = out.error.empty() ? out.status.message : out.error; lastOpenWarming_ = liteOpenErrorIsWarmup(lastOpenError_); } lastLifecycleResult_ = std::move(out); } +void LiteWalletController::stashPendingLifecycleSecurity(const std::string& passphrase, + PendingSecurityOp op) +{ + clearPendingLifecycleSecurity(); // wipe any stale prior stash first + pendingLifecyclePassphrase_ = passphrase; // independent copy; wiped in apply/clear + pendingLifecycleSecurityOp_ = op; +} + +void LiteWalletController::applyPendingLifecycleSecurity() +{ + const PendingSecurityOp op = pendingLifecycleSecurityOp_; + if (op == PendingSecurityOp::None || pendingLifecyclePassphrase_.empty() || + !walletOpen_.load() || !bridge_) { + clearPendingLifecycleSecurity(); + return; + } + if (op == PendingSecurityOp::Encrypt) { + // encryptWallet() takes its own copy and wipes it; the backend encrypts + locks + saves. + const auto enc = encryptWallet(pendingLifecyclePassphrase_); + if (!enc.ok) + liteLog("wallet created/restored but ENCRYPTION FAILED — wallet is NOT encrypted: " + + enc.error); + } else if (op == PendingSecurityOp::Unlock) { + if (!unlockWallet(pendingLifecyclePassphrase_)) + liteLog("wallet opened but unlock failed (wrong passphrase?)"); + } + clearPendingLifecycleSecurity(); +} + +void LiteWalletController::clearPendingLifecycleSecurity() +{ + secureWipeLiteSecret(pendingLifecyclePassphrase_); // no-op if already empty + pendingLifecycleSecurityOp_ = PendingSecurityOp::None; +} + void LiteWalletController::startSync() { if (syncLaunched_.exchange(true)) return; diff --git a/src/wallet/lite_wallet_controller.h b/src/wallet/lite_wallet_controller.h index 7ea90df..5b03dac 100644 --- a/src/wallet/lite_wallet_controller.h +++ b/src/wallet/lite_wallet_controller.h @@ -388,6 +388,20 @@ private: std::make_shared>(); LiteWalletLifecycleResult lastLifecycleResult_; // main-thread only; last finalized request + // Passphrase to apply on the main thread once an async create/restore/open finalizes. The + // backend wallet is not controller-"open" (walletOpen_) until pumpLifecycleResult() runs + // onLifecycleResult(), and encryptWallet()/unlockWallet() gate on walletOpen_ — so the async + // worker thread cannot do it. The passphrase is stashed here (main-thread only) and applied in + // pumpLifecycleResult(), mirroring the synchronous createWallet()/restoreWallet()/openWallet() + // wrappers. Without this the UI's async path silently discarded the passphrase, storing the + // seed UNENCRYPTED (create/restore) or never unlocking an encrypted wallet (open) — audit C1/M1. + enum class PendingSecurityOp { None, Encrypt, Unlock }; + std::string pendingLifecyclePassphrase_; // main-thread only; wiped after use + PendingSecurityOp pendingLifecycleSecurityOp_ = PendingSecurityOp::None; + void stashPendingLifecycleSecurity(const std::string& passphrase, PendingSecurityOp op); + void applyPendingLifecycleSecurity(); // encrypt (create/restore) or unlock (open), then wipe + void clearPendingLifecycleSecurity(); // wipe + reset (rejected request / failed open) + // Joinable background refresh worker (fast iterations: syncstatus, plus data once synced). std::thread worker_; std::atomic running_{false}; diff --git a/tests/test_phase4.cpp b/tests/test_phase4.cpp index e909b23..5f88379 100644 --- a/tests/test_phase4.cpp +++ b/tests/test_phase4.cpp @@ -1628,6 +1628,7 @@ void testNetworkRefreshRpcCollectors() {"time", 50}, {"memoStr", "shielded memo"}} })); transactionRpc.addResponse("z_listreceivedbyaddress", json::array()); + transactionRpc.addResponse("z_listsentbyaddress", json::array()); transactionRpc.addResponse("z_viewtransaction", json{ {"spends", json::array({json{{"address", "zs-from"}}})}, {"outputs", json::array({ @@ -1639,12 +1640,13 @@ void testNetworkRefreshRpcCollectors() auto transactionResult = Refresh::collectTransactionRefreshResult(transactionRpc, snapshot, 321, 4); EXPECT_TRUE(transactionRpc.methodNames() == std::vector({ "listtransactions", "z_listreceivedbyaddress", "z_listreceivedbyaddress", - "z_viewtransaction", "gettransaction" + "z_listsentbyaddress", "z_viewtransaction", "gettransaction" })); EXPECT_EQ(transactionRpc.calls[1].params, json::array({"zs-one", 0})); EXPECT_EQ(transactionRpc.calls[2].params, json::array({"zs-two", 0})); - EXPECT_EQ(transactionRpc.calls[3].params, json::array({"pending-send"})); + EXPECT_EQ(transactionRpc.calls[3].params, json::array({"zs-one", 0})); EXPECT_EQ(transactionRpc.calls[4].params, json::array({"pending-send"})); + EXPECT_EQ(transactionRpc.calls[5].params, json::array({"pending-send"})); EXPECT_EQ(transactionResult.blockHeight, 321); EXPECT_EQ(transactionResult.newViewTxEntries.size(), static_cast(1)); EXPECT_EQ(transactionResult.newViewTxEntries.count("pending-send"), static_cast(1)); @@ -1657,6 +1659,129 @@ void testNetworkRefreshRpcCollectors() EXPECT_EQ(transactionResult.transactions.front().timestamp, static_cast(500)); EXPECT_EQ(transactionResult.transactions[1].txid, std::string("transparent-a")); + // Outgoing shielded (z->z) send discovery. A pure z->z send has valueBalance == 0, so it is + // absent from listtransactions AND (being a send, not a receive) from z_listreceivedbyaddress — + // it is surfaced only via the whole-wallet z_listsentbyaddress sweep, whose txids are folded + // into the z_viewtransaction enrichment to build the send row. Regression test for the + // "outgoing transactions missing from History" bug. + { + Refresh::TransactionRefreshSnapshot zzSnapshot; + zzSnapshot.shieldedAddresses = {"zs-mine"}; + MockRefreshRpc zzRpc; + zzRpc.addResponse("listtransactions", json::array()); // no transparent movement + zzRpc.addResponse("z_listreceivedbyaddress", json::array()); // it's a send, not a receive + zzRpc.addResponse("z_listsentbyaddress", json::array({ + json{{"txid", "zz-send"}} // discovered only here + })); + zzRpc.addResponse("z_viewtransaction", json{ + {"spends", json::array({json{{"address", "zs-mine"}}})}, + {"outputs", json::array({ + json{{"outgoing", true}, {"address", "zs-recipient"}, {"value", 1.25}, + {"memoStr", "z2z memo"}} + })} + }); + zzRpc.addResponse("gettransaction", json{{"time", 800}, {"confirmations", 3}}); + auto zzResult = Refresh::collectTransactionRefreshResult(zzRpc, zzSnapshot, 400, 4); + EXPECT_TRUE(zzRpc.methodNames() == std::vector({ + "listtransactions", "z_listreceivedbyaddress", "z_listsentbyaddress", + "z_viewtransaction", "gettransaction" + })); + EXPECT_EQ(zzResult.transactions.size(), static_cast(1)); + EXPECT_EQ(zzResult.transactions[0].txid, std::string("zz-send")); + EXPECT_EQ(zzResult.transactions[0].type, std::string("send")); + EXPECT_NEAR(zzResult.transactions[0].amount, -1.25, 0.00000001); + EXPECT_EQ(zzResult.transactions[0].address, std::string("zs-recipient")); + EXPECT_EQ(zzResult.transactions[0].memo, std::string("z2z memo")); + EXPECT_EQ(zzResult.transactions[0].timestamp, static_cast(800)); + EXPECT_EQ(zzResult.newViewTxEntries.count("zz-send"), static_cast(1)); + + // Guard: with no shielded addresses the sweep is skipped entirely (transparent sends are + // already covered by listtransactions), so no z_listsentbyaddress call is made. + Refresh::TransactionRefreshSnapshot noZSnapshot; + MockRefreshRpc noZRpc; + noZRpc.addResponse("listtransactions", json::array()); + auto noZResult = Refresh::collectTransactionRefreshResult(noZRpc, noZSnapshot, 401, 4); + EXPECT_TRUE(noZRpc.methodNames() == std::vector({"listtransactions"})); + EXPECT_EQ(noZResult.transactions.size(), static_cast(0)); + } + + // z->t deshield: the external transparent recipient is reported ONLY in z_listsentbyaddress's + // sends.transparentSends (z_viewtransaction sees only the shielded change note, isOutgoing=false; + // listtransactions omits the t-vout because inputs are shielded). The row must be synthesized + // directly from that payload — a direct sibling of the z->z gap. + { + Refresh::TransactionRefreshSnapshot ztSnapshot; + ztSnapshot.shieldedAddresses = {"zs-mine"}; + MockRefreshRpc ztRpc; + ztRpc.addResponse("listtransactions", json::array()); // z-originated: no t-input row + ztRpc.addResponse("z_listreceivedbyaddress", json::array()); + ztRpc.addResponse("z_listsentbyaddress", json::array({ + json{{"txid", "zt-send"}, {"confirmations", 5}, {"blocktime", 900}, + {"sends", json{ + {"transparentSends", json::array({ + json{{"address", "R-recipient"}, {"amount", -2.5}, {"vout", 0}} + })}, + {"saplingSends", json::array()} + }}} + })); + // z->t viewtransaction: only a change note back to self -> no outgoing outputs. + ztRpc.addResponse("z_viewtransaction", json{{"spends", json::array()}, {"outputs", json::array()}}); + auto ztResult = Refresh::collectTransactionRefreshResult(ztRpc, ztSnapshot, 500, 4); + EXPECT_EQ(ztResult.transactions.size(), static_cast(1)); + EXPECT_EQ(ztResult.transactions[0].txid, std::string("zt-send")); + EXPECT_EQ(ztResult.transactions[0].type, std::string("send")); + EXPECT_NEAR(ztResult.transactions[0].amount, -2.5, 0.00000001); + EXPECT_EQ(ztResult.transactions[0].address, std::string("R-recipient")); + EXPECT_EQ(ztResult.transactions[0].confirmations, 5); + } + + // t->t send is reported by BOTH listtransactions AND z_listsentbyaddress's transparentSends — + // it must appear ONCE (deduped by txid+address+amount), not doubled. + { + Refresh::TransactionRefreshSnapshot ttSnapshot; + ttSnapshot.shieldedAddresses = {"zs-mine"}; + MockRefreshRpc ttRpc; + ttRpc.addResponse("listtransactions", json::array({ + json{{"txid", "tt-send"}, {"category", "send"}, {"amount", -1.0}, + {"time", 800}, {"confirmations", 3}, {"address", "R-dest"}} + })); + ttRpc.addResponse("z_listreceivedbyaddress", json::array()); + ttRpc.addResponse("z_listsentbyaddress", json::array({ + json{{"txid", "tt-send"}, {"confirmations", 3}, {"blocktime", 800}, + {"sends", json{{"transparentSends", json::array({ + json{{"address", "R-dest"}, {"amount", -1.0}, {"vout", 0}} + })}}}} + })); + ttRpc.addResponse("z_viewtransaction", json{{"spends", json::array()}, {"outputs", json::array()}}); + auto ttResult = Refresh::collectTransactionRefreshResult(ttRpc, ttSnapshot, 500, 4); + int ttSends = 0; + for (const auto& t : ttResult.transactions) + if (t.txid == std::string("tt-send") && t.type == std::string("send")) ++ttSends; + EXPECT_EQ(ttSends, 1); // not double-counted + } + + // Multi-output send: two outgoing outputs of EQUAL value to DIFFERENT recipients in one tx must + // both render (address-keyed dedup) — an amount-only dedup dropped the second. + { + Refresh::TransactionRefreshSnapshot multiSnapshot; + multiSnapshot.sendTxids = {"multi-send"}; + MockRefreshRpc multiRpc; + multiRpc.addResponse("listtransactions", json::array()); + multiRpc.addResponse("z_viewtransaction", json{ + {"spends", json::array({json{{"address", "zs-from"}}})}, + {"outputs", json::array({ + json{{"outgoing", true}, {"address", "zs-a"}, {"value", 0.5}, {"memoStr", "a"}}, + json{{"outgoing", true}, {"address", "zs-b"}, {"value", 0.5}, {"memoStr", "b"}} + })} + }); + multiRpc.addResponse("gettransaction", json{{"time", 700}, {"confirmations", 2}}); + auto multiResult = Refresh::collectTransactionRefreshResult(multiRpc, multiSnapshot, 500, 4); + int multiSends = 0; + for (const auto& t : multiResult.transactions) + if (t.txid == std::string("multi-send") && t.type == std::string("send")) ++multiSends; + EXPECT_EQ(multiSends, 2); // both equal-value outputs kept + } + Refresh::TransactionRefreshSnapshot cachedOnlySnapshot; auto cachedOnlyEntry = cachedEntry; cachedOnlyEntry.timestamp = 450; @@ -1802,6 +1927,7 @@ void testNetworkRefreshRpcCollectors() strictRpc.addResponse("listtransactions", json::array()); strictRpc.addResponse("z_listreceivedbyaddress", json::array()); strictRpc.addResponse("z_listreceivedbyaddress", json::array()); + strictRpc.addResponse("z_listsentbyaddress", json::array()); auto strictResult = Refresh::collectTransactionRefreshResult(strictRpc, strictSnapshot, 102, 4); EXPECT_EQ(strictResult.shieldedAddressesScanned, static_cast(2)); // both re-scanned @@ -1813,10 +1939,12 @@ void testNetworkRefreshRpcCollectors() MockRefreshRpc tolerantRpc; tolerantRpc.addResponse("listtransactions", json::array()); // No z_listreceivedbyaddress responses: the addresses must be skipped (else call() throws). + // The shielded scan still completes, so the whole-wallet z_listsentbyaddress sweep runs once. + tolerantRpc.addResponse("z_listsentbyaddress", json::array()); auto tolerantResult = Refresh::collectTransactionRefreshResult(tolerantRpc, tolerantSnapshot, 102, 4); EXPECT_EQ(tolerantResult.shieldedAddressesScanned, static_cast(0)); // both skipped EXPECT_TRUE(tolerantResult.shieldedScanComplete); - EXPECT_TRUE(tolerantRpc.methodNames() == std::vector({"listtransactions"})); + EXPECT_TRUE(tolerantRpc.methodNames() == std::vector({"listtransactions", "z_listsentbyaddress"})); } Refresh::TransactionRefreshSnapshot recentSnapshot; @@ -1902,10 +2030,11 @@ void testNetworkRefreshRpcCollectors() MockRefreshRpc finalShieldedRpc; finalShieldedRpc.addResponse("listtransactions", json::array()); finalShieldedRpc.addResponse("z_listreceivedbyaddress", json::array()); + finalShieldedRpc.addResponse("z_listsentbyaddress", json::array()); auto finalShielded = Refresh::collectTransactionRefreshResult( finalShieldedRpc, partialShieldedSnapshot, 400, 0); EXPECT_TRUE(finalShieldedRpc.methodNames() == std::vector({ - "listtransactions", "z_listreceivedbyaddress" + "listtransactions", "z_listreceivedbyaddress", "z_listsentbyaddress" })); EXPECT_EQ(finalShieldedRpc.calls[1].params, json::array({"zs-two", 0})); EXPECT_TRUE(finalShielded.shieldedScanComplete); @@ -1922,9 +2051,10 @@ void testNetworkRefreshRpcCollectors() }); MockRefreshRpc cachedShieldedRpc; cachedShieldedRpc.addResponse("listtransactions", json::array()); + cachedShieldedRpc.addResponse("z_listsentbyaddress", json::array()); auto cachedShielded = Refresh::collectTransactionRefreshResult( cachedShieldedRpc, cachedShieldedSnapshot, 500, 0); - EXPECT_TRUE(cachedShieldedRpc.methodNames() == std::vector({"listtransactions"})); + EXPECT_TRUE(cachedShieldedRpc.methodNames() == std::vector({"listtransactions", "z_listsentbyaddress"})); EXPECT_TRUE(cachedShielded.shieldedScanComplete); EXPECT_EQ(cachedShielded.shieldedAddressesScanned, static_cast(0)); EXPECT_EQ(cachedShielded.transactions.size(), static_cast(1)); @@ -1970,6 +2100,7 @@ void testNetworkRefreshRpcCollectors() json{{"txid", "shielded-mined"}, {"amount", 4.0}, {"confirmations", 102}, {"time", 210}, {"memoStr", "pool"}} })); + miningTxRpc.addResponse("z_listsentbyaddress", json::array()); auto miningTxResult = Refresh::collectTransactionRefreshResult(miningTxRpc, miningSnapshot, 328, 0); EXPECT_EQ(miningTxResult.transactions.size(), static_cast(2)); EXPECT_EQ(miningTxResult.transactions[0].type, std::string("mined")); @@ -2005,6 +2136,25 @@ void testNetworkRefreshResultModels() EXPECT_EQ(state.longestchain, 110); EXPECT_EQ(state.notarized, 90); EXPECT_EQ(state.last_balance_update, static_cast(1234)); + EXPECT_TRUE(state.sync.tip_known); // longestchain>0 -> peer-derived tip is known + EXPECT_FALSE(state.sync.isSynced()); // still catching up (blocks 100 < tip 110) + + // Peerless node: the daemon reports longestchain == 0. Even at the local best height (blocks == + // headers) it must NOT be reported synced (the tip is unknown) — otherwise Send would be offered + // against a stale balance on a node that has silently lost all peers. (audit: peerless-synced) + { + dragonx::WalletState peerless; + auto pc = Refresh::parseCoreRefreshResult( + json{{"private", "5.00000000"}, {"transparent", "0.00000000"}, {"total", "5.00000000"}}, + json{{"private", "5.00000000"}, {"transparent", "0.00000000"}, {"total", "5.00000000"}}, + true, + json{{"blocks", 200}, {"headers", 200}, {"bestblockhash", "peerless-200"}, + {"verificationprogress", 1.0}, {"longestchain", 0}, {"notarized", 0}}, + true); + Refresh::applyCoreRefreshResult(peerless, pc, 1235); + EXPECT_FALSE(peerless.sync.tip_known); + EXPECT_FALSE(peerless.sync.isSynced()); // peerless -> not synced despite blocks == headers + } auto connectionInfo = Refresh::parseConnectionInfoResult( json{{"version", 120000}, {"protocolversion", 170002}, {"p2pport", 8233}, @@ -2638,6 +2788,28 @@ void testNodeStatusBanner() NodeBannerInputs in; in.connected = true; EXPECT_TRUE(!evaluateNodeStatusBanner(in).show); } + // Connected but no peer-derived tip (0 peers) once chain info is in → amber "no peers" banner. + { + NodeBannerInputs in; + in.connected = true; in.tip_known = false; in.blocks = 3262059; + in.connection_status = "Peers: 0"; + NodeBannerState s = evaluateNodeStatusBanner(in); + EXPECT_TRUE(s.show); + EXPECT_TRUE(s.severity == NodeBannerSeverity::Warning); + EXPECT_TRUE(s.reason == NodeBannerReason::NoPeers); + EXPECT_TRUE(s.action == NodeBannerAction::None); + EXPECT_EQ(s.detail, std::string("Peers: 0")); + } + // Connected, tip unknown, but no chain info yet (blocks==0) → no banner (avoid startup flicker). + { + NodeBannerInputs in; in.connected = true; in.tip_known = false; in.blocks = 0; + EXPECT_TRUE(!evaluateNodeStatusBanner(in).show); + } + // Connected with a known tip → no banner. + { + NodeBannerInputs in; in.connected = true; in.tip_known = true; in.blocks = 100; + EXPECT_TRUE(!evaluateNodeStatusBanner(in).show); + } // Expected startup phases own the screen (loading/warmup overlay) → no banner. { NodeBannerInputs in; in.warming_up = true; @@ -2792,6 +2964,12 @@ void testSeedMigrationResume() // No txid and no opid → the Sweep gate (whether or not connected). EXPECT_TRUE(decideSeedMigrationResume(true, true, "", "", true) == MigrationResume::SweepGate); EXPECT_TRUE(decideSeedMigrationResume(true, true, "", "", false) == MigrationResume::SweepGate); + + // fund-safety: the Sweep step may only short-circuit a ~0 balance straight to adopt when NO sweep + // was ever submitted (a genuinely empty wallet). Once a sweep has been broadcast, a ~0 balance is + // ambiguous (the tx may be unconfirmed in the mempool), so direct adopt must be blocked. + EXPECT_TRUE(dragonx::sweepGateMayAdoptEmptyWallet(/*sweepSubmitted=*/false)); // empty wallet -> ok + EXPECT_FALSE(dragonx::sweepGateMayAdoptEmptyWallet(/*sweepSubmitted=*/true)); // in-flight -> blocked } void testLoggerFileSink() @@ -5301,6 +5479,92 @@ void testLiteWalletControllerAsyncLifecycleFailover() dragonx::test::g_liteFakeWarmupServerSubstr.clear(); } +// C1/M1: the ASYNC lifecycle path (the one the Settings UI actually calls) must apply the +// passphrase — encrypt a freshly created/restored wallet, unlock an existing encrypted one. +// Previously the async path discarded the passphrase (encrypt/unlock lived only in the sync +// wrappers), so a UI-created wallet stored its seed in PLAINTEXT and an encrypted wallet opened +// but stayed locked. +void testLiteWalletControllerAsyncAppliesPassphrase() +{ + using namespace dragonx::wallet; + const auto liteCaps = makeWalletCapabilities(WalletBuildKind::Lite, false, true); + LiteConnectionSettings conn; + conn.chainName = "main"; + conn.servers = { LiteServerEndpoint{"https://good.example", "Good", true} }; + conn.selectionMode = LiteServerSelectionMode::Sticky; + conn.stickyServerUrl = "https://good.example"; + + const auto drain = [](LiteWalletController& c) { + for (int i = 0; i < 400 && c.lifecycleRequestInProgress(); ++i) + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + c.pumpLifecycleResult(); + }; + + // C1: async create with a passphrase encrypts (and locks) the new wallet; same passphrase unlocks. + { + dragonx::test::resetLiteFakeCounters(); + dragonx::test::g_liteFakeWalletExists = false; + dragonx::test::g_liteFakeEncrypted = false; + dragonx::test::g_liteFakeLocked = false; + LiteWalletController controller(liteCaps, conn, + LiteClientBridge::fromApi(dragonx::test::makeFakeLiteApi())); + LiteWalletCreateRequest req; + req.passphrase = "hunter2"; + EXPECT_TRUE(controller.beginCreateWalletAsync(req)); + drain(controller); + EXPECT_TRUE(controller.walletOpen()); + const auto s = controller.encryptionStatus(); + EXPECT_TRUE(s.encrypted); // async create ENCRYPTED the wallet (C1 fixed) + EXPECT_TRUE(s.locked); // encrypt locks immediately + EXPECT_TRUE(controller.unlockWallet("hunter2")); + EXPECT_FALSE(controller.encryptionStatus().locked); + } + + // C1: async restore with a passphrase likewise encrypts the restored wallet. + { + dragonx::test::resetLiteFakeCounters(); + dragonx::test::g_liteFakeWalletExists = false; + dragonx::test::g_liteFakeEncrypted = false; + dragonx::test::g_liteFakeLocked = false; + LiteWalletController controller(liteCaps, conn, + LiteClientBridge::fromApi(dragonx::test::makeFakeLiteApi())); + LiteWalletRestoreRequest req; + req.seedPhrase = "abandon abandon abandon abandon abandon abandon abandon abandon " + "abandon abandon abandon abandon abandon abandon abandon abandon " + "abandon abandon abandon abandon abandon abandon abandon art"; + req.birthday = 0; + req.passphrase = "hunter2"; + EXPECT_TRUE(controller.beginRestoreWalletAsync(req)); + drain(controller); + EXPECT_TRUE(controller.walletOpen()); + EXPECT_TRUE(controller.encryptionStatus().encrypted); // C1 fixed for restore + } + + // M1: async open of an already-encrypted+locked wallet unlocks it with the supplied passphrase. + { + dragonx::test::resetLiteFakeCounters(); + dragonx::test::g_liteFakeWalletExists = true; + dragonx::test::g_liteFakeEncrypted = true; // existing wallet is encrypted... + dragonx::test::g_liteFakeLocked = true; // ...and locked at open time + LiteWalletController controller(liteCaps, conn, + LiteClientBridge::fromApi(dragonx::test::makeFakeLiteApi())); + LiteWalletOpenRequest req; + req.passphrase = "hunter2"; + EXPECT_TRUE(controller.beginOpenWalletAsync(req)); + drain(controller); + EXPECT_TRUE(controller.walletOpen()); + const auto s = controller.encryptionStatus(); + EXPECT_TRUE(s.encrypted); + EXPECT_FALSE(s.locked); // async open UNLOCKED it (M1 fixed) + } + + dragonx::test::g_liteFakeWalletExists = false; + dragonx::test::g_liteFakeEncrypted = false; + dragonx::test::g_liteFakeLocked = false; + dragonx::test::g_liteFakeDeadServerSubstr.clear(); + dragonx::test::g_liteFakeWarmupServerSubstr.clear(); +} + // M2: a parsed lite refresh bundle maps through to the app's WalletState (the last hop // the Balance/Receive/Transactions tabs read), with zatoshi->DRGX conversion, z/t address // split, transaction typing, confirmations, and sync progress. @@ -7500,6 +7764,7 @@ int main() testLiteWalletControllerM5Persistence(); testLiteWalletControllerEncryption(); testLiteWalletControllerCreateEncryptsWithPassphrase(); + testLiteWalletControllerAsyncAppliesPassphrase(); testLiteChainNameMigration(); testLiteRefreshModelAppliesToWalletState(); testLiteSendShowsRecipientFromOutgoing();