From 2fd68c55eeef5cfab8ce5eecd63b908516b23a52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Longoni?= Date: Mon, 15 Mar 2021 07:02:48 -0300 Subject: [PATCH 01/37] (feature) Wording improvements (#1999) * Fix reject and cancel button on Rejection modal * change buttons labels on modal (Settings/Policies) * fix Policies button align to the right * change wording cancel for reject --- .../Settings/ThresholdSettings/ChangeThreshold/index.tsx | 6 +++--- .../safe/components/Settings/ThresholdSettings/index.tsx | 2 +- .../Transactions/TxList/modals/ApproveTxModal.tsx | 4 ++-- .../components/Transactions/TxList/modals/RejectTxModal.tsx | 6 +++--- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/routes/safe/components/Settings/ThresholdSettings/ChangeThreshold/index.tsx b/src/routes/safe/components/Settings/ThresholdSettings/ChangeThreshold/index.tsx index 34ededcd..8f6a7455 100644 --- a/src/routes/safe/components/Settings/ThresholdSettings/ChangeThreshold/index.tsx +++ b/src/routes/safe/components/Settings/ThresholdSettings/ChangeThreshold/index.tsx @@ -199,8 +199,8 @@ export const ChangeThresholdModal = ({ )} - diff --git a/src/routes/safe/components/Settings/ThresholdSettings/index.tsx b/src/routes/safe/components/Settings/ThresholdSettings/index.tsx index bdac5ec3..2e9246f2 100644 --- a/src/routes/safe/components/Settings/ThresholdSettings/index.tsx +++ b/src/routes/safe/components/Settings/ThresholdSettings/index.tsx @@ -57,7 +57,7 @@ const ThresholdSettings = (): React.ReactElement => { onClick={toggleModal} variant="contained" > - Modify + Change )} diff --git a/src/routes/safe/components/Transactions/TxList/modals/ApproveTxModal.tsx b/src/routes/safe/components/Transactions/TxList/modals/ApproveTxModal.tsx index eca47c1d..687dea96 100644 --- a/src/routes/safe/components/Transactions/TxList/modals/ApproveTxModal.tsx +++ b/src/routes/safe/components/Transactions/TxList/modals/ApproveTxModal.tsx @@ -396,8 +396,8 @@ export const ApproveTxModal = ({ {/* Footer */} - diff --git a/src/components/Stepper/OpenPaper/index.tsx b/src/components/Stepper/OpenPaper/index.tsx index 4e19dfbc..d013ed93 100644 --- a/src/components/Stepper/OpenPaper/index.tsx +++ b/src/components/Stepper/OpenPaper/index.tsx @@ -7,7 +7,7 @@ import { lg } from 'src/theme/variables' const useStyles = makeStyles({ root: { - margin: '10px', + margin: '10px 0 10px 10px', maxWidth: '770px', boxShadow: '0 0 10px 0 rgba(33,48,77,0.10)', }, diff --git a/src/routes/open/components/ReviewInformation/index.tsx b/src/routes/open/components/ReviewInformation/index.tsx index 8e899bee..0a400112 100644 --- a/src/routes/open/components/ReviewInformation/index.tsx +++ b/src/routes/open/components/ReviewInformation/index.tsx @@ -128,7 +128,7 @@ const ReviewComponent = ({ values, form }: ReviewComponentProps): ReactElement = - + You're about to create a new Safe and will have to confirm a transaction with your currently connected wallet. The creation will cost approximately {gasCostFormatted} {nativeCoin.name}. The exact amount will be determined by your wallet. diff --git a/src/routes/open/components/SafeNameForm/index.tsx b/src/routes/open/components/SafeNameForm/index.tsx index 82ebd944..5d7e1837 100644 --- a/src/routes/open/components/SafeNameForm/index.tsx +++ b/src/routes/open/components/SafeNameForm/index.tsx @@ -1,5 +1,6 @@ import { createStyles, makeStyles } from '@material-ui/core/styles' import * as React from 'react' +import styled from 'styled-components' import OpenPaper from 'src/components/Stepper/OpenPaper' import Field from 'src/components/forms/Field' @@ -28,6 +29,12 @@ const styles = createStyles({ }, }) +const StyledField = styled(Field)` + &.MuiTextField-root { + width: 460px; + } +` + const useSafeNameStyles = makeStyles(styles) const SafeNameForm = ({ safeName }: { safeName: string }): React.ReactElement => { @@ -36,13 +43,13 @@ const SafeNameForm = ({ safeName }: { safeName: string }): React.ReactElement => return ( <> - + You are about to create a new Gnosis Safe wallet with one or more owners. First, let's give your new wallet a name. This name is only stored locally and will never be shared with Gnosis or any third parties. - /> - + By continuing you consent to the{' '} terms of use diff --git a/src/routes/open/components/SafeOwnersConfirmationsForm/index.tsx b/src/routes/open/components/SafeOwnersConfirmationsForm/index.tsx index a23bfb4b..0e87697e 100644 --- a/src/routes/open/components/SafeOwnersConfirmationsForm/index.tsx +++ b/src/routes/open/components/SafeOwnersConfirmationsForm/index.tsx @@ -5,6 +5,7 @@ import { makeStyles } from '@material-ui/core/styles' import CheckCircle from '@material-ui/icons/CheckCircle' import * as React from 'react' import { styles } from './style' +import styled from 'styled-components' import QRIcon from 'src/assets/icons/qrcode.svg' import trash from 'src/assets/icons/trash.svg' @@ -45,6 +46,10 @@ const { useState } = React export const ADD_OWNER_BUTTON = '+ Add another owner' +const StyledAddressInput = styled(AddressInput)` + width: 460px; +` + /** * Validates the whole OwnersForm, specially checks for non-repeated addresses * @@ -152,7 +157,7 @@ const SafeOwnersForm = (props): React.ReactElement => { return ( <> - + Your Safe will have one or more owners. We have prefilled the first owner with your connected wallet details, but you are free to change this to a different owner.
@@ -167,7 +172,7 @@ const SafeOwnersForm = (props): React.ReactElement => { rel="noreferrer" title="Learn about which Safe setup to use" > - + Learn about which Safe setup to use @@ -176,8 +181,8 @@ const SafeOwnersForm = (props): React.ReactElement => {
- NAME - ADDRESS + NAME + ADDRESS @@ -187,7 +192,7 @@ const SafeOwnersForm = (props): React.ReactElement => { return ( - + { testId={`create-safe-owner-name-field-${index}`} /> - - + { const newOwnerName = getNameFromAddressBook(addressBook, newOwnerAddress, { filterOnlyValidName: true, @@ -246,7 +251,7 @@ const SafeOwnersForm = (props): React.ReactElement => { @@ -256,7 +261,7 @@ const SafeOwnersForm = (props): React.ReactElement => { Any transaction requires the confirmation of:
- + (jg#ANeBuO z0-}U;?d5syH=cW6_jf;k?0)v_nK?Bx=e%d;=<4YxDmg3zngNGI09}+1PuQLM`uf4M zz1Y5Y<43z|#|Km~ek8`O|~ax$&O|`{2ZY&Yg|dKbJo5{My^u-9FsskMJKH9$a5v zAKw2xJ3CuBKbbMyZTq_T_vU8(_b=<)zo#yaz7x-wB7C-vkFbRqyN5^1#|MB!zvRJ= zx9i_UlEZwPtKKht^=hnCh!5R6IC}G@X=VS<;>EETE?}2PbgC;B$cO+W`!zK+u;6@l zj*ir_6G;*R7~}jm)_$lwjpclJr*(Iocz!WMJPmHETie=fpPzPpRisq#IH~a!cj`mS zifpFTu!o&Z--u@<@qUfXjcjp&n%5337idFek-PfW|QzU*)99v;X%iv~Upbg0bR-QDf$@BMmy zsF(ZroJc$&5?@V??*I8?U7CF*DrjQw_Z;!;`OFAMQi#&y@cf~kH)Egnh{QP}@#xQf z>GzrK?QQqEQsyYXgM&Sqln^eQ4=OEYb>jznOaOOWka zq(Hi);H{$*!ngUxuTvIJ64thNUN_YJ+1@Gq_W9@esbXf#mFR%2y+03YN@=5gYu3J* zCq; zADEZyo$O+&?=0adz^RctIBH+xYtR*6Hz&AFJy> z)*70c-ZVD!?Q9$!T(UVlIykOg{hCJT-TJfBeX;{g4!%~NMV9FQX7$^zqr+Qo%KYBF z`h9r#b8}O#Fbz|c+cEMfyd)2h6j0w-MVkwi*kZF4hCG*(+$ToMcf005Uo zRz_HamP3HU<@Z(f5FZC8PnQ6MqswjgJF47UZ$EG&+?`dquge)p8u@6txVdBQ`?;9i zH#T>=@9CuE%&o45xEi7y;_c(@65xOc@%FmouN|2+h6pZ~DE<1haAaKxSN zdP{^j_(({JOa3*ae^)dz`u|t;_Wn<8{{T~$|MvI)$=Kigo{x)!sf+*JKtCs!%j!IT zHRYqM>F44QaM#cL?p?2c&mz|CZoplCx4S+FX(@4OgrJdwllz^&N`(F#m_y+#aYMit~cWEQ7gOu&wbJQANl=@*7<+z zi@f&#NGoyK8HvBv_8)8g@1jcq`s?*Sl6(2%KeF#~=ThMPE+rdCyf{BQJvlx)JlNm+ zv-_9I{@VPxvA*_Wb!B+R|8>g;I$ z@V@Qc+t!xmH%*NVuj}h-YhG1Xy{xP#FDor6E-Ea@&wKGaHz)g9R_4{-V zNr?&Zaj`McQIQe2@JA0HgoWO}7ZMy47~t>c>vQ*xx0k1fyW4G77iT9&2m4!gwl>x` zZ&+Qov@ka_HNjppHZnBO*Td-QXlrR|pi$~-s#jH%l@t}^mn; z6aXaxPRThiwfP;9PHBuv~j(ZO~R}Yb`USWBn%lfGMHEWGveljv#FR;Hiz)|D5>ds|Cl>9+iz!s;ZK2}TqKq5k*&tlm+YZIF{|4UW{S zNhID1ld<}>)uclCTJ3$?tW9rg(V>mXo72ZWS;InFe!*FQYk84&W zrj~{gu=j6FBu_^t?QdYc7#Cw~N1X@nFpQN8<*ORj(g(5-(;08HZvcPm|z8T z;*1yP&q}rK8Peu4p*?Z=NX~1%K&6i1uUJkqlwV&?$La{JWSCp(mB=vS7ux+t=f7Wv z(jM8bWH~=|Uw!7Lw%$W-b0YZ6=AhaoQISz=-5@(;wc^JM;}cV~Jc5pt>K@GzRzEFR z`j|2=Nq$4{N&JcZH9g!cqhXSgL#2uM63fn7S$@pT6&W>W@J)5Qj}<0RMvm6<^4eY^ z+lI`DIbqcq9mMFQLo4{KysDklV72fA|2+-%49@b3>!oq=&O!tVkzWli_ZNHZ)r5*> z$VX(|yTOiGV@b`W8J@pe)=FRgZryD9`TOm5>r0f>n+eaYwxiX=yYkOiM#a);-c8&n zffU3PnYGyPpz6*Pd5j;6f0EBoeIc!n?nXxMOpz8V<%iNBMD zse@|%`?5Z3=3)o*w7j4fcZB_541o2*( zfy5B7T`ekfj6}>uFd_)eA>#UR+(6AgIB^>oyYUjkzcI!g!GSvZ0jw>6YD7Lhke@i4 z6FOezKL8EcSg{9Cc46*3jQWwcMUP_8V=vCN4yp;fzg+dU{Ehwt8F!W6`F|b~Iy$?O zmp6JcLku^ed>@u~TYmlyLsz89hfPf8WEn_`yVvj@wrqVpMRnKO&K~KR-cpP0`_mR^ zkU00X?KFw<1-`hC_HVDqpIR0o=v^q+UIfo&XW%`$jYB`snw&|TR04o%7uU0<|hc^NQe)2C@Ce8 zA^osOlBXu!bVrfQ@7#1GV znw-MPo?aHA>gavi_fW{G;$bRyRo znt~h=L8pub3*-WULA=9E>|+BCO;~8jPY3Co&Y|*XfPl1YPpM5LQ9Bobq!Plk(|O?E zs%&=Uu0_G!58Tp%?%<<`MKt{>^sY_pEHRG|ZqFa2jqIS{3{EVa0vUw1ktct0*LjUD z=)M!4_~5hXTxhhPX}s2Q2~fGfFRFft1V9xFisv=U@3=Z6qZ{@RT^J7yyIBo$!+;mE zjc!0WkMze+SF;u4fiS^QU_RSkzTksoG2!k{)qIT_`hie0^p6My!QKS9^)emTZ2Fg$ z7L^K|*Th|6k%0|`KolQ`#>*LIDp#XO@Venm=6Xw7CKSyLtt^eZ5tG>j#=MXc52H(lAKj z_1kp;%Z_}YmEm=!EJU~ZQ0)~R8;lIn-7d5=?-lK0 zV|TV($UK`B;eDuNuqvq8ZCKf(W}M>)qF1Wb{mjYS4p@RZHT5#Y?8c~7Wo_=Q)f&R*SW@a6s?v)%(xsd5piKVoC#|v{=&gC%v8u0=&6~0NAH`> znBouPb|T4kkP4QPwY{TS)%9aB(Z==Wbv6wG0V6M0pRqFi{e74e>C@<$6ty@MgdWpW z|Kx#}v%uRc`&Iy|_bdQ^LIH_c|0Pry{^ez`6o16oP<&|k)!tZ3#nfOFgpa(g)XTGL|YD44TO1G zQ*xkH7urdN>`BD%=9HHG9VAo$-yWN1CDlZu_^z(EPcd2&L&mH!`F%2An(1)ptuq@k zWrs*r*{p>JzJnf&#$K5k6Lq4Q0z)DIyqGA_ZCsb(GP)uW8BP_RoUhd+9;iMQ&d0z_I!E9gee=VZplY+;op`VMInDN| z4-aWH00pj3-2Xj~%a_a2B84FaKfYSA{AAlaT5*0jZ)md`DL}*U%_kBbH{5{HIai~x zkEiu1>c|?syPEilu6}UhTgJoI9!iNm5?KeIWC{1}Cn$32xf@ax;R;9HU$=_}<=0>I zo&PS&{N3Q>I&QKo0=!UOFn=UB&?3@FgnMmA3`Hq^&8}(tvqk-2KJ4N4=dCJr7WiY; z?fjS8ir&m@`4N=i2r9vRP-MD_92QbdAg7dM`>6%fE%Ey~b?>*pgDBkxz`oG8I16Yk z(0Ds!9!%-5LaQ1MVL_4Jng9TNC@=(YR$VxFG~DSh{Fj0b9rV7DF2~neup~j1KMp7~ z%>-=+0ST%iP2juXkSqtQ!OVv={r994(K66S1qvJ{cnARHEGY?z!duK^NvKh#QR6^; z6i_^dRX`y$>=0{c6@=>xXjQA-5E;;RM7y4p{t)@S(HRp%$wUm zbTL6D&Fqx55BUL}wo^3Y!~^wPr7Tn~F+azHF=a8!JRvbwkzJ{A6HRfjA8`{*@$pgt zWQ!5Q0&(3t4|AqtnENSx6=UtAjTL-j?`5fkPREuU#WuW&B7$+!m^elZ?uSm&QXRnD zE!wI)+GZ{K26K!Pl>J>uOwM7V5sQAORnpz^9%Y~qO(*qt)e_) z9=0h4RW2x%t+=#8L!R}=FlB`v_Fub|iBq>Qt!)R**TPN9Xn4y#qMRbiU|``m)^V$N zQMh9J8(I-2vU#7dJu7ub(&*g&YaBq*gDK#0hRQYpkc*%o29tu0l*%S46!GCjqj>5G zIy)H{I^npS{hln?zV^X4N&C+v4>f}Vzv}{4VvWqZnItPTs0+-+cwp!y zAUS}XdfRqtEo#QEW!cVT6))<~?toyDJnbWsV)?(yY^s-NSNTocDR?`M*sjxXv+q0KPhIwEu2Ui;+W25^J16cbOfK}WW6h5Tdw=ZbtF z9q8V`2EUU+}_7-Ry1$pQ}wwq8}O4j|vF`bdC(!N&{KDH@G|2 zV<8(E;z+cNa`Dc3!gnBbL@%?-EThX>(a#yW?r<(yp#?Q>`Q{tjmPkldYAC;QcGa&Jw%AXo zFq9ShQ1m@Cq;1sue_vulZ$y%0_g7}ETlZ^Cy8~g zRHt<4g!JGICPOUEC`i}K8Ny|TTE>Db5-R8JCxlnhS#OlntNHdO9#UkcDXm!?-UlxHjag@d@D+q>h`;u34;tlZ<`#f8D10LvLg|i zvv1oJ>+~K8SA8(x*lPu?zQqcsk~RFHtU_~C*Yk3lz2ABXAIpIWn=cKfr&Z09mdQh2&5M9;WeU@bXu$}g7eHQ zi2DBFE!7eer{ZgPE%4cti9MIKZMRc#Q&7x{e*{k+1b>fky}`IGAyOr%6xRJ1Bu!-o zyaRjURQLK}-JS1Pso=C$Oju9BcbT&Vl34`H$V*PPML@Ca2P>Q2l<#0ZNtRjARS#zE zfX+@=j=p=QKwD8kRR;bBX+9}0zEIJ=8}E9a7C61` zaNZa0Pn$5Iy}^b8Hd0NFC2NX4!zP%u^h^@WuWtD5m*MnOu~szwz7w)|5!HJ1Jer_Kpf$f$k;_GH07-5VPq zm|taJ7&Y1zsf@gmS@^^zhK!I1j7?FA2^juyh1YroyQ={1)vMchzhnmE3^nnS;khjamS}ioAGweG_sVM^b zoW)8-E5uamNw_vfm7_h1dd7#^PIC12cdDs&8l)r7i2;2$3{>Vt5mG&6W_DHOJFUSf zkH*d(8|+}lBiD~+nGOrfiuy-H;W8;raRMIZ;!M9ORRw zUz{<>;b4C_FLH1_`O1A?1ry}god@i1v0p8~RAmpoF&8qzK}-j~zU@9|==Gs$L(M;g zGoBkQ)Ns!5pX&lsfBn>*ngc*DF8`;6ae#`BaV(!t+;F=d;9lWFn}6~ zdq_Od4DTGU=+OpuYZ72(&X4S6Att#^Zn5INuH?rw2PgkT)gg z$4HxNlx9?HI|=z_v36RifiCHNOPV?NM%Rwp@e?fZ&ga|wS}=&+3`0oAwlexTr@GEG z6R_<#qG>LN7W&_b%DbE>aRAHT6GbHFU@6G|fFqs>on`)9Ku5uN^cma??8R|pq10iD zSRPGh3MJ_(ML4o19V&6>-I_$E*i)jN^jVoyaeo@fc0d7^@+m2*+m-qHCE8t)T>IdPE#y$^MBdQr7>&S zO~8a@6PJe{57H|I8&lb1aCRg_^eUSlX_gq7B<)2NN0-f;7GIq z^oBHivUqB}fKi;o>%h+_5&3W_u=cuk5KQ`R9Cu^{bx@fOKq0^kZ+P=rDVvN#Hk>&6faof^id z2m{@2FYHEADV8xlVKhCuhF*_D^$12DF{h(EB81@g`8e14qub^(*i(u5KTM4UaeBPY z;y_`O9PjULE{4kS_ju#MB#{4eouvSXf35Q-A0ZEogVLX{AB|Xc#=>}nMUPT4du(XQ zDFU*7E@&pcIPSfoI-ouNA_JOqMkux zIBw!^=V-;HTShHBFAvy!mcvnP+#}eqFXMS#siM@MHz#Rg7TJg%<>~Wu$M{HiyJc#5= zMx@%f`5l{uqddxYtV4fwI6mfZQY{AHK;BE{8W`si*2q&pyDmdZ2HC7QLku%c9sal$ z8LQu*0br4_ih3c*CmuwgdKKmih@~QR_{1*vu$xj2q;1LpVpkk};bEX54M9?q9FJJ3 z4rmQi*?akh(@}W@0RUV>;h*#&yA1QzSG9d<0lzH9U(kJ97&W3g0QSaVji2|Av9Ff* zFR1~~SU7Qx9e>5Tmwn<}(tq!2;iB8kN61<_l$R0TR z{X#7|h#NK)n?|9-kZDBRW(`-VwsMGse3Q%IjOGMHLs+gC12iCJCoYt-jMNrIp>zcT zumE3gULj9-vc>qlI_{{?PHh63QUDcHETi+-5Z zU9sSvURXw^Qd6>D{ItN=V4u*pV3c;ZX#k9%(Bnq(sUO{fhk$aE*ERMl1@r}u4JBir zK^9~Ozh3g6?7mk4Wun}UC0zT4c%4qhvj(-68_NBsj;^=(&9{M{y?=;FM{q_KigED* zNL@Sg;otb3*GJmbSwRAt5RGlerDiBzg$UVhEBYBBj=!!moLW#VpUiV+@Z;>82QHJg zG9L&!CH=oA;8Xw`a#B^5((NL>qk=1ar2(|z1uJe zS_{{F)b2Sr-_TTu!SW$B@ zuYm#fl9~{`r)7ArjVGnuK+}GlmX$se0~q8hIEr@X6zm`aNx@{Tvm|U~HL2tq#=_G} zNU-4?6eKfN*fz``2%z9e(>1_O6!R*2y;zU{%4~=zb2F~`6LHHvhLwa1z|uMbxAv(M z)S`k*Yn8=xwv!`^{CKC~FF?w8l9$zL5D2iAO<1J5k$VsHs_v7rLk*~{R{K@SFa4?- zYa8ixH9&sHM(v>fcqa=pLIWTY7vOJ)`mZ>ZU+zeP0L6drNSN(g;*Y?gR!`CYLv zW^U1t-+9%K=y^Oi`X!xuqu4c-H(e5$Yf|VL6DFqX7PWC?{th-RsRep=aP9zE%rLLf zQ?XE7f%H!R z)uERRK<$BHQpFBje;i8k9xmte?L9ZG**mMa8^s?V^|uxvFekUR%p@rfS}n#GX<}0? zc+6*fn2n;Ot{C%&JXsx1ng8B+pgObHW%uUBQ{wXeH3!=3G0-V&WTTVqqdwH{-{(xI zrzMQ6cKg9M5I9R}w`DUA(Ec6+E|diM%z0oGLZ-z$4CrMdHt&z9?TrRxqx2Zfzo-Xm zeJ00o?Z?8R@yH{XgsQEx2Ab7TJ5rnLA|OkLQ=H*zv`AX8laZW8VWB1l%Fv;{F4#E2 z$$EiVP=!xs-Mk@5<-YO+)SUL9n;30r62kd)JIzvSJHFy1qNN8F3=O1dotUsi(9S+gY zZh8rI{K&Zq0LCP!;OfX|IYrmA>8KY;|#69}_sq3Xx8R&p$Q4@x~N$%N?y9o5(5l^>T z`;JH|9_cVa-@*@xltPw6iiAa-L}s~|i6i(TCJAQXdi5AztpBM0Ogqk}F0;uFNxdpe zZpJTRCw1_yvXHIWf@q(`VQ;px|b_`T0JZPsQYocyX1BpW3i^pb*(dwzDs=yBadYgY)NuwVs`c zwp;wgSADN{HeI=a$fzG>o_~^Xi@Xi+RSQ6K373~EVxv&sCW2xgiaj~#rE#r5I+GE7;^qS< zC7YhO(_8WrNW;DNvJHac;>_H?K#PsFRGtZ18GIsVVB*q;xidL+%N^uuUd-)lQ20(F zX4JS#Cv5GT0^BG0$T0l3y&?TLh~7I%$2k{bcma^aHbAm0w>s%M%z+K=nj4?qiYK88 z9%@O!8y7D)qLZ*PSz1zraIzaO#->QPpxp@z+3{0G!URK-At35y8s~Noy=wkzuEMSh z9x`x-?5qcMu%bg`qM9@eQgbS}W5hV$fAc2oe$}`>xEjjA8J~HUB(tX7JC+%Jx7xyl#s9R=Q)MP(`}v&k5Wl8wH0VGxrd3YIE_jlTakfq2#!iwWG#+<%#!C$vty@Z zw|b6-GIa;Gab}nr2(al^``!Ka8rT4!9H}98WzX!oUly#rYm5%+N2b5s5zg_=mZ9Tg zFe`+VncBdhI)ifo4`K)Cuyj#g-RpCP2M>KA2S%@gUWT{zM*_IcadX;We`iz|YK@IW zQvoMS&?d98kHD}5wlDukT}3-tazl9Oda3M6Fs8k6ix!RAyxO(H70w?7=#zYyp$)qg z{6PsO|4@#!OXX1er*d2#Vj<^9b3sC$W^@xwKNsU$pb}jJw4KDQsM&VR|yfT`}Y?Y7IT6PW^FmksBGT4Q{;!O=ohU{bS{Xf(yIixczH1 z^-qmOc4viaGxeWg+&0|+OLh!}pc@B+351`zkF3x4rG07J-;?Uzu4in1GSyy8^KDIW z0=@8_4U+Tw`Px&7nk)MVQTK3`I+Sdp0QaB1*9gKR>kA>)=DT_Aw8A_afoui7PA@JB zG2M|1dAHtlNK+=kI#x&0=!OG-_Pl*_2dBW#o%*MS@heX5F~)u7&(WWlhm;3w_S1Xn zTYW;VM0Lfs0CeD7Qfyv-ONG)Jq>OfRQt@5Pqr7p7B$*TA`pN*QqlqR*H~GVsWd&}18lfg`!}o1**jQ3W zJ|2yy^Hy*qXw<6cF)466C55=WJ(|qe3gy+9lzZy*n05flfGumNFE)`vH54Y&T}1ZX z%AJSOg)HzjIEKntxX$tt_r9kmZJdvz;Pe!{V5CjknuWeCp7aVxA(|O|i`DYTAqC9X zL>8kTu||*zG?&$UVLR4fT0-2rv1;8G5HzPFRPAaV0yJR~$Z@C>vmdMANPZXmg<-SZrLzK%{nQWt*1Q;R=^%4*(#CN$OD9?Q~)i zsLt&PrDGSbW5~t=J{mMh;0l1WoEetcOqS&c5+KCOfmDsmo#WRdkjXYPdf&Y#(cIY0 zCi_u|>dW3ovY+vPI`Q`Wgt{ghxtE@l#bY0bPUnz+#wgW2VL*GBN}l(U)H6@2XaJ#o z%x2K*wF7Zk5XMm+5X}b)3rO7=A_h{w7=8jb&q>y9>p%hS{4m11zoPM%waeGmCPXTk z!s>#iymMmL)VG{1d?x22(9sN*vJ0OaFiwx_Q9@^t-Bn&I5maR6hu5!66m>~+{cQ&t4@dh+;{f`jD*atuq!cGjo|?<4v(iGA=m&5-Ugd zu2QBmN$Yy6cATV~XLp9Y_sRy}uU-`aS5AG#X}s`QvL|I0PF528rbySR*JVxS#|InY zga^WBAV0PkLq;2wPWVP838DI$n!61^SAyaGpWu zOP0nk%G)dL;zW;$%fpa{XLOs_%4Rg=Pv=~&KU#O%XxldeLsNFDdtm=-B)&$MI463A zC%im$P@q`gx~hh_HC3k%5q9xm3;ctS5~tpPS~-Px_s~f0k#)gobm+tA@ziPxB|I(|2-Dvy}`olpMw$X^RCYtcl;(+&@Iiik@VG7ky^AqI^C2!^_&cm zsGGtNql`tyJyMPg!BHF=Pc9CSPPD&Ahfha9?32Wi4(&|UvWPh~G8W?Zk5p|(TKqav zF0p;;w)n#oHZU?D>8W}_o=koS216^)mokyD)$E&qkEoJuE})Jn z_cYPuEb;NI;CRGT04=#IvwgmF#hB#cdI?J1HM@n0+-ajD71|G@J9$^pH)HuCL53=Qpp_21e|%c;Y$Hu70K388s& zE^hm?R;r+xFmFYDiGG|UNqvBxKi^Oe{PyR$8?YZnk1rB-I4VbfT*I}*#LP`09crN1 z#0Ibu0QuXsua;VAj?n&P2H2968i(xWzV{aRCA0wPYr4k>Da%#2)HCepi5cw`q|+k({JiP8kP7^jytKl7_Mz zQ#;3?0d8ej2{4CmyBdQjOFQ-a8U0uF{_W4~yriR=qo7O{%SE+Ie5;|^HYd+uZBo2@ zjT&Sk2!m0Ir9l7!m)KkNU7Z|EPRi|Ph zynYs#IBA8{Bn#|90aPE3Cs0cbwO@J62~dE6q#x+QSWw#p?OcZYg%A=bLklvrJz5h^ z=eO*GVrJxK0#F9%xMOK8gcv%gUI7TG6hJ@fX)$;hmxKCg#H3As^~E#*P=*-o^4vub zAvX6()^)Jr64MJ>DTbHFV-~%}X%nN1<+KdVV%FsW^-u3$SZ#wOerQ`l6Cv!ZU49O^TP zZnBcBTYJ$(WH)Xu58DgCQHG032&QtT;yH zcbpJbn4iPUT&u}7IqfY8n)(J=#YnpRVifqttK{qfhHQNe<aAh$!$0WYgG%GWF?2JQ&vqY$0W zPn^qF-`RJ6uycAkbCs74oZ1P4I7UOxx|B5QulsFwr6o6OW2nq-8Vk#(W%L#oiP8vJ z7)2FJNLTZDp4`ksD77{Fbck8A%jA3V@A-M`ESsWp+}yn~sN4y|X3GNr>$Fa7?f zu~oNUWs~8)a>ibEGb&E+XKX1xS_r1|%X4N}C)@+vltsdP35&5KebjvA93Pvy;!_}-H zNT^sImRtEqJfh<{8($}&aPX(c*$?(e2hbh!^1)1JpTFH6dCY$4sxIz*uM)-%XvgZ7 zt25W1uh6~Y2+*EmW%f(;i>ldrQ0*hCIQLE}YxuE>0P$e6#>pWjY88NKqa|h2AZD$8 z+UI=dSlZSZ#~r?IF&o?R*T%o8gd&4^g1__h!{1C0fX9Ykii z`ML)hV3LO?YeTi`Umz-I0Ms^p*VFBxq$^m=Y6u`9jHJCMcm+A}F%vn%KTNOe@yrpZ zUVj4?Mmh<&jLc*H&{Aoe9u+*78V(`&VIS)f?h_dtU=i{qN?BspUOBsIWT|BUwJ3~_ zQeHsNns`SnQv|*&_n~Z7>NRa$0=o)~XdbEJQJyNKl~2b|fEMhLyM8B>BS4oKS|^<5 zO)Q=mq*AyyGksg=*@=*|DvDPFtaR`v93h`)3Gp^ca_wT zwBTP7Cn`zb?L@P5sWUK`59|5(TEgBlth&}>mhZO7^uDF46Gi@Q zO~%$(GTQxCIuzHPu)`6(R*w9tx*TR zi*8rv=3G@Wh|r7d7K(xGZFOpg&no(qTvNQiF0=)1S2tz~YK06N4^V&^cGEy*by|%_(gaiDT^*Ke|RJb|dU~)S6c3#?D3o1l` zzNG^do9|OU?~kALkcBR0CpjvT5%Y1E0e^?V>qfE0qOCpEN>krZt@Nq6AVRo=H6Y9| z*HAI5Di?H><7GubF(n%ti^wF0jpIzpHJ~9kh_UEv@9Z67x@WKiOI@!W5D(w6K(@pA{`GVY(&r6V0;!l{tfD|@ z78KbeY;Tx`?Ad`%XaM<-1&!0Q^zZ6q&~78h0$P9os!es}aikKLwGAV+o&M^8x`?F% z#6GvmjmEGE6{SLsS?tm<9sy)%plDRKTd5SN#Ead#1dpGX0j)fO@>^jg#uyq3Bh)Ws z__HYXO=Cp>O+r`+d#rCXgKJhtJVl5XuKh8oUZfUJ;Oq>+lWIVSpm5XZcm!%RdmMq* zKBLFzNR}U6`ly66y5MQA;$k!}_QoD-s@Umqrsnk$vm;Sss*sIrBFrVrou8UNHKlsn z4OT$X#^i-Ig?95vOhm<)#(GgKBd$Xj3sbKJ&r!6iCgOWNCFm1^^fepB11jj#=oltZ z(of}>Lr8xW#i(4sJqgq=C<6Kvh*jX224glCmJlV*WyTNl-S#!;C0{ET*AwaKE~9sy zIZmXV5y}KAT0SyIW^jpBNlwdhXR&@~hXLUZ8&L)&AwW9;z6``#6F8nUhTej3#3sGW zX?tOKQ@CT7vHC3pedgY({cxbsV~1NE_nFL?I}XmQEToGFu zMQa)G{zcQ=8fttcEoh>Rb~!>l$8%yZq@7sQyBB`wsJe2IYXl=>=?i|T<5L>Yx91@# zODahB9K2^k$nU;-qrxv$BWy;v;Tg)Z9EE2+ViDa})3$>5lfrq6FwU+JWZqusj|v7O zAzKm9TsYHV3bHvIA)uuD=St_*eVi_6M>?+f{Z!MnYFF)_?TEJ*9U?6dFG?hR9LQu$ z8jt3672E;n^IGAJqbd;!?OIeW>=7bg?`e>Ge6e>aU)79(Ub_XlN46szr9!u;b(u91 z!N`VyeI@@HFkv6nunCc4Hz9>vFF^RWQ^SMH?JX%gkb!X)lAlXnCEPi6(B#m{z7<{0 z8)s7JR;t-3@5Vrr^}O|If@Y>#@}?z&kKeHj;EL{2Wo0U|A8R%SAyO*H z>n|XKzzblPzGTpmd_0;ec(n*nXvNvEtV?J;y0~aGn5=xIBKO-PDZ0+7+$qznr_(V!hr=`b>ft^MAS1y(yp(AL^C6vxhhGKHy6?beJsWzcY z!-OS&Hic4K5=;sy5X7hFu?nqb*&8>y#8S5Eu4YlYSA0^ZczQTONkYQBnvGS4w4>1S z@jq}vPmVw5k%3zA!mk<4NIKdKPG9^#@a%(9bP;&H^;?6Fp`~= z(t4{uuz8>vpH%%w?4SgeOSs3o zHvSkvQU(~lA@P{aRaZrraOTAqW<{A__?${Sm-y^DjWW%m$BFe@CGV-roPj$=J!(&w z{Y^vy0;q^Lw)zKUiJN=6_ZPDXgNr1r)4tzPUegYEx?YtL%`8UMoRx7`#%Hpa{PrJK zMbSZIcV72+=FHm=&8Vr|P;(rW7`B_K>H^39^m&sk&Kzy07)(QE8_`J`Ef2G*-V}zT z?A$pRk-s#|i_D9Zs=>dchiR0qgk+KSfOpYaW?wtlZt1Mb8>f=|NO=S@5{lM-ODWr@ zPKsShd7gMnYjRM6>mIB3CrvZztEoyjEfHjebjmT)A3hrB@&h8K9YwnOs+)vHRjEb) zIbo$Tf<#=oRJtFtdp%9sHqM@?WUA8{^J;BvNNU~e35jQbMh4^`X;zQ^A2xt^24C*+ zv5$&{{^h<*Ev&{}Fi;Fl{^a7ZgQ!??=t(5kKk^DlE1y}$kVYa2jqC=QQBF1BmR9UKK$E~s_g zXK%R4?6mta2hX#_5YoN7$fi%TjN&U?QGZduqkhF|DKGoW=|uBS0v}BT3+e6uMc7;T zHT{2o|FDf_u#J(@-JyVh;x^bwfzd4~k`mH_bk`_J3F(p$P!yC70Uad>~Z0V0+x3EGFV-W)NC!aPi7k8*_uj3WTPDN(I zQp<#}3ZPk+G8+!&mu1$#wS#5!-Ks%0Ehth1>-DKz7y>5RpufgNnO4D*<=3Xc7}|hU zt4ESLl`uXcnLVBys%_yE-$~}OgowYGc_#!6Gq1p2PC(MZv&@4Ou18X0##BK{8}>Q? z1?Rh%FG3>A$Qxy_VuZ{iJkc)7O_3NWp6@gd@wG7W~7EREpDVXhUWuqUoq z>2R31DVd~wrKq+6n)K#oJoeW_#g3LcS6xjTb8`)Rq=PF)!{B5kPpj+epR(HeK|rk` z#8XC}B7NstbRLJ0`Lj5fJX`%e77F#cOXesDI!1_Yxv7%RePWlSr_{GIPgQ>NWQmb`~P@%eEn4)^m>bo}G` zK$W0QU*v-aszoO$OS+^y`0=y zz%&HAW4`Dc3i5mW&*crTK0$KTs%20z1_1{GYe|KUa zPOZBZI1!!mnBMM9ZumNK_|>MwdsRo(g!H&jOPPEs_VCgeFJ*^k8O%#N24MPRVwb@@ zlVK{P2{cB_E~=qZNvjcFpR*OZIf1W5{`OR^ewpc+9Eet&Q>|P96zuFF2(r-xJYXyv z1=!AlU(Wi_2nIZQCE#RsJT_U|3R)Ss-YiR>B$v2a^n&O z9O0crVs@#9+Fca+w_{X*sOUHy0usUZoeJRg1@R?{F=}HI;1H>ADngsZ)7?7CbE}nc z;yh2Z|9myzvcN)H^eEe=*W!?Mnk?uCg)f)3PR;yWd^5 z4|nW%qcOkFjR!t?H0aW;(sEm(mlvKz05iC3nAb-K&S2LbKYY%=I_lAHR^kc!jSQyj zxqRL2t(`4eob=_kT4wWW4ggJJ7Ve(!&(F4(1?77(c@v-g{`3mJ%vffG63@A={AKxz zR{|uMJ8y)DX#7B@5S^y3JakG4rC=cD|Lpm>*ADEsqG-|6J@zj462*Th6J~Z?Pu0)% zpS z$qI-|x~Kj2g6T(TY*s9bdOfF!R=Hr^bzH9B(*OwBIe0L^`Vq@dbQEARPq2b|d!`o| z5NQP|%`H^Pq(-E>+xapI6KmWtSLv;4D#Q7|_oHh#SLd)jbB~Ml2EPQ`sxh3HJqbMG zny!)R8TA)YGxm!2@RMaeD?y}|QHkn7E1#$7&@t<7wajo^+k?~Z`+M1V7f5*PQ!nK> zn}fm&bfw{!Ots~1;({&RKhg<{2wc5p4YG%K=85*7OD1yZBfRPFZkDyg!2+Fj2|Q&q zUxf{MRJR%x8%QKy`V~l9;2WaL_ag+O>w9%h5buk&-3>ac1JAN5?yIaCsVbN#y{P_^C+`{8^q$&Zp=lYYm1`UkDNBZA= z%Xim^UU4hc+c&Q2k2pk>J}H%19$`cBWQBr*&YrPBcxmJ zHBLLv<{)K?n1gCVG!8a2?MGzWGx$oyO!>J#-tDEi-Nmdd4~pRB5@h(fQhq<;LHAtpE^Z0>sV03<1Ld`5^^MPt*p(CAZd700L{{}&qkwPxwY>gPJCt?mv z`ig&>&CIUdLUg^4^KW4#kjw|jEZxZc8__0-ykI=thBTm^PCNP0l@%oBl9ya?^qpN- zZ*ug{yfUXTEfJlEX6<3?g2%z@4_W+1%ee!e#<4laN$;{e_!)r8`gafbXMWKCj~+mE zaq;}H`4cjs`|@Nz!Lz*AhMKg!7?sPDN~I;P70!87j^fx6el}w;zUD+axi_l{hK_m> z3_`FiX1@N;;%nBl%*n8dQKy&v=U1)?=ftwy6gY2iowCQ5TQC`6UN--_C1lk|-xXOo z(%WD;!*XD;6NyxCrwl!;U+ra)y+-)Cbmsm0%?&V_eLmlzz5bhrua8V6@qgX`>sg%9 zDZx#{;eK7G6ulxkeI;>>=K7-8!CrR-4!e?vKd4ou0ho-E@R2enkvPZ%cRlvYTP53^ zmfIK2k1H91E-6KM?;T@Zfky1z*(xV?F%4yF->N-s$6Lzn!HDud&&xQO5>)IpM2oX) z*J;J_%pZoAy^g?8#-qM{SVf$$k16t?&LG!uq&Hg*q6LTww9uyqC*stUJ^hCfs@k<5 zZ$teQa5N)jl`9%D3MM$Uu2H#B1Q#cPjGHw{>63 z-v}nyWb#)jSEk0f<0N=PVzJQn;Rl)kxO8ph6wWAEO~; zw22B9PHq5K=TQ#e9JB&o5V%muBG8@`{KQ2bfD6v~NCHmHjMRxCL z2&zFqH*#t~U1Q?)5=-A#vxi;$(REX8t94*vqIpws7Yj*AfqPox)F-v9+OM^_vMwW?_XHVr2!TO|qC3CtIAC=ZQQJN0QLUcG!o!%aH^v#x`_dYj&03zE~> z$AFB!ySzn;=`bgKQok8QNetKQCphGa(vjc3_HwmCrqa_hHWv-#H7%J5EC~y|V>dQ-}W58sqDr?sj z!`FwPWa_y;RMO9!hbJoeQUVaaeg;2Hoh3uU3~yKZOn<1E4Mm;1R6cm505a2}uQkxC zE3LOpvNYH6_k*g>-?*n=Ny#K?UUAkt^dR$M2=O_aDsj9?=W(VN=ccXzY6j^e3<(oN z^195?5LtxziD{I2o^`yLscMyFJJB57N@ej^sh>AWdIg24O(oq9dMeDPC;IQr|2)yHe_A2|2=4(l=uWlY8c0e*7mxIVHwtEv1*A%Hay)vcxrh|^iU$P3- z6Fw5nXa#e=my+y#$5Z-urrhtp==!6CiXs#AiMFE%%^>Av2d^&S(VKv{c4cq9my0w49bgf%9flS2!-5^1S5^r|GB_u|8Vxa%gZ7 zGKT9qk7lQ+yfI1ugYCBkgWKUgJF@16aecn4j?&`H{!g7G8PJ~t6B73{G+0!ap{cA> zioNcm*x3q8A=n_3j6HlO1NPYz{ymfWE&;A{1J3ppF|HuUlwm?!`3b3Oqz)0J;7*uF zAkQD9s%gYq!eG>a&^0HqE>RH;K!B4I2C3gJqIQ*U22RGR*fMZMZm04`8Jlt_guPjT zqT(KofuL?@kSpp|oo=dLoi7+p`!fZ!CX6q%7YcFDC5{qZoPuyK)mSmjSse}~z)3QV zXDk6Ha1+1t2h=SThesJvz}ZAkj4fran%ajutK_H&VGA$qeQ4YEi{&>g(kPzAY8F|z7RuxQp`nyxtFH z1BI`SK#55j$AU8WW z&%JZ$c`&`&9l7YFFZH(O>Cy)SCr>zF^`Jq;*O+RT7Liir+#abBZj1>_v>n< zROJl>+lG7s1Gy4x4w$(zfvnLD48oR14wK_XBm)H_RKZ-{P9iL^UtT2}T4bNW8|aHe ziu*sNeCg!;ykL^tq%~5;*89PAT!U*%L^D~*kSA{abR_JjoTK{E6v zd){ArIheREC#s(RKo=0LXcjeR%L*F+sxl6J-jj7tf@F4g6i@a|m2i9k)SEEQp`t1iEUKwe%9s-mYFCmJXe%%_pk5pR zU;NWwZq{|7BZvKW!_C7|=Rx-??fhge!{dqFvw6NDhm=ISjiyPT;4)uttIuVHUDJ>)bmH=_0 zxbI!ppN`@&p_=#sjK3iHt^q53z!Z`JDK!XFW9x|RrA(0TB_&`i-X2NLcBeNm9y*yO4LqUlT1)yjdDNXnNRi*puXk@9{>X;~ zQeyf%4SEPG@7mMMJmwz^I!a`A5`9iTaJ>rE6}_(?O2>u*LgeQV`uR6lyTr$=r}&uy zOlo@SxrCVCvWwM9_L4HS#r~k|!GTDiTkZ~;pVMUEHKQ>|Du|hNb*f&-bRruJ4<~$5 zJpZW+3FUieqe;(u?f4twBaXsHH{hA+TuaPvk7o!U20}{7+$}ftj_=U>~U zEx_eXe?N2AyB2)(j4DO{U1e~#i=Hce6}Ku*Uj3SvuFR?aXhp-H>X0Nj5eGIm{^cI# zCav1y`4i{DK0W(RmG@Hx#F$2^M>P9ENBeuE`bSMUlsfM;c){LlLSRJNcb}1x?b_gV z!3<7|uCS@{fqWrdEEcY0x`PtQizntwMRujsP?`Mjg}6iuw8R39`&%7P60F1(p$FV zp;GuM7To6yE5>7>hVJL1wH4ZVqS|j3lb}y0BCcO>(QO+1=*2o+UfizBs+q~_Ounq# zzh}=Hqr)H__AbB$b>)}=eo~_`*%$6goMo$ZDl>7m4LNaV_|_@%Q&z^HB93pA0TLkTndYSSMwf9*(K?BCdr6Lu~3UhB)H&;n9&% z9Cz;yV<2m4pNvNRWBNI`d_LJx+2YlxM7unh`xJq1ObAHjUYy}|sQlQhjr11TfWLbS zx|pY z*6(~VJDcQ`U1ZnNpc6h&R{AAQ+xI-Z!YysLoE|^-!H9UVcWT>_8q(+O08KJIbFY9g zz!=BCtZmQ7+94Z(z}qs=gft5-`c02Ojqv^-xVxp#dYEP#!~>o!8Z0rzJqfNLKE}6& z=PCN3{DaZpcYemF2IRzcuRj8@ppwk@Apz$fRPNVt1*7XYSGCuLbxShsex5Ut&*+Vy zap^1w{v~{^^@TCoAESW<5{pG&89zL+#C-$gZeJ5E@8Lc-dT_E|zOGlpd(aOwRgXui z7V9_c)EQqg{w~>8cx
Z4%G6GIqonNyj^z`!@vS`2n1gs&6Qs1Gi-$>hI@ox8ky ztEjW$I*+)yTgcxIprXUko=eWW?cHAC)hwf8omkDNm4t+wJ}g85e1IS0`B&K0 zm|$meN}Vzgpno@$?li-LFKevQg*QrK;@eG@Se~6{ESZcw^nyBFWd;(X@v2qiLtzDS z#&mitD)YHAYS3uvp`D}bCV7#>x}HdfQaS2*)%We787u5whP5Jvz z$cN>^jWd78ZX|>126v^XY`KOtn=!}g(63VU7{Sb3XV+=ocTu8D>;83ZKQND_h#2_1 zuCrz!?9$Ni)Zo*26oXjudC*MFY>ecEDwo!u(nf}}%URL=R;=PV=bN#Dgfv1!m+!4Rd2|tf-)T{psPBwBx|%m_oEISuia%lH;}c75dh)RN zuR%3p^ig?#i3OG0(HH|juVPlu8k!S}boUpAJOB*I0_`@zgm;KjQW$~Hnq+mi#9ROg z>Q{5`l_0Yl_toM-eGjnEKYUt{cr`EQDIVb3$Q(J&J`&Byxdz}xiF{jyqBJ3da#8P% zU{e`u%p9AOdo6j2vLE#-K9hMnS2uMv^at}H&;sD*aQ=Pb%Vk4Dqk8!zIc(;7G9T4q z`Npl;RFb93u3q6uo^A=TY8`1wT6H7jM}glFKaiAQQvAGc>6T?6en^#mM;Ve&DFj!) z&bo5+)AYxLAWZjKWa|*Qj_k)i4t8EV-jsSVa!LcL9klf1`z6zmDjC6thMYsIrb0U= zbTGw@=wnDh|0Qm=m%)1wDdrw1oHKr9eXu`?hGDJYH)h)+mId|Pkq`xwnCeW^b#!JM zQ0aX3l7cmJd*M?WyPc4A237gU%((a`Pka;S+OJPIwR!pnC^m?5j&sE6mRnY(Wqcu% z2BW-Y#+mFVBRzJbQRDGPO)CosO-Q8Qj6jrB=TwiD)7+e+dh*c%Ae?M~97a1CIO)TB z;8^ouz_kN;^#?KBln-m6_-ty7mmLt zL?@y6tjjr7*;hk*Grc5Bf}L+HrKsMHu!4viJ>qe;hFf-b`em`F*zm_LsT%u0sfr~;ZPxXYw zAX*YCa%B2qK9S$P$1ebS0lwj~k>1I+$@2WsvY!2Fm(<)8`*-_>p5UUzOiJ4L z840|L+&%)g<{Eh^f19cjvZ3GvnW2y}*^D?m*bruWmGd>Lbk^?t@Cc0>kkPbZV9Bt(Dq1km2QXQ9O?hPFC-oqm(*#<~xo+li?~^DyijTZ6 zo6Bn1q0j1>#WXdiBksn1L{>pg8mG*`dq*kBP*-Mi>VSU zZELfsE!qHwpVTm)O#8z@R;z7J3yfYEkN`E zI@m(}=j?0`@aA?N;Q{?>*UbZt?cEbinOMcw4v#Yc>{0|dA|~b+r|D+SnvFG zTpC!6cfLqJeLh8=UYtce^BSf_uh87+h>>>xu)_dMR)!iyEcRW8z`X4VV>(3HHdw^f z`DuqjlI|TD-OKgr136h39;iZNrfsG6Tsah%P^LgcC(oB-)J+p4?ebD>J9d2RO_^C0f39OH-?lBFA`_#uV5%bO^|F z=V}pl-g+kAE7WvgineR=ZzIEY9{4Sx+FOplppdi_sED2e6$q;lr$Ym*POBWHzvf~B zMuBfeC!2Yzt!|kvqL&3^2o|UD%H*kq!z@l)w3enf_*QzHoZEp1koM(Nb_L!%0E;7$4lcu3|I9G zPOrp_kA1N(=rN6cF zy<$a-WzFC2-ZAv*SX!6u1kFKzODWyq8cLAHnT`!WWAo}Z4h}-XnyriaQL^-)Nt7P- zof`o_Io5P)?LU7XMr-ic8THZKstMba79w_M*XiEJDy6%r_Gr*8-64fxyxvX?epEk} zi2W9kBgF>tA1|CxqR081S|7{fIrg@V2b;pQ$TXU11)l8bD2hgt*;A`?WcRDJ^}NV! zAg8j320O|E$%G~E9L(sW9DSc|bb|adlpL@vfDP> zVE-(h)E*{8GxK^Ah@NY0cX5@-Y%UqciCMaD!+DfU(ww6{t0VEu?12-tuxf)eX|B_8 zA;WT=c;a3xy8uoMWv#_t2v0nynT?iW=HJF>^1g}KQK4Q8s)^6ar(-%Q4(VvZWNBT_ zzdUZ%N{a}h`t6ap#zR-=34HCv)^4vR>qE)fzUw_KuW_B(V{24)r6?xkSdoyL_Z$hJ z{C1fFLHVgqS2egCNMh4GqJk_~ZJZYB-%b-x-+g>N)lnEj77vfrG3xkS45+}L{ZR4# zJf{$ru~a+T{*evv`h2GQN@uToM>QR>dpx{!o#Wx#+{TM3mUi;*R9}=uA1~KGp0=_l zG!8F$w-bv8xY=T`M!0(l@?~-CfL%4P-c|OTL5nkj>{lel^*Y{UOM#TeeRv^4M-If` zVX<;2`|%9+T%ueLDW-VwmQ-9VK!+Pomqqta3 zV*7M&U@e0@@cun3Q*|f>7ZE5HfaW+ffcrC7lBBzCzCvUV*$nnExXu#N8ul*M3d1(# z#{68TQ$a$lmL?wJLNsC8e$Z&j!l&Wu`qInV8sh9gLX@=pe8CZ7X;;d1eg#dtHiNp_OUuOSznviMXY$B^YLo1b+MAZQVx;pji|5v9U&k;LC$yBd(Z~;|+0zm)Jm>)MW&0Of z?VlBe;eUp@Xcjm&n#U^`4qmPBRc(*a}sw}-o~c}@AA)il=sH77}swFOjnFV za2mk;N82ipnF6LiEB`_`-emwhF0C=LIk(GXo!-FN9@H4e1NCUn`DFXvGMtsTFNOMu z^@Y0=oI6C5i`1TpVJ?}h@R5BSLGG0~x)7SHd9D&FlA=HC9!EPFQ?iKu;sHl*?ggHA z1(Z5P#W2G768ByE9=TpSLi#0t<7&bzUmBwu{%5rVLh+7`3dz@2U08^jlh68(t# zl`u2b#IUmLf=j`8IC zsWCT-pIdFO@2|lX=J{{4XyMA|lUX;Kg1@3}+`pHpVW+2T#_D>`q<&{9VBhSHf}G10 z^pa}aT`WcT`eF{tth|%TC0cd>>A!6p1EZwP$;>qf5rpcDu&oNCUCiJy`AmmTq3-%? zM?a8@7uVFIsR-AxrG`fG`3T7+ikUDh3t=*ko+1V!GqVa`Mo{?^6-w_ao*r3A24`_SN zPlYeYujeu3FjYzCtyV|a58+}Kso6qAcbE7x;~T%nJz^uFvB2S?MfvoI7q1e(0MdDSu0|2@1^Yw%G3*sBk^(Taj z{XYud=jud}`KVyY`B~Z{hq`!i0G>9&(?f_v1SkNdZy(}7A$%~0BWG^N|5O%Y%1Vfl zcCv}2W}e-H12n*HXr^yTRPOjdhJg2e>tDn+yMEjepinVLO22j5tWo+b`)9-J-&|Wy z1jJwKv_A;DVV-+~AcKDWYlv;LFT4KXZa48%d2dYSLs%!T3Nfr~+92apm%&=5^@#c3 zV7LFOM%n-7Iwtz@!8g45NP{~UNf($Z#l(Uo?z9id3Jhhy1dhbnb_LLP=&8g_l<`kB zak4@SRv|lVH)w!$)LWQ+JN0xbyWv>n>9V&4us>U$KRMXbW(h?xJUML^MP(Qyz%1vo z>jzRU^%-KRhZ}msNx#@q7I69)qz4Vh>2ZvG>`SI9TF*$??FDL4nQu}r+eB%jW-qs| z1pnB=4=1x$bQS)pvF{iGiTgX?o1KPBsH7R1^2NYe0%2kRb^F zh)Cm1Nx$acZ>s+KT-^=@?ikKTZCJ$Rc~u7F7|Q(7BVbcLPj!Aw0QRZjyj*C8Guer? zkCn?9KU-HrWFB)JF!lW+-mxe!lAkI`b5t4&xrD=mv~*Us2q>ND0uFj zr_E)|7c3@V-!KuirLt>$XBA@bN%dp|?`bJN(2F7$G&DpQJYbq!^c zQz@#JN_iIx%D*)JfN1yJ%;S_+P^p7c*QrCVLdI6qAab|(e^!DDhyn|Ky!y`k_n#;6 zrX{xAw-iGOlygAQNY7TR%<%5QFO$)8rape3D$I%+Nl6@SdtrzcfWgjN<7mya_A;1e z$&;mUI<8q=8gjb%FD5j~*%Q*k5)d zm(u>$9WnR+ENqZb^M4O_jD=grdg^df2K&0wo<wcykjI^+C(m-oV05Hr*d}7q@tkUJ0?n*<$X3i;>n!K znNEF)-N7`ATKH1UELm;wu#*o?%{Lt=)7f2N0wXDMF-D+JoK_wK;F!kbDt=fyk1b9| zNXythT1bJxMap8E=Wz_}Lw&9rJMZ6ypG}yVsQ8fPRRD<)L~~ENubFb|on-KGsa3Qm z(H-+r!#*R`Bq<#{zH2hwdymRx=_mfl>q=t17nk4FdfRkJ#qGt=8cM5JPh#DPrt;p~ zR}!ibN37pVQ!5zeja+`!X(&`-1WPdwt(LPQR%F-P;_@ZiBedyD^ljBygri_ShgsIL zhj(Kg4=;)P@|iOTWvD={w?39;x=AN0=;Xy+-k@^63LX=6<$KZ*J+kYJx+`P6PdKQ5 zUrP!GO?|_M5hHgDbWFGi<8e%T{ZkxVNSDV(WJ(`#T0eyd*=k1q<`6oCA@~=Dtg7k) zWUX`Uw6SoN|996q8aVIOlyVKI3Df2VBO8%3ItQun^0^f-I7~PT7OtI3r*NIyx6tj` ztaFy509XqPf;A}`YAMmWeGHR7XM)N2P^@KYAJ6(|x|6q=L%CnF6Vj8&V9L|V!Z6c& z&S0Fyw?unaB+dP*ZR1!b;QU^-&dQiJp7Zu827A3@7e0Y>6=!9+y5#JW@eoEnRu)`_ zA`1Sx@^oT~jEva+;CA&lTlpBR@hH~J#zAi7_*+EYvL6jnumguU*ch#AEAX9__;ZOK zldsr*Bi*ft#43>ebt8SPNd#*F85=6Z^e67c(4{86$(-G=Dq3@3A5Fl^;=xJd;uKh zzWFjp#5#EOwar492#8QbLau?Vs<&s562D^iJw7A>&kW$lWJpdcs@v1)@_c+H$|zOd zC~`7z*c(Q&2Z@1un^JrgxKL+WIEwMQ|L<2!ajUwowc03`)am2qe95f1c3P;|_@Fn> zwMGdJk>L3DX%d`)P!8*vi9vir{vfE2Feki(uqvc(DfJ*Vq)o8g_&1CJnxIWrfw2N( zwol(=#^MeZ;jI2W8D5t@92jYe>PWKx%-;OB|I?T*hRb~s9BZ>p&!w`Yt8O_78*&Xc zaW~~V$w_w}GB~=!b2a)>$K9su>RO-82#hvoe!NsF^BTC`dCZtkD+dtt?Pk30a=u*o zWe2ZC>+<^&EYO++`~b%_>4{3D^B6ynY6`;-9l$iz^yl0bll#l)l!V|J5mT?@n2g5aEOeyH$a_(cOLZ#yxRZT$*EG3k#nsJz_ZT@oCSkCCQ?>-UM=TC?F4uPlr}2nP5VNVYp0s(PbMS1Pi;l_3 zOo~znVU@>LXtKlD2eV~7?;xIJxmMzSb!fCq;t9d<$vdGRI#EgQ} z9Ygv`4h_k53kPg@2{AZ3fMN_Div(Sj#Z_>ibUaL2r)El1B%xw!&8%sGg{bbMN?7$M zi##_aGP7!?7NOxWb;r+QqyD+RZH*VI-G5~)7(VJ&uMu-04-ELuEq{mH5Arz+1(k#8 zcT;$|uD@dE8uv!ESafpe1NSACSY!mZcC0`ulDmWFeuhFrZvo|a2XnO)Uwgx_Q#nVM zQ8PT`YkK3}J6uzx-1uVDnux|^eZ<^asQd0X{9e!3o}7aiMzoj zznK1RS$!6uuV@bs9FuRrXr9jilUyCSju6lZ`%L=SRsNPq;eIRFRXN_?1lV?_B6h%n z?!YovX71(k;H@c&%MW}UWW$hNSc3o5Jp&uRUap0IoW?E^B|#B6w4iv(96b2*57I_1 z@-y|13pbgiMY8c9rC(Rjw80;eU(PTon{-muweN+HEWJ@kdd#Uh7+sw=Ye?_EGet2? z6}CHQEmWX%_Rdl$2OCee^*t6WV#y%uUA!zFFbcnQ!%788=`5>{UP7N72sf_{= zVli+i0%2nC=j$F~1X)VF%Kx})!#fhZf&t-4yX=3PVE=)4qyC==ZwSplv42!d4?V-d z$c~*%1QB#qWdrvQ1Bm97X>{D~Su{PL;`#BEqqMc~H5X56LjjaE{!ZsX0=;8w5h(M5 zscT=TFN24vesI+HhgO!BS8jK~dpjAoSM)l(%rk2U4MOmRy0U*QK3U_T-$l?0&zlGx z6}H7sn3P;TF{7g`9ab@vAcB^pW_#P2^MkrDWXdm|O+BVkKLr}|j=psd`17}ES4I!z z+;#u4yr)mKqqWwTo9_6yeUv1TKEE?DzZ_XJ{nMTdZ(sX0HSjXYV$!J9JHCgd(am&~ z=Wg-{%~6j!E(Ap;ZuWthecDVbN~V)jIluVZmp3m`7Ky^O>ZdDT`M&1Um1#`=?HNvy z;Q9LH0ihTxJHj5rsKqR1K9A;j#M;4K$P;@Q?rE-9v!VGIJImGwM@D z?PM@lb=2hf>1b(XNV6+oE4)Xd?P0Sj#4uulk{JgwEYav(Sc$4iW z$5C`MC1&cG+aD|E&F*Gcw9Gwa&maiLt!-VGXMoox)0%bd)4ZrdmfN5D*_AxmT42{w z=C0g;oA9)$hTo7eU<=RgK+L0H==px(hC!zwMoUxQPGarMBI-fy#%{wy`f4W3 z<>)>mg{cuGIe@eJ%6<|ke8}F87~DU=jTqb7F%Av?RKcmm+0kqahuYplEh#pcv@Gkq z%BHgjFiA724-0*vgN3ISjL#i4If6ie;|?JskW5ewtofR>Z=B8N84O`o-i2&nN#6IUo-dp=U^QR4=#Mk~4Ne7A7e!#Xxq z(G501<1ae=7$fFhgrHy{d|LE?$t^qGka-LPX4@s{Q1`WYqu9a4_Vi^ z$ua_Oe;o7(^ssK~6qUA^N#?cf$EmL`NyxUorO;%6bnp94A_PGruybVvKdq+G&IJQa`;h&J>~4i=3_s!#d)Uc%OjR+VCt}d(_N!S9PVzy%`HPQOZ4PLk5eXPTF9p?tL{iv zsJbP_dHl-L$ea>QZJN1Ev*-?jo~bU}IS(DADkw$~0B+`fAgxVbD_>?jd7{TP{nEu8RJ%b}}t7e~$UWD+N}}tQn(~ z?#>ZjMEUiyWKtn(o@M|0g_Zh*O@ry|kgI9F4qVV>yk-ws%seBDj2}}-(!UPY zHysHvl#zjEQP7{sx5SbdN@X)zPj?qc+)MvHDxjaFl~`|SlrIIpcWkJirv5Dj6)gXL zXj!UK9sGE+uK%^lmT6p(=gZ82G;`$tKhu_yo~xuEd!S$ zE1%v%vy5ZZ<%{2&W@!{o{sS{rKr~qz%eu<8$+9PC7eLQxd$!{ip)2s3|3=RyT$|ep*3iEjkv%8iI8oqKd zqkL*njl_9bt>s)nB{`?N`KgeIapGTNf%%u7tV|6R)0FvjULR{Rg@|Q9VTwxkgMK7@ zS|hVwz}*Csd#%MjuDbI6PiW%EL@$c+xZMckJUqpA7o@rvIY91S%V*{K?Q$li{?nJc z1VCQ!>rz*Rt!s52M0!V_L=tQ`$#uBttO7}KQ!~vgZtYbTg7gnKSf1cM=;?PnR!+Kg zYVyDpSI1Ik#S13gBB6Rx8HtQGJc7JeX$p(EO3on&7t89Qmjy_%<7KU70;H0iLFnsM zUc_~C1 z{r-W!Gm&CoxN>wN+cj|$*VYwu3WvI)&0MQsUkaZ>wB-D*vrZ~6qRrV2|FI>HLFm0_ z64Q_O1y>%roNF7C7z%0+V3TVy=F(B4)ax99&uiY^p$T!j2$7H0Y*f3Lr&WRcGE3?5 zT$_HE;?`WF?a;QEJS)&wE3+%!k(gR%mF+n827SWV*!#p@+J^j^y1h(PU#Q04nmhsR zlun4V*3;F}uIs+X0l{+(4H%G^kr=zJoLh_#lX3~VhXjlWC;9}bX#c$%A7MQYTii29 zD~tkzhBwL;?vtQpdgl#>17)1^+P=gYycK_wbl33`@#Irn#&{(ro%50O&gZ^g?}^MI zUy69|v$Q^RZ)P**l}tcVgUrJq1$j(S9AIBu^a zJOUJ~lp6@ZK-%}eudT--yRl9V31pBQN0d{p1^yN>s&y)bqfHUZOHJ+K1U`#Y!lo(e zCwxwtzIkL|rD$f1=f6Fhw~8RI&gR1qD}C^;i@>$XW5APaT^Yo=L%bvyK2R_v$5$XeOD{KPKSGXSv5fH@xp<&i35bfE zs$%W)ezhTHW}dJP0ukjr#gfHK}~}TzrucVQmu$=RS|Ntp%XjgWMAIB`Vhtd z&WE(g%unmyklnEo>^NINI>po3OzUJ~w$vp_M|+}$quTZJL1YwvW-!2TX|&#hhw%p% z4`Q9k)k$FCEH$=(+m$s5B&(GH+coUK=oqak#ybqCc0!n>agg|`z4+Sl>zYf;yh_} z8cE{Su9vigI=st;vK!@4Kd;ut@uti!2Jct)+S~{^DrT2o$ChH!xO)n6z8h8QG2V*f zFkK^?*stHT&om{8%wX#Xpovnd~P2#XXx)?kQc z)npta0ihLKyS^i|+?8;2^5K8YWq$J=#?z4Jhdkj3zTZ`a)D4rz+k%wcOs~^}u(3}t z;-a&8tQxVmukR8<5(?Z|+S5x>d_M6^fzLWKIHV0RFq?mRXysUs@ZyaqeWhtSh>vD+ z4ab>376R+a-&pjD{CzM?5w*OZPI~~^NdRf(ywvm8=bzcoIbz2t&)f7e?pm6x zx%HSg`-R7ik34N(+XEJkPF=@C|J3i~khyFwG-04GyCbM}IybKVJi1 zXHs)ipd4gDS(Hb^t?fzOL#^*qoE`f=7kBpW=1)b+(#v5(0GK^So1*zPl67hEe^qrR z{!s4GA0K9zu{LJLz757&wvg@0m>GL&$QDCm%TkI$N*Xi9*coNZOFdat-brl=aR7|=Ecl- z`O_#M3xI71@}*2|zDh#ctd!fIQ5kdrs$)QvjR}f6YkdIp2NH15OsXt!csVv>eQw{d z*P%1TNY=s6HRXbd+cn-@1^g;r9%qr!SOHbvC|a+j&`z?W_yH2UM>Ny%#QO`{0(LI~ zbHo78-FSaU1ul^~Chsu1_1Y8PZgBR56WXqD@C0)AtH1=2fkY$o|6T<_wUv^dK4n}g zSP{2oFY#*(%5$XyxF`E;d2^@R#dk41;p}}szKt8qI^<{sJs+;QThzxiIw$uwtg(po zJ2qBWlD%Jbpwb|9{%sgCoOr`22p7CKT-{qC8+~iIxbD{IwN`OY(!_wq3rYaRj$@bN zVM>ueT@;NluC?_gTu|xNr201eVfppPlIxkSCu;TRZsxr4-vDckeW)!oNK}hTgW`}a z__7j;V*CK#>!u}_XGVeJVI$<5Cw0L>`M;^A^tnC@<2{YHadb?zyz~8M-HDZ#tcXql zrN%Xa&(SLbHV90*M2Dc}os-Mp8*c-}X7~$?+hN1?H*{-UkIkzZL$NCy!H0W>xqI5L ziot4TxKNi#BnaC6vE*=R@PI3RTZrB*Z>dm5Pg)gn@4B>o$boq(RD15VeS9^N3fN!| z@?8R}hnCyiN5#Cqkh4WE)D8a$7L;`J9!!#xd~RBPDQ)MdeHQ;Ze^pTH`1CY3Plo== z+Ia}iQbwQ+rF^)kYaFbn)GRX`JXP?9364a1d6NrcX0g$>XeokU$fj!H56RvTTBsd8 z#2@A#zPlgF1%-k1|Bqw0zWU1nQ|5O}RI*(ZFcWK5a>{4dRdkaURWEJs$2sw(A|mH9 z7WBuuuWIh?8OPf=rY06u6)z3;VV?i4nddlrZ_Xjl(q!Y)$=Fvt#WIlClBV>l6;CrB z{e3AZ23s?rx;+dT3A7ck;)g7V22VDj9VmvzO_zjXj*FFz+!nS1&2(kKLd0&Uv%j%D zT5-wGBS9>^_w9nQQV<;0gUs^$;;o*==eVa^E5XqyRWOOvk(+`z7$SSl*56Ugu2@1p zGnBoJ`JzVB>Ivyb>e~G+Nu{Dx^+#&9EFVfj-n6J&G?@=6%P6|tc9+y?x2cOQJHb5k z`HS&k@gR7l@)TX$xzM`0^`rV5#r3o6W)lyj7xbs&Hj#6c?H`QM|LLY&;3H7^_ai`s zo#e1c0%AI(8UvLsVB^opSjM%YJ_9Me8c6sA{P5n6JCUD8oH6!SGrGKNsE$O0Yb(!B zZZ;IX;B%w}W6}HnAQ;4GX@y6TMT%t)iv5ru7tu^gB=WuehORGSRr7Ea3N6F8o;2_R z!^#G#!NYPy5343Q#Ip5m{)E<+?I z{oaY;`)w&R_A2~KT|m@bOKK6UebdQj)y{X|B08Kf51OVB1K#{RPiV5Pm&jiQN~E$3 z{L*tNsulAaR#P6%)N_d@V^X%21P#73nntU@mAn}R@}A-bcCk@tXgv5eYl>%~RZd6S z=?{ImUm+^GN{ToKexb97SP;81cUn6b>7~?an}T%)7FhUrc~VLtB!PH87aqFnu$H*H ztq(*&*enk}ysSagQ?N^tq(_I`FK|=Ct&O_*)h@g~WEDd9*ri$1fwR~A_w1Y08_QV! zy!fv=3$Jc=q{3AupNAOW=#O1l;z)Fi+FO6*pWglF3s}WqLtLuwlRqO>SG9&M33B4D7NF=3Z2R_(p{_@SXJebhPWc_#~x%f&WunQ1|40UIfZm`ffS?Cco9e zWr<37l3=R9XXl+Web(WRn1EG!-JCUDP3>{xRMn*7Q{E=mH^Gsd8Ie*!&hUw)X z)Efnql7{aTJt1*l20)z*R_#IG2~#qc)h0sa++klmnaHK~sM7H5kU!-u_GJIUaapn9 zN8tBok5I?_Fdb4q+Nty)RYO%7JoFv}NmVUCZx>6F_V{dwAR3j>(HUl8ngW$%WIMQj)tC(ued?e>hL zKVDIFdn{9&YN80_7*H9oz2&MBx7@>DIGPwS6ldlR>hEZT$hv}OX!fIj`2<7(Cf&*& znE{p?X#v1g?XUA0#$`3|0PiQF(znA?yy~!0JcYeTr|P#P{4aYy2H<^|!vQ zkm{l8EWH?MoI)U`+{;yu5iAo@|U7;#LJc>I$2`)mvsOfPkA2Q zEDyUBGboHvHW)sB0tH;`D)At|MldYmqZdubYGllMD9y-LUs2d=AhvZaPz3kLcwRX) zM?J0Vg3&Fr=~VNf1!dgti(0cTw@be>^KfzQADnjU0xA#|<$uEh^jyIA0!DKC)CU0S z85iY5U}qL}r0{mu@14(5A*3PjlL&7nSdxE}?3y-oH?T`qSbOz{mm!0$`~Bd%39ngD zR@|eV34ZNC^-k(L8OZpv{P1MY1D!8cOpU5WWc0e3K!N{azd@WO+SE}8;qdR zu}>>s>>f;uN#d+q+C`M*3{QF7mU|OX=MFx2Iehs}W5*M@ZSu7Q@Ykuu7g)EQqvt>f zhO7iTM^o95!2-eC(fRRT)<^r~)B5>)+#3|Z@)v4U@MNMuk5nF=i<*69JnZgW?8@H? zb@PLl9Dq~ReH`*rwVM~FGd-x|RS{ zcddV6i4k0GO;MNDSsroz)n{ycdZ=cSlx}JkeR0`@!=}o))xBR3JW3|y+UM!D4C~x> z*M!5hu5Te~pJ+LPTI!?eO43z8kCv%I?6s%R8LX&>d$3n1;V0vooVQZ4)yOA_KhHzd zUIsyOasKgYf7X*K&WS_7v&W0Nk_=xmWwwK|DSX~F@89JSBi}0l)m4y8?*DL=SAR}5 zmV8c(XSN~dxEkbg1XA~DNmSN6inh&FOlTRt<2CsOk_lTLpdyk@!-Eodp|1B>Z$R^? zq#onTYg^3Hja=B-{Fo%v8Fcid>pY(p@syNMz7t#)4gZkx@hgL%Rla60bbwoK3TH?Q zY6PC=mU*h;gcFMjb?p^r$%lnplLO9ImU`zooBwS#ot(cFkILH93Gv=lJt!bYpmP7G zWTMD=z5+-cl38OBo&lHKkV`Z0>dq93bm>mZD|$wO@*-zpfZxSc9+Oy$d4U0ZqNEhU zJ@$$nyr`|m{CjHf<`IgJ8caRyP_U&8bVJ)ijzxOle-PPBllevJgMJ?NmSd$TP_gB! zi1Ayn@iNc5dLhpR`kkpB4H|y(5SUCE_DGM%!voki6r39By2VP6f6BI?6!YBXK1UfI zLYlCWJ7(;kjch}u1#wDgu9S*de2d*|XFFWDX>Q|q>eZXP`_q3d+^i=lJ-cye3Uhae zrec5VF*_+B3rM~qZ$~t>US~SL8FRw+qmd&M(Td6?eoC4jOBYd;+Eqt@1y&$L|JD(99ft)l?v$2C@KYTQlU3^$ zVS90Hu;v}}-@KV)ESZ|=sw?{i-RTrIBo4FST5VILG>3w5YLgL`zA|eI1!t#$GSt@L zo<>f-nz$W-jx2b7{S`^x^z6i0NbQJcqg2=qbWORzf{gevE2q+_DhtYZJMlmYB!`2V z;6&tOIW&{{Q6DQDi2a1*CwV6(o~7dkLxS|!)U+?XWMGKKAYoS-@P!fY(C7)u0R-;(bl`8~k6>3Z_X+_QhB9zVd!F>* z_HU5N;%Gjo6(}X^vgemca~;C=F6>iO5n0$WGnYBRCzvddDQR$;8f}wv zM6;)Wnf}ncNHs$<19m*!FVd_cv&`IX`bb;Ichy{@)A^OzLc8@I1yFmtq5-fn@6cdK z0KFQ8#``ym+M(E)a%w3;2?2z7W?2tINt?Fwj9J3UX|_1MPo}*3ng1i4$}~DE(1j{z zIFFI3^eqGD6Jzkr`_(#6oANI&4Uxsc<_Cljx;o4OxXkuF9rH#oL9{}GYwNSvub zk$CXC^P$JTc~Vo247_YJa38Ax>w7ui z(zvhOoHzOS{rFenpca3?wiNqe7{arDIS_plfsJ&-~ntz;XhCxnhS%ASG8;XGk3P;J&!V})tj}@ED zzO&=o1;eEPLp~MY*<}JuYCbFb3t52m}-0EX_swNf#HP*hQIM(=s2EliZ+VHBThyh(XdW{53a(UqBsx zT3+2a?=(enBv}tZjOJ54aH>OC%%R2u6%l!NuFzFT_k3V_jZllHzMWrcsT+5e#*7p% z%||#5U#~%B!P1*blX--Dg}%)-Ez@`ffjOyc&5{KLhw!}d-J<$`WAwi{>>oHhxq~Dq zFzLh|KsTheUPSkrI_I!RBJvSgtie$wZN7b0*CaZW?XtCk2FgdU=?LM3z_Q`6yN%)+PV8(VdvAa!eC^<;cV75js^S@D`)Ri28Wv-H3Vy>pkuwAegPauJcZ_)o z<7tb4J!$b$`BR2y!rZ;}JrhrJ1rmM+r5P02(^PFN4sGSmJjvi!W4k*dtLsS)o#?iq zdey!XsS7TdhK$#mpj(1r7@;*488fO}%)}T7tAepA(O5Gz5L7gR<}b_FRoN7rc8LGE z-Ab%?qC~ZF-(Y812TeW?zn$<>V=Ec>i1idC5VA||raiMTPsVaui;coNQ8G|}=;L$= zWy7v&FG%`6ia*`g;M}}tX$5S}?5|I~gNDXCM~9|zu&@0Nib<@`$C87Muo7k~k>qJX z28?bqboO2tGYS}^1_TXRciXlpjN523x~@G|6tB1;5#(1V2ssv$AREu&beJB;77z0w zf?Gemd-6Th*(XbM^d~aee)Cc6z~#XX6TDX4tQXu1wlTfOq*Y;N&wi5OJtB2abj^p$ z^0=8-49I;CB)#fqGUu}DI}9^*x_Xmyc}Q%&EaS68Q$?0|PfBGj`U-tI8#?JwMb@8B z8^E1W*ecJcdh}f(#$q}&*hxcpTrJC59D`7sQs}M59y_pGbuxjlq5mBR{IgX|}ts+<)dhNlWH)w-3lP(nT<;}5>Pd@L@O(?c{T*6JEWF?N>Rf^S-y zxSxzM$cPE)N)?jkgstb^@_VS5zY^YOqNY#5HY?PgT>tE$j$&GjTw_*Q8wk*r>rQ&> z20n%hX{;@s5iHgP^6cZ^O{2|T=v#41$KzS*ZH(HuvNct^J29HaGzs$!UN}9Pc3&p} zn@{)1LHZ9r@@=&?&*Me^(EPa3o7s15>9N&JN4}D3t)T^?_}k47tEl@%`Zf7~)~}U} v?5$$ouh@CI{;BWPs{$^8yIyUojN9Y`M#%!)(6U76&;xV?!pZ?iT$cX>f4?N` literal 0 HcmV?d00001 diff --git a/src/routes/opening/assets/loader-dots.svg b/src/routes/opening/assets/loader-dots.svg deleted file mode 100644 index bd96324c..00000000 --- a/src/routes/opening/assets/loader-dots.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/routes/opening/assets/safe-created.svg b/src/routes/opening/assets/safe-created.svg new file mode 100644 index 00000000..2cfaaaa7 --- /dev/null +++ b/src/routes/opening/assets/safe-created.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/routes/opening/assets/success.svg b/src/routes/opening/assets/success.svg deleted file mode 100644 index d635672d..00000000 --- a/src/routes/opening/assets/success.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/src/routes/opening/components/Footer.tsx b/src/routes/opening/components/Footer.tsx index 8f213484..c4b30ac5 100644 --- a/src/routes/opening/components/Footer.tsx +++ b/src/routes/opening/components/Footer.tsx @@ -1,19 +1,36 @@ -import React, { SyntheticEvent } from 'react' +import React, { ReactElement, SyntheticEvent } from 'react' import styled from 'styled-components' +import { Icon, Link, Text } from '@gnosis.pm/safe-react-components' + import Button from 'src/components/layout/Button' -import { connected } from 'src/theme/variables' import { getExplorerInfo } from 'src/config' +import Hairline from 'src/components/layout/Hairline' -const ExplorerLink = styled.a` - color: ${connected}; +const StyledText = styled(Text)` + display: inline-flex; + a { + margin-left: 4px; + } + svg { + position: relative; + top: 4px; + left: 4px; + } ` - const ButtonWithMargin = styled(Button)` margin-right: 16px; ` +const FooterContainer = styled.div` + width: 100%; + height: 76px; -export const GenericFooter = ({ safeCreationTxHash }: { safeCreationTxHash: string }) => { + button { + margin-top: 24px; + } +` + +export const GenericFooter = ({ safeCreationTxHash }: { safeCreationTxHash: string }): ReactElement => { const explorerInfo = getExplorerInfo(safeCreationTxHash) const { url, alt } = explorerInfo() const match = /(http|https):\/\/(\w+\.\w+)\/.*/i.exec(url) @@ -21,20 +38,23 @@ export const GenericFooter = ({ safeCreationTxHash }: { safeCreationTxHash: stri return ( -

This process should take a couple of minutes.

-

+ This process should take a couple of minutes. + Follow the progress on{' '} - - {explorerDomain} - - . -

+ + {explorerDomain} + + + +
) } @@ -45,16 +65,19 @@ export const ContinueFooter = ({ }: { continueButtonDisabled: boolean onContinue: (event: SyntheticEvent) => void -}) => ( - +}): ReactElement => ( + + + + ) export const ErrorFooter = ({ @@ -63,13 +86,14 @@ export const ErrorFooter = ({ }: { onCancel: (event: SyntheticEvent) => void onRetry: (event: SyntheticEvent) => void -}) => ( - <> +}): ReactElement => ( + + Cancel - + ) diff --git a/src/routes/opening/index.tsx b/src/routes/opening/index.tsx index 22c961fc..7f3f79a3 100644 --- a/src/routes/opening/index.tsx +++ b/src/routes/opening/index.tsx @@ -12,20 +12,19 @@ import Paragraph from 'src/components/layout/Paragraph' import { instantiateSafeContracts } from 'src/logic/contracts/safeContracts' import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions' import { getWeb3 } from 'src/logic/wallets/getWeb3' -import { background, connected } from 'src/theme/variables' +import { background, connected, fontColor } from 'src/theme/variables' import { providerNameSelector } from 'src/logic/wallets/store/selectors' import { useSelector } from 'react-redux' -import LoaderDotsSvg from './assets/loader-dots.svg' -import SuccessSvg from './assets/success.svg' +import SuccessSvg from './assets/safe-created.svg' import VaultErrorSvg from './assets/vault-error.svg' -import VaultSvg from './assets/vault.svg' +import VaultLoading from './assets/creation-process.gif' import { PromiEvent, TransactionReceipt } from 'web3-core' const Wrapper = styled.div` display: grid; grid-template-columns: 250px auto; - grid-template-rows: 62px auto; + grid-template-rows: 43px auto; margin-bottom: 30px; ` @@ -44,29 +43,31 @@ const Body = styled.div` grid-column: 2; grid-row: 2; text-align: center; - background-color: #ffffff; + background-color: ${({ theme }) => theme.colors.white}; border-radius: 5px; min-width: 700px; - padding-top: 50px; + padding-top: 70px; box-shadow: 0 0 10px 0 rgba(33, 48, 77, 0.1); display: grid; - grid-template-rows: 100px 50px 70px 60px 100px; + grid-template-rows: 100px 50px 110px 1fr; ` const CardTitle = styled.div` font-size: 20px; + padding-top: 10px; ` interface FullParagraphProps { inversecolors: string + stepIndex: number } const FullParagraph = styled(Paragraph)` - background-color: ${(p) => (p.inversecolors ? connected : background)}; - color: ${(p) => (p.inversecolors ? background : connected)}; - padding: 24px; - font-size: 16px; + background-color: ${({ stepIndex }) => (stepIndex === 0 ? connected : background)}; + color: ${({ theme, stepIndex }) => (stepIndex === 0 ? theme.colors.white : fontColor)}; + padding: 28px; + font-size: 20px; margin-bottom: 16px; transition: color 0.3s ease-in-out, background-color 0.3s ease-in-out; ` @@ -77,17 +78,12 @@ const BodyImage = styled.div` const BodyDescription = styled.div` grid-row: 2; ` -const BodyLoader = styled.div` - grid-row: 3; - display: flex; - justify-content: center; - align-items: center; -` const BodyInstruction = styled.div` - grid-row: 4; + grid-row: 3; + margin: 27px 0; ` const BodyFooter = styled.div` - grid-row: 5; + grid-row: 4; padding: 10px 0; display: flex; @@ -154,7 +150,7 @@ export const SafeDeployment = ({ } if (stepIndex <= 4) { - return VaultSvg + return VaultLoading } return SuccessSvg @@ -326,20 +322,26 @@ export const SafeDeployment = ({ - Vault + Vault {steps[stepIndex].description || steps[stepIndex].label} - {!error && stepIndex <= 4 && Loader dots} - - - - {error ? 'You can Cancel or Retry the Safe creation process.' : steps[stepIndex].instruction} - - + {steps[stepIndex].instruction && ( + + + {error ? 'You can Cancel or Retry the Safe creation process.' : steps[stepIndex].instruction} + + + )} {FooterComponent ? ( @@ -354,9 +356,12 @@ export const SafeDeployment = ({ ) : null} - - Back - + + {stepIndex !== 0 && ( + + Back + + )} ) } diff --git a/src/routes/opening/steps.ts b/src/routes/opening/steps.ts index 8863d7a4..a2f6e2d7 100644 --- a/src/routes/opening/steps.ts +++ b/src/routes/opening/steps.ts @@ -1,6 +1,6 @@ import { ContinueFooter, GenericFooter } from './components/Footer' -export const isConfirmationStep = (stepIndex?: number) => stepIndex === 0 +export const isConfirmationStep = (stepIndex?: number): boolean => stepIndex === 0 export const steps = [ { @@ -42,7 +42,7 @@ export const steps = [ id: '6', label: 'Success', description: 'Your Safe was created successfully', - instruction: 'Click below to get started', + instruction: undefined, footerComponent: ContinueFooter, }, ] From 1818302d5de64410bbe1e71939c86ef4355c3fb0 Mon Sep 17 00:00:00 2001 From: Fernando Date: Tue, 16 Mar 2021 19:04:48 -0300 Subject: [PATCH 09/37] (Fix) Stop from notifying user's wallet tx confirmation (#2007) Co-authored-by: Daniel Sanchez Co-authored-by: Mati Dastugue --- .../safe/store/actions/createTransaction.ts | 8 +-- .../safe/store/actions/processTransaction.ts | 11 ++-- .../middleware/notificationsMiddleware.ts | 28 ++++++++-- .../safe/store/reducer/gatewayTransactions.ts | 1 + src/logic/safe/utils/aboutToExecuteTx.ts | 51 +++++++++++++++++++ 5 files changed, 84 insertions(+), 15 deletions(-) create mode 100644 src/logic/safe/utils/aboutToExecuteTx.ts diff --git a/src/logic/safe/store/actions/createTransaction.ts b/src/logic/safe/store/actions/createTransaction.ts index 4ce9c211..b2e6b347 100644 --- a/src/logic/safe/store/actions/createTransaction.ts +++ b/src/logic/safe/store/actions/createTransaction.ts @@ -12,6 +12,7 @@ import { tryOffchainSigning, } from 'src/logic/safe/transactions' import { estimateGasForTransactionCreation } from 'src/logic/safe/transactions/gas' +import * as aboutToExecuteTx from 'src/logic/safe/utils/aboutToExecuteTx' import { getCurrentSafeVersion } from 'src/logic/safe/utils/safeVersion' import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses' import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions' @@ -148,6 +149,9 @@ export const createTransaction = ( await saveTxToHistory({ ...txArgs, txHash, origin }) + // store the pending transaction's nonce + isExecution && aboutToExecuteTx.setNonce(txArgs.nonce) + dispatch(fetchTransactions(safeAddress)) }) .on('error', (error) => { @@ -156,10 +160,6 @@ export const createTransaction = ( onError?.() }) .then(async (receipt) => { - if (isExecution) { - dispatch(enqueueSnackbar(notificationsQueue.afterExecution.noMoreConfirmationsNeeded)) - } - dispatch(fetchTransactions(safeAddress)) return receipt.transactionHash diff --git a/src/logic/safe/store/actions/processTransaction.ts b/src/logic/safe/store/actions/processTransaction.ts index f54d2c12..28934b23 100644 --- a/src/logic/safe/store/actions/processTransaction.ts +++ b/src/logic/safe/store/actions/processTransaction.ts @@ -11,6 +11,7 @@ import { } from 'src/logic/safe/safeTxSigner' import { getApprovalTransaction, getExecutionTransaction, saveTxToHistory } from 'src/logic/safe/transactions' import { tryOffchainSigning } from 'src/logic/safe/transactions/offchainSigner' +import * as aboutToExecuteTx from 'src/logic/safe/utils/aboutToExecuteTx' import { getCurrentSafeVersion } from 'src/logic/safe/utils/safeVersion' import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions' import { providerSelector } from 'src/logic/wallets/store/selectors' @@ -117,8 +118,6 @@ export const processTransaction = ({ dispatch(updateTransactionStatus({ txStatus: 'PENDING', safeAddress, nonce: tx.nonce, id: tx.id })) await saveTxToHistory({ ...txArgs, signature }) - // TODO: while we wait for the tx to be stored in the service and later update the tx info - // we should update the tx status in the store to disable owners' action buttons dispatch(fetchTransactions(safeAddress)) return @@ -154,6 +153,10 @@ export const processTransaction = ({ try { await saveTxToHistory({ ...txArgs, txHash }) + + // store the pending transaction's nonce + isExecution && aboutToExecuteTx.setNonce(txArgs.nonce) + dispatch(fetchTransactions(safeAddress)) } catch (e) { console.error(e) @@ -172,10 +175,6 @@ export const processTransaction = ({ console.error('Processing transaction error: ', error) }) .then(async (receipt) => { - if (isExecution) { - dispatch(enqueueSnackbar(notificationsQueue.afterExecution.noMoreConfirmationsNeeded)) - } - dispatch(fetchTransactions(safeAddress)) if (isExecution) { diff --git a/src/logic/safe/store/middleware/notificationsMiddleware.ts b/src/logic/safe/store/middleware/notificationsMiddleware.ts index f516a141..1ad8cb52 100644 --- a/src/logic/safe/store/middleware/notificationsMiddleware.ts +++ b/src/logic/safe/store/middleware/notificationsMiddleware.ts @@ -1,4 +1,5 @@ import { push } from 'connected-react-router' +import { Action } from 'redux-actions' import { NOTIFICATIONS, enhanceSnackbarForAction } from 'src/logic/notifications' import closeSnackbarAction from 'src/logic/notifications/store/actions/closeSnackbar' @@ -8,14 +9,19 @@ import { getSafeVersionInfo } from 'src/logic/safe/utils/safeVersion' import { isUserAnOwner } from 'src/logic/wallets/ethAddresses' import { userAccountSelector } from 'src/logic/wallets/store/selectors' import { grantedSelector } from 'src/routes/safe/container/selector' -import { ADD_QUEUED_TRANSACTIONS } from 'src/logic/safe/store/actions/transactions/gatewayTransactions' +import { + ADD_QUEUED_TRANSACTIONS, + ADD_HISTORY_TRANSACTIONS, +} from 'src/logic/safe/store/actions/transactions/gatewayTransactions' +import * as aboutToExecuteTx from 'src/logic/safe/utils/aboutToExecuteTx' +import { QueuedPayload } from 'src/logic/safe/store/reducer/gatewayTransactions' import { safeParamAddressFromStateSelector, safesMapSelector } from 'src/logic/safe/store/selectors' -import { isTransactionSummary } from 'src/logic/safe/store/models/types/gateway.d' +import { isTransactionSummary, TransactionGatewayResult } from 'src/logic/safe/store/models/types/gateway.d' import { loadFromStorage, saveToStorage } from 'src/utils/storage' import { ADD_OR_UPDATE_SAFE } from '../actions/addOrUpdateSafe' -const watchedActions = [ADD_OR_UPDATE_SAFE, ADD_QUEUED_TRANSACTIONS] +const watchedActions = [ADD_OR_UPDATE_SAFE, ADD_QUEUED_TRANSACTIONS, ADD_HISTORY_TRANSACTIONS] const LAST_TIME_USED_LOGGED_IN_ID = 'LAST_TIME_USED_LOGGED_IN_ID' @@ -70,9 +76,21 @@ const notificationsMiddleware = (store) => (next) => async (action) => { const state = store.getState() switch (action.type) { + case ADD_HISTORY_TRANSACTIONS: { + const userAddress: string = userAccountSelector(state) + const safes = safesMapSelector(state) + + const executedTxNotification = aboutToExecuteTx.getNotification(action.payload, userAddress, safes) + // if we have a notification, dispatch it depending on transaction's status + executedTxNotification && dispatch(enqueueSnackbar(executedTxNotification)) + + break + } case ADD_QUEUED_TRANSACTIONS: { - const { safeAddress, values } = action.payload - const transactions = values.filter((tx) => isTransactionSummary(tx)).map((item) => item.transaction) + const { safeAddress, values } = (action as Action).payload + const transactions = values + .filter((tx) => isTransactionSummary(tx)) + .map((item: TransactionGatewayResult) => item.transaction) const userAddress: string = userAccountSelector(state) const awaitingTransactions = getAwaitingGatewayTransactions(transactions, userAddress) diff --git a/src/logic/safe/store/reducer/gatewayTransactions.ts b/src/logic/safe/store/reducer/gatewayTransactions.ts index a3eada88..fbd7e314 100644 --- a/src/logic/safe/store/reducer/gatewayTransactions.ts +++ b/src/logic/safe/store/reducer/gatewayTransactions.ts @@ -344,6 +344,7 @@ export const gatewayTransactions = handleActions { + // TODO: review if is this `PENDING` status required under `queued.queued` list // prevent setting `PENDING_FAILED` status, if previous status wasn't `PENDING` if (txStatus === 'PENDING_FAILED' && txToUpdate.txStatus !== 'PENDING') { return txToUpdate diff --git a/src/logic/safe/utils/aboutToExecuteTx.ts b/src/logic/safe/utils/aboutToExecuteTx.ts new file mode 100644 index 00000000..7db235e3 --- /dev/null +++ b/src/logic/safe/utils/aboutToExecuteTx.ts @@ -0,0 +1,51 @@ +import { getNotificationsFromTxType } from 'src/logic/notifications' +import { + isStatusFailed, + isTransactionSummary, + TransactionGatewayResult, +} from 'src/logic/safe/store/models/types/gateway.d' +import { HistoryPayload } from 'src/logic/safe/store/reducer/gatewayTransactions' +import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions' +import { isUserAnOwner } from 'src/logic/wallets/ethAddresses' +import { SafesMap } from 'src/routes/safe/store/reducer/types/safe' + +let nonce: number | undefined + +export const setNonce = (newNonce: typeof nonce): void => { + nonce = newNonce +} + +export const getNotification = ( + { safeAddress, values }: HistoryPayload, + userAddress: string, + safes: SafesMap, +): undefined => { + const currentSafe = safes.get(safeAddress) + + // no notification if not in the current safe or if its not an owner + if (!currentSafe || !isUserAnOwner(currentSafe, userAddress)) { + return + } + + // if we have a nonce, then we have a tx that is about to be executed + if (nonce !== undefined) { + const executedTx = values + .filter(isTransactionSummary) + .map((item: TransactionGatewayResult) => item.transaction) + .find((transaction) => transaction.executionInfo?.nonce === nonce) + + // transaction that was pending, was moved into history + // that is: it was executed + if (executedTx !== undefined) { + const notificationsQueue = getNotificationsFromTxType(TX_NOTIFICATION_TYPES.STANDARD_TX) + const notification = isStatusFailed(executedTx.txStatus) + ? notificationsQueue.afterExecutionError + : notificationsQueue.afterExecution.noMoreConfirmationsNeeded + + // reset nonce value + setNonce(undefined) + + return notification + } + } +} From 1c74f39f37f78636d3f3896ef3e5ccdadddb46d2 Mon Sep 17 00:00:00 2001 From: Mikhail Mikheev Date: Wed, 17 Mar 2021 14:47:12 +0400 Subject: [PATCH 10/37] Update Mushrooms finance app (#2035) --- src/routes/safe/components/Apps/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/safe/components/Apps/utils.ts b/src/routes/safe/components/Apps/utils.ts index 3fa9d1f3..6d8a8c7d 100644 --- a/src/routes/safe/components/Apps/utils.ts +++ b/src/routes/safe/components/Apps/utils.ts @@ -77,7 +77,7 @@ export const staticAppsList: Array = [ }, // Mushrooms finance { - url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmQs6CUbMUyKe3Sa3tU3HcnWWzsuCk8oJEk8CZKhRcJfEh`, + url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmT96aES2YA9BssByc6DVizQDkofmKRErs8gJyqWipjyS8`, disabled: false, networks: [ETHEREUM_NETWORK.MAINNET], }, From 4ed181886b3025065777bd0063118e06932fb5f2 Mon Sep 17 00:00:00 2001 From: Agustin Pane Date: Wed, 17 Mar 2021 14:28:37 -0300 Subject: [PATCH 11/37] (Feature) - V2 fetch supported fiat currencies from client gateway (#2023) * Replace collectibles fetch with client gateway * Updates tokenProps types * Replaces balance endpoint with client gateway * Remove default export of tokens list * Set the default rows per page to 100 * Fix ether price load * Remove Add custom token button * Remove add custom asset and add custom token modals * Remove default exports * Remove currencyValues state from store Remove currencyValues selectors * Update balance state with fiatBalance and tokenBalance * Remove default export from fetchEtherBalance.ts * Fix safeFiatBalancesTotalSelector Add totalFiatBalance to safe store * Remove fetchCurrenciesRates.ts * Adds in currencyValuesStorageMiddleware logic for updating the safe tokens when the user changes the selected currency * Move selectedCurrency to simple redux state Remove currencyValues redux state * Updates fetchTokenCurrenciesBalances with selectedCurrency parameter * Revert CurrencyValuesState Remove selectedCurrency from safe state * Remove selectedCurrency from safe state Update currentCurrencySelector selector usage * Add fetchAvailableCurrencies setAvailableCurrencies and updateAvailableCurrencies * Remove availableCurrencies.ts by using availableCurrenciesSelector * Fix display of ETH balance on extendedSafeTokensSelector and extractDataFromRESULT * Fix multiple calls to token balance endpoint * (Feature) - V2 Remove Manage List (#2032) Co-authored-by: fernandomg --- src/components/App/index.tsx | 8 +- src/index.tsx | 2 - .../collectibles/store/selectors/index.ts | 19 +-- .../api/fetchAvailableCurrencies.ts | 8 ++ .../api/fetchCurrenciesRates.ts | 41 ------ .../store/actions/fetchCurrencyRate.ts | 27 ---- .../store/actions/fetchSelectedCurrency.ts | 15 +- .../store/actions/setAvailableCurrencies.ts | 6 + .../store/actions/setCurrencyBalances.ts | 12 -- .../store/actions/setCurrencyRate.ts | 9 -- .../store/actions/setSelectedCurrency.ts | 20 +-- .../actions/updateAvailableCurrencies.ts | 18 +++ ....ts => currencyValuesStorageMiddleware.ts} | 9 +- .../store/model/currencyValues.ts | 66 --------- .../store/reducer/currencyValues.ts | 51 +++---- .../currencyValues/store/selectors/index.ts | 55 +------ .../__tests__/fetchSafeTokens.test.ts | 13 +- .../api/fetchTokenCurrenciesBalances.ts | 16 ++- src/logic/safe/hooks/useFetchTokens.tsx | 19 ++- src/logic/safe/hooks/useLoadSafe.tsx | 4 +- .../safe/hooks/useSafeScheduledUpdates.tsx | 4 +- .../store/actions/activateTokenForAllSafes.ts | 7 - .../safe/store/actions/fetchEtherBalance.ts | 4 +- src/logic/safe/store/actions/fetchSafe.ts | 3 +- .../safe/store/actions/updateActiveAssets.ts | 9 -- .../safe/store/actions/updateActiveTokens.ts | 19 --- .../safe/store/actions/updateAssetsList.ts | 7 - .../store/actions/updateBlacklistedAssets.ts | 9 -- .../store/actions/updateBlacklistedTokens.ts | 9 -- .../safe/store/actions/updateTokensList.ts | 7 - .../safe/store/middleware/safeStorage.ts | 38 +---- src/logic/safe/store/models/safe.ts | 9 +- src/logic/safe/store/reducer/safe.ts | 40 ------ src/logic/safe/store/selectors/index.ts | 57 +------- .../safe/store/tests/safe.balances.test.ts | 59 -------- .../shouldSafeStoreBeUpdated.test.ts | 59 ++------ .../utils/currencyValuesStorage.ts | 0 .../store/actions/activateAssetsByBalance.ts | 44 ------ .../actions/{saveTokens.ts => addTokens.ts} | 4 +- .../tokens/store/actions/fetchSafeTokens.ts | 81 +++++------ src/logic/tokens/store/actions/fetchTokens.ts | 10 +- .../tokens/store/actions/loadActiveTokens.ts | 25 ---- src/logic/tokens/store/model/token.ts | 8 +- src/logic/tokens/store/reducer/tokens.ts | 2 +- src/logic/tokens/utils/tokenHelpers.ts | 8 +- src/logic/tokens/utils/tokensStorage.ts | 25 ---- .../safe/components/Balances/Coins/index.tsx | 16 +-- .../screens/SendCollectible/index.tsx | 4 +- .../SendModal/screens/SendFunds/index.tsx | 2 +- .../safe/components/Balances/Tokens/index.tsx | 60 -------- .../Tokens/screens/AssetsList/AssetRow.tsx | 47 ------ .../Tokens/screens/AssetsList/index.tsx | 132 ----------------- .../Tokens/screens/AssetsList/style.ts | 79 ---------- .../Tokens/screens/TokenList/TokenRow.tsx | 54 ------- .../Tokens/screens/TokenList/index.tsx | 136 ------------------ .../Tokens/screens/TokenList/style.ts | 87 ----------- .../safe/components/Balances/Tokens/style.ts | 15 -- .../safe/components/Balances/dataFetcher.ts | 39 ++--- src/routes/safe/components/Balances/index.tsx | 61 +------- .../components/CurrencyDropdown/index.tsx | 19 +-- src/routes/safe/container/selector.ts | 14 +- src/routes/safe/store/reducer/types/safe.ts | 2 + src/store/index.ts | 14 +- src/test/utils/DOMNavigation/tokens.ts | 31 +--- src/test/utils/transactions/index.ts | 3 +- .../utils/transactions/moveTokens.helper.ts | 37 ----- 66 files changed, 236 insertions(+), 1581 deletions(-) create mode 100644 src/logic/currencyValues/api/fetchAvailableCurrencies.ts delete mode 100644 src/logic/currencyValues/api/fetchCurrenciesRates.ts delete mode 100644 src/logic/currencyValues/store/actions/fetchCurrencyRate.ts create mode 100644 src/logic/currencyValues/store/actions/setAvailableCurrencies.ts delete mode 100644 src/logic/currencyValues/store/actions/setCurrencyBalances.ts delete mode 100644 src/logic/currencyValues/store/actions/setCurrencyRate.ts create mode 100644 src/logic/currencyValues/store/actions/updateAvailableCurrencies.ts rename src/logic/currencyValues/store/middleware/{index.ts => currencyValuesStorageMiddleware.ts} (61%) delete mode 100644 src/logic/currencyValues/store/model/currencyValues.ts rename src/logic/{currencyValues => safe}/__tests__/fetchSafeTokens.test.ts (84%) rename src/logic/{currencyValues => safe}/api/fetchTokenCurrenciesBalances.ts (59%) delete mode 100644 src/logic/safe/store/actions/activateTokenForAllSafes.ts delete mode 100644 src/logic/safe/store/actions/updateActiveAssets.ts delete mode 100644 src/logic/safe/store/actions/updateActiveTokens.ts delete mode 100644 src/logic/safe/store/actions/updateAssetsList.ts delete mode 100644 src/logic/safe/store/actions/updateBlacklistedAssets.ts delete mode 100644 src/logic/safe/store/actions/updateBlacklistedTokens.ts delete mode 100644 src/logic/safe/store/actions/updateTokensList.ts delete mode 100644 src/logic/safe/store/tests/safe.balances.test.ts rename src/logic/{currencyValues/store => safe}/utils/currencyValuesStorage.ts (100%) delete mode 100644 src/logic/tokens/store/actions/activateAssetsByBalance.ts rename src/logic/tokens/store/actions/{saveTokens.ts => addTokens.ts} (54%) delete mode 100644 src/logic/tokens/store/actions/loadActiveTokens.ts delete mode 100644 src/logic/tokens/utils/tokensStorage.ts delete mode 100644 src/routes/safe/components/Balances/Tokens/index.tsx delete mode 100644 src/routes/safe/components/Balances/Tokens/screens/AssetsList/AssetRow.tsx delete mode 100644 src/routes/safe/components/Balances/Tokens/screens/AssetsList/index.tsx delete mode 100644 src/routes/safe/components/Balances/Tokens/screens/AssetsList/style.ts delete mode 100644 src/routes/safe/components/Balances/Tokens/screens/TokenList/TokenRow.tsx delete mode 100644 src/routes/safe/components/Balances/Tokens/screens/TokenList/index.tsx delete mode 100644 src/routes/safe/components/Balances/Tokens/screens/TokenList/style.ts delete mode 100644 src/routes/safe/components/Balances/Tokens/style.ts delete mode 100644 src/test/utils/transactions/moveTokens.helper.ts diff --git a/src/components/App/index.tsx b/src/components/App/index.tsx index fa9b73f5..189000d3 100644 --- a/src/components/App/index.tsx +++ b/src/components/App/index.tsx @@ -20,13 +20,17 @@ import { getNetworkId } from 'src/config' import { ETHEREUM_NETWORK } from 'src/config/networks/network.d' import { networkSelector } from 'src/logic/wallets/store/selectors' import { SAFELIST_ADDRESS, WELCOME_ADDRESS } from 'src/routes/routes' -import { safeNameSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' +import { + safeFiatBalancesTotalSelector, + safeNameSelector, + safeParamAddressFromStateSelector, +} from 'src/logic/safe/store/selectors' +import { currentCurrencySelector } from 'src/logic/currencyValues/store/selectors' import Modal from 'src/components/Modal' import SendModal from 'src/routes/safe/components/Balances/SendModal' import { useLoadSafe } from 'src/logic/safe/hooks/useLoadSafe' import { useSafeScheduledUpdates } from 'src/logic/safe/hooks/useSafeScheduledUpdates' import useSafeActions from 'src/logic/safe/hooks/useSafeActions' -import { currentCurrencySelector, safeFiatBalancesTotalSelector } from 'src/logic/currencyValues/store/selectors' import { formatAmountInUsFormat } from 'src/logic/tokens/utils/formatAmount' import { grantedSelector } from 'src/routes/safe/container/selector' diff --git a/src/index.tsx b/src/index.tsx index 64a80101..7a3aeb82 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -6,7 +6,6 @@ import { Integrations } from '@sentry/tracing' import Root from 'src/components/Root' import loadCurrentSessionFromStorage from 'src/logic/currentSession/store/actions/loadCurrentSessionFromStorage' -import loadActiveTokens from 'src/logic/tokens/store/actions/loadActiveTokens' import loadDefaultSafe from 'src/logic/safe/store/actions/loadDefaultSafe' import loadSafesFromStorage from 'src/logic/safe/store/actions/loadSafesFromStorage' import { store } from 'src/store' @@ -17,7 +16,6 @@ disableMMAutoRefreshWarning() BigNumber.set({ EXPONENTIAL_AT: [-7, 255] }) -store.dispatch(loadActiveTokens()) store.dispatch(loadSafesFromStorage()) store.dispatch(loadDefaultSafe()) store.dispatch(loadCurrentSessionFromStorage()) diff --git a/src/logic/collectibles/store/selectors/index.ts b/src/logic/collectibles/store/selectors/index.ts index 1c5e6412..85efdb98 100644 --- a/src/logic/collectibles/store/selectors/index.ts +++ b/src/logic/collectibles/store/selectors/index.ts @@ -3,8 +3,6 @@ import { NFTAsset, NFTAssets, NFTToken, NFTTokens } from 'src/logic/collectibles import { AppReduxState } from 'src/store' import { NFT_ASSETS_REDUCER_ID, NFT_TOKENS_REDUCER_ID } from 'src/logic/collectibles/store/reducer/collectibles' -import { safeActiveAssetsSelector } from 'src/logic/safe/store/selectors' - export const nftAssets = (state: AppReduxState): NFTAssets => state[NFT_ASSETS_REDUCER_ID] export const nftTokens = (state: AppReduxState): NFTTokens => state[NFT_TOKENS_REDUCER_ID] @@ -26,21 +24,8 @@ export const orderedNFTAssets = createSelector(nftTokensSelector, (userNftTokens export const activeNftAssetsListSelector = createSelector( nftAssetsListSelector, - safeActiveAssetsSelector, availableNftAssetsAddresses, - (assets, activeAssetsList, availableNftAssetsAddresses): NFTAsset[] => { - return assets - .filter(({ address }) => activeAssetsList.has(address)) - .filter(({ address }) => availableNftAssetsAddresses.includes(address)) - }, -) - -export const safeActiveSelectorMap = createSelector( - activeNftAssetsListSelector, - (activeAssets): NFTAssets => { - return activeAssets.reduce((acc, asset) => { - acc[asset.address] = asset - return acc - }, {}) + (assets, availableNftAssetsAddresses): NFTAsset[] => { + return assets.filter(({ address }) => availableNftAssetsAddresses.includes(address)) }, ) diff --git a/src/logic/currencyValues/api/fetchAvailableCurrencies.ts b/src/logic/currencyValues/api/fetchAvailableCurrencies.ts new file mode 100644 index 00000000..4be6a079 --- /dev/null +++ b/src/logic/currencyValues/api/fetchAvailableCurrencies.ts @@ -0,0 +1,8 @@ +import { getClientGatewayUrl } from 'src/config' +import axios from 'axios' + +export const fetchAvailableCurrencies = async (): Promise => { + const url = `${getClientGatewayUrl()}/balances/supported-fiat-codes` + + return axios.get(url).then(({ data }) => data) +} diff --git a/src/logic/currencyValues/api/fetchCurrenciesRates.ts b/src/logic/currencyValues/api/fetchCurrenciesRates.ts deleted file mode 100644 index 20d1d6b2..00000000 --- a/src/logic/currencyValues/api/fetchCurrenciesRates.ts +++ /dev/null @@ -1,41 +0,0 @@ -import axios from 'axios' -import BigNumber from 'bignumber.js' - -import { EXCHANGE_RATE_URL } from 'src/utils/constants' -import { fetchTokenCurrenciesBalances } from './fetchTokenCurrenciesBalances' -import { sameString } from 'src/utils/strings' -import { AVAILABLE_CURRENCIES } from 'src/logic/currencyValues/store/model/currencyValues' - -const fetchCurrenciesRates = async ( - baseCurrency: string, - targetCurrencyValue: string, - safeAddress: string, -): Promise => { - let rate = 0 - if (sameString(targetCurrencyValue, AVAILABLE_CURRENCIES.NETWORK)) { - try { - const tokenCurrenciesBalances = await fetchTokenCurrenciesBalances(safeAddress) - if (tokenCurrenciesBalances.items.length) { - rate = new BigNumber(1).div(tokenCurrenciesBalances.items[0].fiatConversion).toNumber() - } - } catch (error) { - console.error(`Fetching ${AVAILABLE_CURRENCIES.NETWORK} data from the relayer errored`, error) - } - return rate - } - - // National currencies - try { - const url = `${EXCHANGE_RATE_URL}?base=${baseCurrency}&symbols=${targetCurrencyValue}` - const result = await axios.get(url) - if (result?.data) { - const { rates } = result.data - rate = rates[targetCurrencyValue] ? rates[targetCurrencyValue] : 0 - } - } catch (error) { - console.error('Fetching data from getExchangeRatesUrl errored', error) - } - return rate -} - -export default fetchCurrenciesRates diff --git a/src/logic/currencyValues/store/actions/fetchCurrencyRate.ts b/src/logic/currencyValues/store/actions/fetchCurrencyRate.ts deleted file mode 100644 index 580b90e4..00000000 --- a/src/logic/currencyValues/store/actions/fetchCurrencyRate.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Action } from 'redux-actions' -import { ThunkDispatch } from 'redux-thunk' - -import fetchCurrenciesRates from 'src/logic/currencyValues/api/fetchCurrenciesRates' -import { setCurrencyRate } from 'src/logic/currencyValues/store/actions/setCurrencyRate' -import { AVAILABLE_CURRENCIES } from 'src/logic/currencyValues/store/model/currencyValues' -import { CurrencyRatePayload } from 'src/logic/currencyValues/store/reducer/currencyValues' -import { AppReduxState } from 'src/store' - -const fetchCurrencyRate = (safeAddress: string, selectedCurrency: string) => async ( - dispatch: ThunkDispatch>, -): Promise => { - if (AVAILABLE_CURRENCIES.USD === selectedCurrency) { - dispatch(setCurrencyRate(safeAddress, 1)) - return - } - - const selectedCurrencyRateInBaseCurrency: number = await fetchCurrenciesRates( - AVAILABLE_CURRENCIES.USD, - selectedCurrency, - safeAddress, - ) - - dispatch(setCurrencyRate(safeAddress, selectedCurrencyRateInBaseCurrency)) -} - -export default fetchCurrencyRate diff --git a/src/logic/currencyValues/store/actions/fetchSelectedCurrency.ts b/src/logic/currencyValues/store/actions/fetchSelectedCurrency.ts index 2297b676..b4f279a0 100644 --- a/src/logic/currencyValues/store/actions/fetchSelectedCurrency.ts +++ b/src/logic/currencyValues/store/actions/fetchSelectedCurrency.ts @@ -2,18 +2,17 @@ import { Action } from 'redux-actions' import { ThunkDispatch } from 'redux-thunk' import { setSelectedCurrency } from 'src/logic/currencyValues/store/actions/setSelectedCurrency' -import { AVAILABLE_CURRENCIES } from 'src/logic/currencyValues/store/model/currencyValues' -import { CurrentCurrencyPayload } from 'src/logic/currencyValues/store/reducer/currencyValues' -import { loadSelectedCurrency } from 'src/logic/currencyValues/store/utils/currencyValuesStorage' -import { AppReduxState } from 'src/store' -export const fetchSelectedCurrency = (safeAddress: string) => async ( - dispatch: ThunkDispatch>, +import { loadSelectedCurrency } from 'src/logic/safe/utils/currencyValuesStorage' +import { AppReduxState } from 'src/store' +import { SelectedCurrencyPayload } from 'src/logic/currencyValues/store/reducer/currencyValues' + +export const fetchSelectedCurrency = () => async ( + dispatch: ThunkDispatch>, ): Promise => { try { const storedSelectedCurrency = await loadSelectedCurrency() - - dispatch(setSelectedCurrency(safeAddress, storedSelectedCurrency || AVAILABLE_CURRENCIES.USD)) + dispatch(setSelectedCurrency({ selectedCurrency: storedSelectedCurrency || 'USD' })) } catch (err) { console.error('Error fetching currency values', err) } diff --git a/src/logic/currencyValues/store/actions/setAvailableCurrencies.ts b/src/logic/currencyValues/store/actions/setAvailableCurrencies.ts new file mode 100644 index 00000000..e510b494 --- /dev/null +++ b/src/logic/currencyValues/store/actions/setAvailableCurrencies.ts @@ -0,0 +1,6 @@ +import { createAction } from 'redux-actions' +import { AvailableCurrenciesPayload } from 'src/logic/currencyValues/store/reducer/currencyValues' + +export const SET_AVAILABLE_CURRENCIES = 'SET_AVAILABLE_CURRENCIES' + +export const setAvailableCurrencies = createAction(SET_AVAILABLE_CURRENCIES) diff --git a/src/logic/currencyValues/store/actions/setCurrencyBalances.ts b/src/logic/currencyValues/store/actions/setCurrencyBalances.ts deleted file mode 100644 index b7390166..00000000 --- a/src/logic/currencyValues/store/actions/setCurrencyBalances.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { createAction } from 'redux-actions' -import { BalanceCurrencyList } from 'src/logic/currencyValues/store/model/currencyValues' - -export const SET_CURRENCY_BALANCES = 'SET_CURRENCY_BALANCES' - -export const setCurrencyBalances = createAction( - SET_CURRENCY_BALANCES, - (safeAddress: string, currencyBalances: BalanceCurrencyList) => ({ - safeAddress, - currencyBalances, - }), -) diff --git a/src/logic/currencyValues/store/actions/setCurrencyRate.ts b/src/logic/currencyValues/store/actions/setCurrencyRate.ts deleted file mode 100644 index 786a2db7..00000000 --- a/src/logic/currencyValues/store/actions/setCurrencyRate.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { createAction } from 'redux-actions' - -export const SET_CURRENCY_RATE = 'SET_CURRENCY_RATE' - -// eslint-disable-next-line max-len -export const setCurrencyRate = createAction(SET_CURRENCY_RATE, (safeAddress: string, currencyRate: number) => ({ - safeAddress, - currencyRate, -})) diff --git a/src/logic/currencyValues/store/actions/setSelectedCurrency.ts b/src/logic/currencyValues/store/actions/setSelectedCurrency.ts index 8a87126d..b1b7c577 100644 --- a/src/logic/currencyValues/store/actions/setSelectedCurrency.ts +++ b/src/logic/currencyValues/store/actions/setSelectedCurrency.ts @@ -1,20 +1,6 @@ -import { Action, createAction } from 'redux-actions' -import { ThunkDispatch } from 'redux-thunk' - -import { CurrencyPayloads } from 'src/logic/currencyValues/store/reducer/currencyValues' -import { AppReduxState } from 'src/store' -import fetchCurrencyRate from 'src/logic/currencyValues/store/actions/fetchCurrencyRate' +import { createAction } from 'redux-actions' +import { SelectedCurrencyPayload } from 'src/logic/currencyValues/store/reducer/currencyValues' export const SET_CURRENT_CURRENCY = 'SET_CURRENT_CURRENCY' -const setCurrentCurrency = createAction(SET_CURRENT_CURRENCY, (safeAddress: string, selectedCurrency: string) => ({ - safeAddress, - selectedCurrency, -})) - -export const setSelectedCurrency = (safeAddress: string, selectedCurrency: string) => ( - dispatch: ThunkDispatch>, -): void => { - dispatch(setCurrentCurrency(safeAddress, selectedCurrency)) - dispatch(fetchCurrencyRate(safeAddress, selectedCurrency)) -} +export const setSelectedCurrency = createAction(SET_CURRENT_CURRENCY) diff --git a/src/logic/currencyValues/store/actions/updateAvailableCurrencies.ts b/src/logic/currencyValues/store/actions/updateAvailableCurrencies.ts new file mode 100644 index 00000000..77e24961 --- /dev/null +++ b/src/logic/currencyValues/store/actions/updateAvailableCurrencies.ts @@ -0,0 +1,18 @@ +import { Action } from 'redux-actions' +import { ThunkDispatch } from 'redux-thunk' +import { AppReduxState } from 'src/store' +import { AvailableCurrenciesPayload } from 'src/logic/currencyValues/store/reducer/currencyValues' +import { setAvailableCurrencies } from 'src/logic/currencyValues/store/actions/setAvailableCurrencies' +import { fetchAvailableCurrencies } from 'src/logic/currencyValues/api/fetchAvailableCurrencies' + +export const updateAvailableCurrencies = () => async ( + dispatch: ThunkDispatch>, +): Promise => { + try { + const availableCurrencies = await fetchAvailableCurrencies() + dispatch(setAvailableCurrencies({ availableCurrencies })) + } catch (err) { + console.error('Error fetching available currencies', err) + } + return Promise.resolve() +} diff --git a/src/logic/currencyValues/store/middleware/index.ts b/src/logic/currencyValues/store/middleware/currencyValuesStorageMiddleware.ts similarity index 61% rename from src/logic/currencyValues/store/middleware/index.ts rename to src/logic/currencyValues/store/middleware/currencyValuesStorageMiddleware.ts index a361f593..e03420b7 100644 --- a/src/logic/currencyValues/store/middleware/index.ts +++ b/src/logic/currencyValues/store/middleware/currencyValuesStorageMiddleware.ts @@ -1,16 +1,15 @@ import { SET_CURRENT_CURRENCY } from 'src/logic/currencyValues/store/actions/setSelectedCurrency' -import { saveSelectedCurrency } from 'src/logic/currencyValues/store/utils/currencyValuesStorage' +import { saveSelectedCurrency } from 'src/logic/safe/utils/currencyValuesStorage' const watchedActions = [SET_CURRENT_CURRENCY] -const currencyValuesStorageMiddleware = () => (next) => async (action) => { +export const currencyValuesStorageMiddleware = () => (next) => async (action) => { const handledAction = next(action) if (watchedActions.includes(action.type)) { switch (action.type) { case SET_CURRENT_CURRENCY: { const { selectedCurrency } = action.payload - - saveSelectedCurrency(selectedCurrency) + await saveSelectedCurrency(selectedCurrency) break } @@ -21,5 +20,3 @@ const currencyValuesStorageMiddleware = () => (next) => async (action) => { return handledAction } - -export default currencyValuesStorageMiddleware diff --git a/src/logic/currencyValues/store/model/currencyValues.ts b/src/logic/currencyValues/store/model/currencyValues.ts deleted file mode 100644 index 26618349..00000000 --- a/src/logic/currencyValues/store/model/currencyValues.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { List, Record, RecordOf } from 'immutable' - -import { getNetworkInfo } from 'src/config' - -const { nativeCoin } = getNetworkInfo() - -export const AVAILABLE_CURRENCIES = { - NETWORK: nativeCoin.symbol.toLocaleUpperCase(), - USD: 'USD', - EUR: 'EUR', - AUD: 'AUD', - BGN: 'BGN', - BRL: 'BRL', - CAD: 'CAD', - CHF: 'CHF', - CNY: 'CNY', - CZK: 'CZK', - DKK: 'DKK', - GBP: 'GBP', - HKD: 'HKD', - HRK: 'HRK', - HUF: 'HUF', - IDR: 'IDR', - ILS: 'ILS', - INR: 'INR', - ISK: 'ISK', - JPY: 'JPY', - KRW: 'KRW', - MXN: 'MXN', - MYR: 'MYR', - NOK: 'NOK', - NZD: 'NZD', - PHP: 'PHP', - PLN: 'PLN', - RON: 'RON', - RUB: 'RUB', - SEK: 'SEK', - SGD: 'SGD', - THB: 'THB', - TRY: 'TRY', - ZAR: 'ZAR', -} as const - -export type BalanceCurrencyRecord = { - currencyName?: string - tokenAddress?: string - balanceInBaseCurrency: string - balanceInSelectedCurrency: string -} - -export const makeBalanceCurrency = Record({ - currencyName: '', - tokenAddress: '', - balanceInBaseCurrency: '', - balanceInSelectedCurrency: '', -}) - -export type CurrencyRateValueRecord = RecordOf - -export type BalanceCurrencyList = List - -export interface CurrencyRateValue { - currencyRate?: number - selectedCurrency?: string - currencyBalances?: BalanceCurrencyList -} diff --git a/src/logic/currencyValues/store/reducer/currencyValues.ts b/src/logic/currencyValues/store/reducer/currencyValues.ts index ee626001..4855aedf 100644 --- a/src/logic/currencyValues/store/reducer/currencyValues.ts +++ b/src/logic/currencyValues/store/reducer/currencyValues.ts @@ -1,44 +1,35 @@ -import { Map } from 'immutable' import { Action, handleActions } from 'redux-actions' - -import { SET_CURRENCY_BALANCES } from 'src/logic/currencyValues/store/actions/setCurrencyBalances' -import { SET_CURRENCY_RATE } from 'src/logic/currencyValues/store/actions/setCurrencyRate' import { SET_CURRENT_CURRENCY } from 'src/logic/currencyValues/store/actions/setSelectedCurrency' -import { BalanceCurrencyList, CurrencyRateValue } from 'src/logic/currencyValues/store/model/currencyValues' +import { AppReduxState } from 'src/store' +import { SET_AVAILABLE_CURRENCIES } from 'src/logic/currencyValues/store/actions/setAvailableCurrencies' export const CURRENCY_VALUES_KEY = 'currencyValues' -export interface CurrencyReducerMap extends Map { - get(key: K, notSetValue?: unknown): CurrencyRateValue[K] - setIn(keys: [string, K], value: CurrencyRateValue[K]): this +export type CurrencyValuesState = { + selectedCurrency: string + availableCurrencies: string[] } -export type CurrencyValuesState = Map +export const initialState = { + selectedCurrency: 'USD', + availableCurrencies: ['USD'], +} -type CurrencyBasePayload = { safeAddress: string } -export type CurrencyRatePayload = CurrencyBasePayload & { currencyRate: number } -export type CurrencyBalancesPayload = CurrencyBasePayload & { currencyBalances: BalanceCurrencyList } -export type CurrentCurrencyPayload = CurrencyBasePayload & { selectedCurrency: string } +export type SelectedCurrencyPayload = { selectedCurrency: string } +export type AvailableCurrenciesPayload = { availableCurrencies: string[] } -export type CurrencyPayloads = CurrencyRatePayload | CurrencyBalancesPayload | CurrentCurrencyPayload - -export default handleActions( +export default handleActions( { - [SET_CURRENCY_RATE]: (state, action: Action) => { - const { currencyRate, safeAddress } = action.payload - - return state.setIn([safeAddress, 'currencyRate'], currencyRate) + [SET_CURRENT_CURRENCY]: (state, action: Action) => { + const { selectedCurrency } = action.payload + state.selectedCurrency = selectedCurrency + return state }, - [SET_CURRENCY_BALANCES]: (state, action: Action) => { - const { safeAddress, currencyBalances } = action.payload - - return state.setIn([safeAddress, 'currencyBalances'], currencyBalances) - }, - [SET_CURRENT_CURRENCY]: (state, action: Action) => { - const { safeAddress, selectedCurrency } = action.payload - - return state.setIn([safeAddress, 'selectedCurrency'], selectedCurrency) + [SET_AVAILABLE_CURRENCIES]: (state, action: Action) => { + const { availableCurrencies } = action.payload + state.availableCurrencies = availableCurrencies + return state }, }, - Map(), + initialState, ) diff --git a/src/logic/currencyValues/store/selectors/index.ts b/src/logic/currencyValues/store/selectors/index.ts index b1789174..e70adf05 100644 --- a/src/logic/currencyValues/store/selectors/index.ts +++ b/src/logic/currencyValues/store/selectors/index.ts @@ -1,53 +1,12 @@ -import { createSelector } from 'reselect' - -import { - CURRENCY_VALUES_KEY, - CurrencyReducerMap, - CurrencyValuesState, -} from 'src/logic/currencyValues/store/reducer/currencyValues' -import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' import { AppReduxState } from 'src/store' -import { CurrencyRateValue } from 'src/logic/currencyValues/store/model/currencyValues' -import { BigNumber } from 'bignumber.js' +import { CURRENCY_VALUES_KEY, CurrencyValuesState } from 'src/logic/currencyValues/store/reducer/currencyValues' export const currencyValuesSelector = (state: AppReduxState): CurrencyValuesState => state[CURRENCY_VALUES_KEY] -export const safeFiatBalancesSelector = createSelector( - currencyValuesSelector, - safeParamAddressFromStateSelector, - (currencyValues, safeAddress): CurrencyReducerMap | undefined => { - if (!currencyValues || !safeAddress) return - return currencyValues.get(safeAddress) - }, -) +export const currentCurrencySelector = (state: AppReduxState): string => { + return state[CURRENCY_VALUES_KEY].selectedCurrency +} -const currencyValueSelector = (key: K) => ( - currencyValuesMap?: CurrencyReducerMap, -): CurrencyRateValue[K] => currencyValuesMap?.get(key) - -export const safeFiatBalancesListSelector = createSelector( - safeFiatBalancesSelector, - currencyValueSelector('currencyBalances'), -) - -export const currentCurrencySelector = createSelector( - safeFiatBalancesSelector, - currencyValueSelector('selectedCurrency'), -) - -export const currencyRateSelector = createSelector(safeFiatBalancesSelector, currencyValueSelector('currencyRate')) - -export const safeFiatBalancesTotalSelector = createSelector( - safeFiatBalancesListSelector, - currencyRateSelector, - (currencyBalances, currencyRate): string | null => { - if (!currencyBalances) return '0' - if (!currencyRate) return null - - const totalInBaseCurrency = currencyBalances.reduce((total, balanceCurrencyRecord) => { - return total.plus(balanceCurrencyRecord.balanceInBaseCurrency) - }, new BigNumber(0)) - - return totalInBaseCurrency.times(currencyRate).toFixed(2) - }, -) +export const availableCurrenciesSelector = (state: AppReduxState): string[] => { + return state[CURRENCY_VALUES_KEY].availableCurrencies +} diff --git a/src/logic/currencyValues/__tests__/fetchSafeTokens.test.ts b/src/logic/safe/__tests__/fetchSafeTokens.test.ts similarity index 84% rename from src/logic/currencyValues/__tests__/fetchSafeTokens.test.ts rename to src/logic/safe/__tests__/fetchSafeTokens.test.ts index adc85654..4b9a4901 100644 --- a/src/logic/currencyValues/__tests__/fetchSafeTokens.test.ts +++ b/src/logic/safe/__tests__/fetchSafeTokens.test.ts @@ -1,10 +1,7 @@ import axios from 'axios' import { getSafeClientGatewayBaseUrl } from 'src/config' -import { - fetchTokenCurrenciesBalances, - BalanceEndpoint, -} from 'src/logic/currencyValues/api/fetchTokenCurrenciesBalances' +import { fetchTokenCurrenciesBalances } from 'src/logic/safe/api/fetchTokenCurrenciesBalances' import { aNewStore } from 'src/store' jest.mock('axios') @@ -52,11 +49,15 @@ describe('fetchTokenCurrenciesBalances', () => { axios.get.mockImplementationOnce(() => Promise.resolve({ data: expectedResult })) // when - const result = await fetchTokenCurrenciesBalances(safeAddress, excludeSpamTokens) + const result = await fetchTokenCurrenciesBalances({ + safeAddress, + excludeSpamTokens, + selectedCurrency: 'USD', + }) // then expect(result).toStrictEqual(expectedResult) expect(axios.get).toHaveBeenCalled() - expect(axios.get).toBeCalledWith(`${apiUrl}/balances/usd/?trusted=false&exclude_spam=${excludeSpamTokens}`) + expect(axios.get).toBeCalledWith(`${apiUrl}/balances/USD/?trusted=false&exclude_spam=${excludeSpamTokens}`) }) }) diff --git a/src/logic/currencyValues/api/fetchTokenCurrenciesBalances.ts b/src/logic/safe/api/fetchTokenCurrenciesBalances.ts similarity index 59% rename from src/logic/currencyValues/api/fetchTokenCurrenciesBalances.ts rename to src/logic/safe/api/fetchTokenCurrenciesBalances.ts index fd21f689..d1185a3d 100644 --- a/src/logic/currencyValues/api/fetchTokenCurrenciesBalances.ts +++ b/src/logic/safe/api/fetchTokenCurrenciesBalances.ts @@ -16,14 +16,22 @@ export type BalanceEndpoint = { items: TokenBalance[] } -export const fetchTokenCurrenciesBalances = ( - safeAddress: string, +type FetchTokenCurrenciesBalancesProps = { + safeAddress: string + selectedCurrency: string + excludeSpamTokens?: boolean + trustedTokens?: boolean +} + +export const fetchTokenCurrenciesBalances = async ({ + safeAddress, + selectedCurrency, excludeSpamTokens = true, trustedTokens = false, -): Promise => { +}: FetchTokenCurrenciesBalancesProps): Promise => { const url = `${getSafeClientGatewayBaseUrl( checksumAddress(safeAddress), - )}/balances/usd/?trusted=${trustedTokens}&exclude_spam=${excludeSpamTokens}` + )}/balances/${selectedCurrency}/?trusted=${trustedTokens}&exclude_spam=${excludeSpamTokens}` return axios.get(url).then(({ data }) => data) } diff --git a/src/logic/safe/hooks/useFetchTokens.tsx b/src/logic/safe/hooks/useFetchTokens.tsx index 7d19e373..829fefaf 100644 --- a/src/logic/safe/hooks/useFetchTokens.tsx +++ b/src/logic/safe/hooks/useFetchTokens.tsx @@ -1,35 +1,32 @@ import { useMemo } from 'react' -import { batch, useDispatch } from 'react-redux' +import { batch, useDispatch, useSelector } from 'react-redux' import { useLocation } from 'react-router-dom' import { fetchCollectibles } from 'src/logic/collectibles/store/actions/fetchCollectibles' import { fetchSelectedCurrency } from 'src/logic/currencyValues/store/actions/fetchSelectedCurrency' -import activateAssetsByBalance from 'src/logic/tokens/store/actions/activateAssetsByBalance' -import fetchSafeTokens from 'src/logic/tokens/store/actions/fetchSafeTokens' +import { fetchSafeTokens } from 'src/logic/tokens/store/actions/fetchSafeTokens' import { fetchTokens } from 'src/logic/tokens/store/actions/fetchTokens' import { COINS_LOCATION_REGEX, COLLECTIBLES_LOCATION_REGEX } from 'src/routes/safe/components/Balances' import { Dispatch } from 'src/logic/safe/store/actions/types.d' +import { currentCurrencySelector } from 'src/logic/currencyValues/store/selectors' export const useFetchTokens = (safeAddress: string): void => { const dispatch = useDispatch() const location = useLocation() + const currentCurrency = useSelector(currentCurrencySelector) useMemo(() => { if (COINS_LOCATION_REGEX.test(location.pathname)) { batch(() => { // fetch tokens there to get symbols for tokens in TXs list dispatch(fetchTokens()) - dispatch(fetchSelectedCurrency(safeAddress)) - dispatch(fetchSafeTokens(safeAddress)) + dispatch(fetchSelectedCurrency()) + dispatch(fetchSafeTokens(safeAddress, currentCurrency)) }) } if (COLLECTIBLES_LOCATION_REGEX.test(location.pathname)) { - batch(() => { - dispatch(fetchCollectibles(safeAddress)).then(() => { - dispatch(activateAssetsByBalance(safeAddress)) - }) - }) + dispatch(fetchCollectibles(safeAddress)) } - }, [dispatch, location.pathname, safeAddress]) + }, [dispatch, location.pathname, safeAddress, currentCurrency]) } diff --git a/src/logic/safe/hooks/useLoadSafe.tsx b/src/logic/safe/hooks/useLoadSafe.tsx index 116bf24c..bf07f60b 100644 --- a/src/logic/safe/hooks/useLoadSafe.tsx +++ b/src/logic/safe/hooks/useLoadSafe.tsx @@ -3,11 +3,12 @@ import { useDispatch } from 'react-redux' import loadAddressBookFromStorage from 'src/logic/addressBook/store/actions/loadAddressBookFromStorage' import addViewedSafe from 'src/logic/currentSession/store/actions/addViewedSafe' -import fetchSafeTokens from 'src/logic/tokens/store/actions/fetchSafeTokens' +import { fetchSafeTokens } from 'src/logic/tokens/store/actions/fetchSafeTokens' import fetchLatestMasterContractVersion from 'src/logic/safe/store/actions/fetchLatestMasterContractVersion' import fetchSafe from 'src/logic/safe/store/actions/fetchSafe' import fetchTransactions from 'src/logic/safe/store/actions/transactions/fetchTransactions' import { Dispatch } from 'src/logic/safe/store/actions/types.d' +import { updateAvailableCurrencies } from 'src/logic/currencyValues/store/actions/updateAvailableCurrencies' export const useLoadSafe = (safeAddress?: string): boolean => { const dispatch = useDispatch() @@ -20,6 +21,7 @@ export const useLoadSafe = (safeAddress?: string): boolean => { await dispatch(fetchSafe(safeAddress)) setIsSafeLoaded(true) await dispatch(fetchSafeTokens(safeAddress)) + dispatch(updateAvailableCurrencies()) dispatch(fetchTransactions(safeAddress)) dispatch(addViewedSafe(safeAddress)) } diff --git a/src/logic/safe/hooks/useSafeScheduledUpdates.tsx b/src/logic/safe/hooks/useSafeScheduledUpdates.tsx index a18f7349..aba374b4 100644 --- a/src/logic/safe/hooks/useSafeScheduledUpdates.tsx +++ b/src/logic/safe/hooks/useSafeScheduledUpdates.tsx @@ -2,8 +2,8 @@ import { useEffect, useRef } from 'react' import { batch, useDispatch } from 'react-redux' import { fetchCollectibles } from 'src/logic/collectibles/store/actions/fetchCollectibles' -import fetchSafeTokens from 'src/logic/tokens/store/actions/fetchSafeTokens' -import fetchEtherBalance from 'src/logic/safe/store/actions/fetchEtherBalance' +import { fetchSafeTokens } from 'src/logic/tokens/store/actions/fetchSafeTokens' +import { fetchEtherBalance } from 'src/logic/safe/store/actions/fetchEtherBalance' import { checkAndUpdateSafe } from 'src/logic/safe/store/actions/fetchSafe' import fetchTransactions from 'src/logic/safe/store/actions/transactions/fetchTransactions' import { TIMEOUT } from 'src/utils/constants' diff --git a/src/logic/safe/store/actions/activateTokenForAllSafes.ts b/src/logic/safe/store/actions/activateTokenForAllSafes.ts deleted file mode 100644 index 64b9b13c..00000000 --- a/src/logic/safe/store/actions/activateTokenForAllSafes.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { createAction } from 'redux-actions' - -export const ACTIVATE_TOKEN_FOR_ALL_SAFES = 'ACTIVATE_TOKEN_FOR_ALL_SAFES' - -const activateTokenForAllSafes = createAction(ACTIVATE_TOKEN_FOR_ALL_SAFES) - -export default activateTokenForAllSafes diff --git a/src/logic/safe/store/actions/fetchEtherBalance.ts b/src/logic/safe/store/actions/fetchEtherBalance.ts index 146a2033..8025f639 100644 --- a/src/logic/safe/store/actions/fetchEtherBalance.ts +++ b/src/logic/safe/store/actions/fetchEtherBalance.ts @@ -5,7 +5,7 @@ import { Dispatch } from 'redux' import { backOff } from 'exponential-backoff' import { AppReduxState } from 'src/store' -const fetchEtherBalance = (safeAddress: string) => async ( +export const fetchEtherBalance = (safeAddress: string) => async ( dispatch: Dispatch, getState: () => AppReduxState, ): Promise => { @@ -21,5 +21,3 @@ const fetchEtherBalance = (safeAddress: string) => async ( console.error('Error when fetching Ether balance:', err) } } - -export default fetchEtherBalance diff --git a/src/logic/safe/store/actions/fetchSafe.ts b/src/logic/safe/store/actions/fetchSafe.ts index 1c093913..d7dcba4f 100644 --- a/src/logic/safe/store/actions/fetchSafe.ts +++ b/src/logic/safe/store/actions/fetchSafe.ts @@ -80,6 +80,7 @@ export const buildSafe = async ( threshold, owners, ethBalance, + totalFiatBalance: 0, nonce, currentVersion: currentVersion ?? '', needsUpdate, @@ -88,8 +89,6 @@ export const buildSafe = async ( latestIncomingTxBlock: 0, activeAssets: Set(), activeTokens: Set(), - blacklistedAssets: Set(), - blacklistedTokens: Set(), modules, spendingLimits, } diff --git a/src/logic/safe/store/actions/updateActiveAssets.ts b/src/logic/safe/store/actions/updateActiveAssets.ts deleted file mode 100644 index 80be2139..00000000 --- a/src/logic/safe/store/actions/updateActiveAssets.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Set } from 'immutable' -import updateAssetsList from './updateAssetsList' -import { Dispatch } from 'src/logic/safe/store/actions/types.d' - -const updateActiveAssets = (safeAddress: string, activeAssets: Set) => (dispatch: Dispatch): void => { - dispatch(updateAssetsList({ safeAddress, activeAssets })) -} - -export default updateActiveAssets diff --git a/src/logic/safe/store/actions/updateActiveTokens.ts b/src/logic/safe/store/actions/updateActiveTokens.ts deleted file mode 100644 index 012836c9..00000000 --- a/src/logic/safe/store/actions/updateActiveTokens.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Set } from 'immutable' -import updateTokensList from './updateTokensList' -import { Dispatch } from 'src/logic/safe/store/actions/types.d' - -// the selector uses ownProps argument/router props to get the address of the safe -// so in order to use it I had to recreate the same structure -// const generateMatchProps = (safeAddress: string) => ({ -// match: { -// params: { -// [SAFE_PARAM_ADDRESS]: safeAddress, -// }, -// }, -// }) - -const updateActiveTokens = (safeAddress: string, activeTokens: Set) => (dispatch: Dispatch): void => { - dispatch(updateTokensList({ safeAddress, activeTokens })) -} - -export default updateActiveTokens diff --git a/src/logic/safe/store/actions/updateAssetsList.ts b/src/logic/safe/store/actions/updateAssetsList.ts deleted file mode 100644 index 0f098e28..00000000 --- a/src/logic/safe/store/actions/updateAssetsList.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { createAction } from 'redux-actions' - -export const UPDATE_ASSETS_LIST = 'UPDATE_ASSETS_LIST' - -const updateAssetsList = createAction(UPDATE_ASSETS_LIST) - -export default updateAssetsList diff --git a/src/logic/safe/store/actions/updateBlacklistedAssets.ts b/src/logic/safe/store/actions/updateBlacklistedAssets.ts deleted file mode 100644 index 8b52bbfd..00000000 --- a/src/logic/safe/store/actions/updateBlacklistedAssets.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Set } from 'immutable' -import updateAssetsList from './updateAssetsList' -import { Dispatch } from 'src/logic/safe/store/actions/types.d' - -const updateBlacklistedAssets = (safeAddress: string, blacklistedAssets: Set) => (dispatch: Dispatch): void => { - dispatch(updateAssetsList({ safeAddress, blacklistedAssets })) -} - -export default updateBlacklistedAssets diff --git a/src/logic/safe/store/actions/updateBlacklistedTokens.ts b/src/logic/safe/store/actions/updateBlacklistedTokens.ts deleted file mode 100644 index e81293b9..00000000 --- a/src/logic/safe/store/actions/updateBlacklistedTokens.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Set } from 'immutable' -import updateTokensList from './updateTokensList' -import { Dispatch } from 'src/logic/safe/store/actions/types.d' - -const updateBlacklistedTokens = (safeAddress: string, blacklistedTokens: Set) => (dispatch: Dispatch): void => { - dispatch(updateTokensList({ safeAddress, blacklistedTokens })) -} - -export default updateBlacklistedTokens diff --git a/src/logic/safe/store/actions/updateTokensList.ts b/src/logic/safe/store/actions/updateTokensList.ts deleted file mode 100644 index 91123bd6..00000000 --- a/src/logic/safe/store/actions/updateTokensList.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { createAction } from 'redux-actions' - -export const UPDATE_TOKENS_LIST = 'UPDATE_TOKENS_LIST' - -const updateTokenList = createAction(UPDATE_TOKENS_LIST) - -export default updateTokenList diff --git a/src/logic/safe/store/middleware/safeStorage.ts b/src/logic/safe/store/middleware/safeStorage.ts index 90b26346..d51be225 100644 --- a/src/logic/safe/store/middleware/safeStorage.ts +++ b/src/logic/safe/store/middleware/safeStorage.ts @@ -1,7 +1,4 @@ import { saveDefaultSafe, saveSafes } from 'src/logic/safe/utils' -import { tokensSelector } from 'src/logic/tokens/store/selectors' -import { saveActiveTokens } from 'src/logic/tokens/utils/tokensStorage' -import { ACTIVATE_TOKEN_FOR_ALL_SAFES } from 'src/logic/safe/store/actions/activateTokenForAllSafes' import { ADD_SAFE_OWNER } from 'src/logic/safe/store/actions/addSafeOwner' import { EDIT_SAFE_OWNER } from 'src/logic/safe/store/actions/editSafeOwner' import { REMOVE_SAFE } from 'src/logic/safe/store/actions/removeSafe' @@ -9,9 +6,7 @@ import { REMOVE_SAFE_OWNER } from 'src/logic/safe/store/actions/removeSafeOwner' import { REPLACE_SAFE_OWNER } from 'src/logic/safe/store/actions/replaceSafeOwner' import { SET_DEFAULT_SAFE } from 'src/logic/safe/store/actions/setDefaultSafe' import { UPDATE_SAFE } from 'src/logic/safe/store/actions/updateSafe' -import { UPDATE_TOKENS_LIST } from 'src/logic/safe/store/actions/updateTokensList' -import { UPDATE_ASSETS_LIST } from 'src/logic/safe/store/actions/updateAssetsList' -import { getActiveTokensAddressesForAllSafes, safesMapSelector } from 'src/logic/safe/store/selectors' +import { safesMapSelector } from 'src/logic/safe/store/selectors' import { ADD_OR_UPDATE_SAFE } from 'src/logic/safe/store/actions/addOrUpdateSafe' import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook' import { checksumAddress } from 'src/utils/checksumAddress' @@ -26,28 +21,10 @@ const watchedActions = [ REMOVE_SAFE_OWNER, REPLACE_SAFE_OWNER, EDIT_SAFE_OWNER, - ACTIVATE_TOKEN_FOR_ALL_SAFES, - UPDATE_TOKENS_LIST, - UPDATE_ASSETS_LIST, SET_DEFAULT_SAFE, ] -const recalculateActiveTokens = (state) => { - const tokens = tokensSelector(state) - const activeTokenAddresses = getActiveTokensAddressesForAllSafes(state) - - const activeTokens = tokens.withMutations((map) => { - map.forEach((token) => { - if (!activeTokenAddresses.has(token.address)) { - map.remove(token.address) - } - }) - }) - - saveActiveTokens(activeTokens) -} - -const safeStorageMware = (store) => (next) => async (action) => { +export const safeStorageMiddleware = (store) => (next) => async (action) => { const handledAction = next(action) if (watchedActions.includes(action.type)) { @@ -57,10 +34,6 @@ const safeStorageMware = (store) => (next) => async (action) => { await saveSafes(safes.toJSON()) switch (action.type) { - case ACTIVATE_TOKEN_FOR_ALL_SAFES: { - recalculateActiveTokens(state) - break - } case ADD_OR_UPDATE_SAFE: { const { safe } = action.payload safe.owners.forEach((owner) => { @@ -72,10 +45,7 @@ const safeStorageMware = (store) => (next) => async (action) => { break } case UPDATE_SAFE: { - const { activeTokens, name, address } = action.payload - if (activeTokens) { - recalculateActiveTokens(state) - } + const { name, address } = action.payload if (name) { dispatch(addOrUpdateAddressBookEntry(makeAddressBookEntry({ name, address }))) } @@ -94,5 +64,3 @@ const safeStorageMware = (store) => (next) => async (action) => { return handledAction } - -export default safeStorageMware diff --git a/src/logic/safe/store/models/safe.ts b/src/logic/safe/store/models/safe.ts index 7d0547e0..38616f17 100644 --- a/src/logic/safe/store/models/safe.ts +++ b/src/logic/safe/store/models/safe.ts @@ -1,5 +1,6 @@ import { List, Map, Record, RecordOf, Set } from 'immutable' import { FEATURES } from 'src/config/networks/network.d' +import { BalanceRecord } from 'src/logic/tokens/store/actions/fetchSafeTokens' export type SafeOwner = { name: string @@ -28,14 +29,13 @@ export type SafeRecordProps = { address: string threshold: number ethBalance: string + totalFiatBalance: number owners: List modules?: ModulePair[] | null spendingLimits?: SpendingLimit[] | null activeTokens: Set activeAssets: Set - blacklistedTokens: Set - blacklistedAssets: Set - balances: Map + balances: Map nonce: number latestIncomingTxBlock: number recurringUser?: boolean @@ -49,13 +49,12 @@ const makeSafe = Record({ address: '', threshold: 0, ethBalance: '0', + totalFiatBalance: 0, owners: List([]), modules: [], spendingLimits: [], activeTokens: Set(), activeAssets: Set(), - blacklistedTokens: Set(), - blacklistedAssets: Set(), balances: Map(), nonce: 0, latestIncomingTxBlock: 0, diff --git a/src/logic/safe/store/reducer/safe.ts b/src/logic/safe/store/reducer/safe.ts index d327e560..44112c4b 100644 --- a/src/logic/safe/store/reducer/safe.ts +++ b/src/logic/safe/store/reducer/safe.ts @@ -1,7 +1,6 @@ import { Map, Set, List } from 'immutable' import { Action, handleActions } from 'redux-actions' -import { ACTIVATE_TOKEN_FOR_ALL_SAFES } from 'src/logic/safe/store/actions/activateTokenForAllSafes' import { ADD_SAFE_OWNER } from 'src/logic/safe/store/actions/addSafeOwner' import { EDIT_SAFE_OWNER } from 'src/logic/safe/store/actions/editSafeOwner' import { REMOVE_SAFE } from 'src/logic/safe/store/actions/removeSafe' @@ -10,8 +9,6 @@ import { REPLACE_SAFE_OWNER } from 'src/logic/safe/store/actions/replaceSafeOwne import { SET_DEFAULT_SAFE } from 'src/logic/safe/store/actions/setDefaultSafe' import { SET_LATEST_MASTER_CONTRACT_VERSION } from 'src/logic/safe/store/actions/setLatestMasterContractVersion' import { UPDATE_SAFE } from 'src/logic/safe/store/actions/updateSafe' -import { UPDATE_TOKENS_LIST } from 'src/logic/safe/store/actions/updateTokensList' -import { UPDATE_ASSETS_LIST } from 'src/logic/safe/store/actions/updateAssetsList' import { makeOwner } from 'src/logic/safe/store/models/owner' import makeSafe, { SafeRecord, SafeRecordProps } from 'src/logic/safe/store/models/safe' import { AppReduxState } from 'src/store' @@ -29,8 +26,6 @@ export const buildSafe = (storedSafe: SafeRecordProps): SafeRecordProps => { const owners = buildOwnersFrom(Array.from(names), Array.from(addresses)) const activeTokens = Set(storedSafe.activeTokens) const activeAssets = Set(storedSafe.activeAssets) - const blacklistedTokens = Set(storedSafe.blacklistedTokens) - const blacklistedAssets = Set(storedSafe.blacklistedAssets) const balances = Map(storedSafe.balances) return { @@ -38,9 +33,7 @@ export const buildSafe = (storedSafe: SafeRecordProps): SafeRecordProps => { owners, balances, activeTokens, - blacklistedTokens, activeAssets, - blacklistedAssets, latestIncomingTxBlock: 0, modules: null, } @@ -102,21 +95,6 @@ export default handleActions( ) : state }, - [ACTIVATE_TOKEN_FOR_ALL_SAFES]: (state, action: Action) => { - const tokenAddress = action.payload - - return state.withMutations((map) => { - map - .get('safes') - .keySeq() - .forEach((safeAddress) => { - const safeActiveTokens = map.getIn(['safes', safeAddress, 'activeTokens']) - const activeTokens = safeActiveTokens.add(tokenAddress) - - map.updateIn(['safes', safeAddress], (prevSafe) => prevSafe.mergeDeep({ activeTokens })) - }) - }) - }, [ADD_OR_UPDATE_SAFE]: (state, action: Action) => { const { safe } = action.payload const safeAddress = safe.address @@ -195,24 +173,6 @@ export default handleActions( return prevSafe.merge({ owners: updatedOwners }) }) }, - [UPDATE_TOKENS_LIST]: (state, action: Action) => { - // Only activeTokens or blackListedTokens is required - const { safeAddress, activeTokens, blacklistedTokens } = action.payload - - const key = activeTokens ? 'activeTokens' : 'blacklistedTokens' - const list = activeTokens ?? blacklistedTokens - - return state.updateIn(['safes', safeAddress], (prevSafe) => prevSafe.set(key, list)) - }, - [UPDATE_ASSETS_LIST]: (state, action: Action) => { - // Only activeAssets or blackListedAssets is required - const { safeAddress, activeAssets, blacklistedAssets } = action.payload - - const key = activeAssets ? 'activeAssets' : 'blacklistedAssets' - const list = activeAssets ?? blacklistedAssets - - return state.updateIn(['safes', safeAddress], (prevSafe) => prevSafe.set(key, list)) - }, [SET_DEFAULT_SAFE]: (state, action: Action) => state.set('defaultSafe', action.payload), [SET_LATEST_MASTER_CONTRACT_VERSION]: (state, action: Action) => state.set('latestMasterContractVersion', action.payload), diff --git a/src/logic/safe/store/selectors/index.ts b/src/logic/safe/store/selectors/index.ts index 7675d0a9..2c9ebd7d 100644 --- a/src/logic/safe/store/selectors/index.ts +++ b/src/logic/safe/store/selectors/index.ts @@ -76,51 +76,6 @@ export const safeActiveTokensSelector = createSelector( }, ) -export const safeActiveAssetsSelector = createSelector( - safeSelector, - (safe): Set => { - if (!safe) { - return Set() - } - return safe.activeAssets - }, -) - -export const safeActiveAssetsListSelector = createSelector(safeActiveAssetsSelector, (safeList) => { - if (!safeList) { - return Set([]) - } - return Set(safeList) -}) - -export const safeBlacklistedTokensSelector = createSelector( - safeSelector, - (safe): Set => { - if (!safe) { - return Set() - } - - return safe.blacklistedTokens - }, -) - -export const safeBlacklistedAssetsSelector = createSelector( - safeSelector, - (safe): Set => { - if (!safe) { - return Set() - } - - return safe.blacklistedAssets - }, -) - -export const safeActiveAssetsSelectorBySafe = (safeAddress: string, safes: SafesMap): Set => - safes.get(safeAddress)?.get('activeAssets') || Set() - -export const safeBlacklistedAssetsSelectorBySafe = (safeAddress: string, safes: SafesMap): Set => - safes.get(safeAddress)?.get('blacklistedAssets') || Set() - const baseSafe = makeSafe() export const safeFieldSelector = (field: K) => ( @@ -172,14 +127,6 @@ export const getActiveTokensAddressesForAllSafes = createSelector(safesListSelec return addresses }) -export const getBlacklistedTokensAddressesForAllSafes = createSelector(safesListSelector, (safes) => { - const addresses = Set().withMutations((set) => { - safes.forEach((safe) => { - safe.blacklistedTokens.forEach((tokenAddress) => { - set.add(tokenAddress) - }) - }) - }) - - return addresses +export const safeFiatBalancesTotalSelector = createSelector(safeSelector, (currentSafe) => { + return currentSafe?.totalFiatBalance.toString() }) diff --git a/src/logic/safe/store/tests/safe.balances.test.ts b/src/logic/safe/store/tests/safe.balances.test.ts deleted file mode 100644 index 4238d4c3..00000000 --- a/src/logic/safe/store/tests/safe.balances.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { Set, Map } from 'immutable' -import { aNewStore } from 'src/store' -import updateActiveTokens from 'src/logic/safe/store/actions/updateActiveTokens' -import '@testing-library/jest-dom/extend-expect' -import updateSafe from 'src/logic/safe/store/actions/updateSafe' -import { makeToken } from 'src/logic/tokens/store/model/token' -import { safesMapSelector } from 'src/logic/safe/store/selectors' - -describe('Feature > Balances', () => { - let store - const safeAddress = '0xdfA693da0D16F5E7E78FdCBeDe8FC6eBEa44f1Cf' - beforeEach(async () => { - store = aNewStore() - }) - - it('It should return an updated balance when updates active tokens', async () => { - // given - const tokensAmount = '100' - const token = makeToken({ - address: '0x00Df91984582e6e96288307E9c2f20b38C8FeCE9', - name: 'OmiseGo', - symbol: 'OMG', - decimals: 18, - logoUri: - 'https://github.com/TrustWallet/tokens/blob/master/images/0x6810e776880c02933d47db1b9fc05908e5386b96.png?raw=true', - }) - const balances = Map({ - [token.address]: tokensAmount, - }) - const expectedResult = '100' - - // when - store.dispatch(updateSafe({ address: safeAddress, balances })) - store.dispatch(updateActiveTokens(safeAddress, Set([token.address]))) - - const safe = safesMapSelector(store.getState()).get(safeAddress) - const balanceResult = safe?.get('balances').get(token.address) - const activeTokens = safe?.get('activeTokens') - const tokenIsActive = activeTokens?.has(token.address) - - // then - expect(balanceResult).toBe(expectedResult) - expect(tokenIsActive).toBe(true) - }) - - it('The store should have an updated ether balance after updating the value', async () => { - // given - const etherAmount = '1' - const expectedResult = '1' - - // when - store.dispatch(updateSafe({ address: safeAddress, ethBalance: etherAmount })) - const safe = safesMapSelector(store.getState()).get(safeAddress) - const balanceResult = safe?.get('ethBalance') - - // then - expect(balanceResult).toBe(expectedResult) - }) -}) diff --git a/src/logic/safe/utils/__tests__/shouldSafeStoreBeUpdated.test.ts b/src/logic/safe/utils/__tests__/shouldSafeStoreBeUpdated.test.ts index 5d029744..51ca8521 100644 --- a/src/logic/safe/utils/__tests__/shouldSafeStoreBeUpdated.test.ts +++ b/src/logic/safe/utils/__tests__/shouldSafeStoreBeUpdated.test.ts @@ -7,9 +7,7 @@ const getMockedOldSafe = ({ needsUpdate, balances, recurringUser, - blacklistedAssets, - blacklistedTokens, - activeAssets, + assets, activeTokens, owners, featuresEnabled, @@ -34,8 +32,6 @@ const getMockedOldSafe = ({ const mockedActiveTokenAddress2 = '0x92aF97cbF10742dD2527ffaBA70e34C03CFFC2c1' const mockedActiveAssetsAddress1 = '0x503ab2a6A70c6C6ec8b25a4C87C784e1c8f8e8CD' const mockedActiveAssetsAddress2 = '0xfdd4E685361CB7E89a4D27e03DCd0001448d731F' - const mockedBlacklistedTokenAddress1 = '0xc7d892dca37a244Fb1A7461e6141e58Ead460282' - const mockedBlacklistedAssetAddress1 = '0x0ac539137c4c99001f16Dd132E282F99A02Ddc3F' return { name: name || 'MockedSafe', @@ -46,14 +42,12 @@ const getMockedOldSafe = ({ modules: modules || [], spendingLimits: spendingLimits || [], activeTokens: activeTokens || Set([mockedActiveTokenAddress1, mockedActiveTokenAddress2]), - activeAssets: activeAssets || Set([mockedActiveAssetsAddress1, mockedActiveAssetsAddress2]), - blacklistedTokens: blacklistedTokens || Set([mockedBlacklistedTokenAddress1]), - blacklistedAssets: blacklistedAssets || Set([mockedBlacklistedAssetAddress1]), + assets: assets || Set([mockedActiveAssetsAddress1, mockedActiveAssetsAddress2]), balances: balances || Map({ - [mockedActiveTokenAddress1]: '100', - [mockedActiveTokenAddress2]: '10', + [mockedActiveTokenAddress1]: { tokenBalance: '100' }, + [mockedActiveTokenAddress2]: { tokenBalance: '10' }, }), nonce: nonce || 2, latestIncomingTxBlock: latestIncomingTxBlock || 1, @@ -61,6 +55,7 @@ const getMockedOldSafe = ({ currentVersion: currentVersion || 'v1.1.1', needsUpdate: needsUpdate || false, featuresEnabled: featuresEnabled || [], + totalFiatBalance: 110, } } @@ -209,43 +204,9 @@ describe('shouldSafeStoreBeUpdated', () => { const mockedActiveTokenAddress2 = '0x92aF97cbF10742dD2527ffaBA70e34C03CFFC2c1' const oldActiveAssets = Set([mockedActiveTokenAddress1, mockedActiveTokenAddress2]) const newActiveAssets = Set([mockedActiveTokenAddress1]) - const oldSafe = getMockedOldSafe({ activeAssets: oldActiveAssets }) + const oldSafe = getMockedOldSafe({ assets: oldActiveAssets }) const newSafeProps: Partial = { - activeAssets: newActiveAssets, - } - - // When - const expectedResult = shouldSafeStoreBeUpdated(newSafeProps, oldSafe) - - // Then - expect(expectedResult).toEqual(true) - }) - it(`Given an old blacklistedTokens list and a new blacklistedTokens list for the safe, should return true`, () => { - // given - const mockedActiveTokenAddress1 = '0x36591cd3DA96b21Ac9ca54cFaf80fe45107294F1' - const mockedActiveTokenAddress2 = '0x92aF97cbF10742dD2527ffaBA70e34C03CFFC2c1' - const oldBlacklistedTokens = Set([mockedActiveTokenAddress1, mockedActiveTokenAddress2]) - const newBlacklistedTokens = Set([mockedActiveTokenAddress1]) - const oldSafe = getMockedOldSafe({ blacklistedTokens: oldBlacklistedTokens }) - const newSafeProps: Partial = { - blacklistedTokens: newBlacklistedTokens, - } - - // When - const expectedResult = shouldSafeStoreBeUpdated(newSafeProps, oldSafe) - - // Then - expect(expectedResult).toEqual(true) - }) - it(`Given an old blacklistedAssets list and a new blacklistedAssets list for the safe, should return true`, () => { - // given - const mockedActiveTokenAddress1 = '0x36591cd3DA96b21Ac9ca54cFaf80fe45107294F1' - const mockedActiveTokenAddress2 = '0x92aF97cbF10742dD2527ffaBA70e34C03CFFC2c1' - const oldBlacklistedAssets = Set([mockedActiveTokenAddress1, mockedActiveTokenAddress2]) - const newBlacklistedAssets = Set([mockedActiveTokenAddress1]) - const oldSafe = getMockedOldSafe({ blacklistedAssets: oldBlacklistedAssets }) - const newSafeProps: Partial = { - blacklistedAssets: newBlacklistedAssets, + assets: newActiveAssets, } // When @@ -259,11 +220,11 @@ describe('shouldSafeStoreBeUpdated', () => { const mockedActiveTokenAddress1 = '0x36591cd3DA96b21Ac9ca54cFaf80fe45107294F1' const mockedActiveTokenAddress2 = '0x92aF97cbF10742dD2527ffaBA70e34C03CFFC2c1' const oldBalances = Map({ - [mockedActiveTokenAddress1]: '100', - [mockedActiveTokenAddress2]: '10', + [mockedActiveTokenAddress1]: { tokenBalance: '100' }, + [mockedActiveTokenAddress2]: { tokenBalance: '100' }, }) const newBalances = Map({ - [mockedActiveTokenAddress1]: '100', + [mockedActiveTokenAddress1]: { tokenBalance: '100' }, }) const oldSafe = getMockedOldSafe({ balances: oldBalances }) const newSafeProps: Partial = { diff --git a/src/logic/currencyValues/store/utils/currencyValuesStorage.ts b/src/logic/safe/utils/currencyValuesStorage.ts similarity index 100% rename from src/logic/currencyValues/store/utils/currencyValuesStorage.ts rename to src/logic/safe/utils/currencyValuesStorage.ts diff --git a/src/logic/tokens/store/actions/activateAssetsByBalance.ts b/src/logic/tokens/store/actions/activateAssetsByBalance.ts deleted file mode 100644 index eae19074..00000000 --- a/src/logic/tokens/store/actions/activateAssetsByBalance.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { nftAssetsSelector } from 'src/logic/collectibles/store/selectors' -import updateActiveAssets from 'src/logic/safe/store/actions/updateActiveAssets' -import { - safeActiveAssetsSelectorBySafe, - safeBlacklistedAssetsSelectorBySafe, - safesMapSelector, -} from 'src/logic/safe/store/selectors' - -const activateAssetsByBalance = (safeAddress) => async (dispatch, getState) => { - try { - const state = getState() - const safes = safesMapSelector(state) - - if (safes.size === 0) { - return - } - - const availableAssets = nftAssetsSelector(state) - const alreadyActiveAssets = safeActiveAssetsSelectorBySafe(safeAddress, safes) - const blacklistedAssets = safeBlacklistedAssetsSelectorBySafe(safeAddress, safes) - - // active tokens by balance, excluding those already blacklisted and the `null` address - const activeByBalance = Object.entries(availableAssets) - .filter((asset) => { - const { address, numberOfTokens }: any = asset[1] - return address !== null && !blacklistedAssets.has(address) && numberOfTokens > 0 - }) - .map((asset) => { - return asset[0] - }) - - // need to persist those already active assets, despite its balances - const activeAssets = alreadyActiveAssets.union(activeByBalance) - - // update list of active tokens - dispatch(updateActiveAssets(safeAddress, activeAssets)) - } catch (err) { - console.error('Error fetching active assets list', err) - } - - return null -} - -export default activateAssetsByBalance diff --git a/src/logic/tokens/store/actions/saveTokens.ts b/src/logic/tokens/store/actions/addTokens.ts similarity index 54% rename from src/logic/tokens/store/actions/saveTokens.ts rename to src/logic/tokens/store/actions/addTokens.ts index 0aa58838..f5367cce 100644 --- a/src/logic/tokens/store/actions/saveTokens.ts +++ b/src/logic/tokens/store/actions/addTokens.ts @@ -2,8 +2,6 @@ import { createAction } from 'redux-actions' export const ADD_TOKENS = 'ADD_TOKENS' -const addTokens = createAction(ADD_TOKENS, (tokens) => ({ +export const addTokens = createAction(ADD_TOKENS, (tokens) => ({ tokens, })) - -export default addTokens diff --git a/src/logic/tokens/store/actions/fetchSafeTokens.ts b/src/logic/tokens/store/actions/fetchSafeTokens.ts index fb585e94..227c49b8 100644 --- a/src/logic/tokens/store/actions/fetchSafeTokens.ts +++ b/src/logic/tokens/store/actions/fetchSafeTokens.ts @@ -2,63 +2,56 @@ import { backOff } from 'exponential-backoff' import { List, Map } from 'immutable' import { Dispatch } from 'redux' -import { fetchTokenCurrenciesBalances, TokenBalance } from 'src/logic/currencyValues/api/fetchTokenCurrenciesBalances' - -import { - AVAILABLE_CURRENCIES, - CurrencyRateValueRecord, - makeBalanceCurrency, -} from 'src/logic/currencyValues/store/model/currencyValues' -import addTokens from 'src/logic/tokens/store/actions/saveTokens' +import { fetchTokenCurrenciesBalances, TokenBalance } from 'src/logic/safe/api/fetchTokenCurrenciesBalances' +import { addTokens } from 'src/logic/tokens/store/actions/addTokens' import { makeToken, Token } from 'src/logic/tokens/store/model/token' import { TokenState } from 'src/logic/tokens/store/reducer/tokens' import updateSafe from 'src/logic/safe/store/actions/updateSafe' import { AppReduxState } from 'src/store' import { humanReadableValue } from 'src/logic/tokens/utils/humanReadableValue' -import { safeActiveTokensSelector, safeBlacklistedTokensSelector, safeSelector } from 'src/logic/safe/store/selectors' +import { safeActiveTokensSelector, safeSelector } from 'src/logic/safe/store/selectors' import { tokensSelector } from 'src/logic/tokens/store/selectors' -import { currentCurrencySelector } from 'src/logic/currencyValues/store/selectors' import { sameAddress, ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses' -import { setCurrencyBalances } from 'src/logic/currencyValues/store/actions/setCurrencyBalances' import { getNetworkInfo } from 'src/config' +import BigNumber from 'bignumber.js' +import { currentCurrencySelector } from 'src/logic/currencyValues/store/selectors' + +export type BalanceRecord = { + tokenBalance: string + fiatBalance?: string +} interface ExtractedData { - balances: Map - currencyList: List + balances: Map ethBalance: string tokens: List } const { nativeCoin } = getNetworkInfo() -const extractDataFromResult = (currentTokens: TokenState, fiatCode: string) => ( +const extractDataFromResult = (currentTokens: TokenState) => ( acc: ExtractedData, { balance, fiatBalance, tokenInfo }: TokenBalance, ): ExtractedData => { const { address: tokenAddress, decimals } = tokenInfo if (sameAddress(tokenAddress, ZERO_ADDRESS) || sameAddress(tokenAddress, nativeCoin.address)) { acc.ethBalance = humanReadableValue(balance, 18) - } else { - acc.balances = acc.balances.merge({ [tokenAddress]: humanReadableValue(balance, Number(decimals)) }) - - if (currentTokens && !currentTokens.get(tokenAddress)) { - acc.tokens = acc.tokens.push(makeToken({ ...tokenInfo })) - } } + acc.balances = acc.balances.merge({ + [tokenAddress]: { + fiatBalance, + tokenBalance: humanReadableValue(balance, Number(decimals)), + }, + }) - acc.currencyList = acc.currencyList.push( - makeBalanceCurrency({ - currencyName: fiatCode, - tokenAddress, - balanceInBaseCurrency: fiatBalance, - balanceInSelectedCurrency: fiatBalance, - }), - ) + if (currentTokens && !currentTokens.get(tokenAddress)) { + acc.tokens = acc.tokens.push(makeToken({ ...tokenInfo })) + } return acc } -const fetchSafeTokens = (safeAddress: string) => async ( +export const fetchSafeTokens = (safeAddress: string, currencySelected?: string) => async ( dispatch: Dispatch, getState: () => AppReduxState, ): Promise => { @@ -66,38 +59,40 @@ const fetchSafeTokens = (safeAddress: string) => async ( const state = getState() const safe = safeSelector(state) const currentTokens = tokensSelector(state) - const currencySelected = currentCurrencySelector(state) if (!safe) { return } + const selectedCurrency = currentCurrencySelector(state) - const tokenCurrenciesBalances = await backOff(() => fetchTokenCurrenciesBalances(safeAddress)) + const tokenCurrenciesBalances = await backOff(() => + fetchTokenCurrenciesBalances({ safeAddress, selectedCurrency: currencySelected ?? selectedCurrency }), + ) const alreadyActiveTokens = safeActiveTokensSelector(state) - const blacklistedTokens = safeBlacklistedTokensSelector(state) - const { balances, currencyList, ethBalance, tokens } = tokenCurrenciesBalances.items.reduce( - extractDataFromResult(currentTokens, currencySelected || AVAILABLE_CURRENCIES.USD), + const { balances, ethBalance, tokens } = tokenCurrenciesBalances.items.reduce( + extractDataFromResult(currentTokens), { balances: Map(), - currencyList: List(), ethBalance: '0', tokens: List(), }, ) // need to persist those already active tokens, despite its balances - const activeTokens = alreadyActiveTokens.union( - // active tokens by balance, excluding those already blacklisted and the `null` address - balances.keySeq().toSet().subtract(blacklistedTokens), - ) + const activeTokens = alreadyActiveTokens.union(balances.keySeq().toSet()) - dispatch(updateSafe({ address: safeAddress, activeTokens, balances, ethBalance })) - dispatch(setCurrencyBalances(safeAddress, currencyList)) + dispatch( + updateSafe({ + address: safeAddress, + activeTokens, + balances, + ethBalance, + totalFiatBalance: new BigNumber(tokenCurrenciesBalances.fiatTotal).toFixed(2), + }), + ) dispatch(addTokens(tokens)) } catch (err) { console.error('Error fetching active token list', err) } } - -export default fetchSafeTokens diff --git a/src/logic/tokens/store/actions/fetchTokens.ts b/src/logic/tokens/store/actions/fetchTokens.ts index 20e170af..d00dde76 100644 --- a/src/logic/tokens/store/actions/fetchTokens.ts +++ b/src/logic/tokens/store/actions/fetchTokens.ts @@ -5,9 +5,7 @@ import ERC721 from '@openzeppelin/contracts/build/contracts/ERC721.json' import { List } from 'immutable' import contract from '@truffle/contract/index.js' import { AbiItem } from 'web3-utils' - -import saveTokens from './saveTokens' - +import { addTokens } from 'src/logic/tokens/store/actions/addTokens' import generateBatchRequests from 'src/logic/contracts/generateBatchRequests' import { fetchErc20AndErc721AssetsList } from 'src/logic/tokens/api' import { makeToken, Token } from 'src/logic/tokens/store/model/token' @@ -85,7 +83,7 @@ export const getTokenInfos = async (tokenAddress: string): Promise async ( const tokens = List(erc20Tokens.map((token) => makeToken(token))) - dispatch(saveTokens(tokens)) + dispatch(addTokens(tokens)) } catch (err) { console.error('Error fetching token list', err) } } - -export default fetchTokens diff --git a/src/logic/tokens/store/actions/loadActiveTokens.ts b/src/logic/tokens/store/actions/loadActiveTokens.ts deleted file mode 100644 index 7b4ae61b..00000000 --- a/src/logic/tokens/store/actions/loadActiveTokens.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { List } from 'immutable' - -import saveTokens from './saveTokens' - -import { makeToken } from 'src/logic/tokens/store/model/token' -import { getActiveTokens } from 'src/logic/tokens/utils/tokensStorage' - -const loadActiveTokens = () => async (dispatch) => { - try { - const tokens = (await getActiveTokens()) || {} - // The filter of strings was made because of the issue #751. Please see: https://github.com/gnosis/safe-react/pull/755#issuecomment-612969340 - const tokenRecordsList = List( - Object.values(tokens) - .filter((t: any) => typeof t.decimals !== 'string') - .map((token) => makeToken(token)), - ) - - dispatch(saveTokens(tokenRecordsList)) - } catch (err) { - // eslint-disable-next-line - console.error('Error while loading active tokens from storage:', err) - } -} - -export default loadActiveTokens diff --git a/src/logic/tokens/store/model/token.ts b/src/logic/tokens/store/model/token.ts index e0c612bb..3e9b5bcd 100644 --- a/src/logic/tokens/store/model/token.ts +++ b/src/logic/tokens/store/model/token.ts @@ -1,5 +1,6 @@ import { Record, RecordOf } from 'immutable' import { TokenType } from 'src/logic/safe/store/models/types/gateway' +import { BalanceRecord } from 'src/logic/tokens/store/actions/fetchSafeTokens' export type TokenProps = { address: string @@ -7,7 +8,7 @@ export type TokenProps = { symbol: string decimals: number | string logoUri: string - balance: number | string + balance: BalanceRecord type?: TokenType } @@ -17,7 +18,10 @@ export const makeToken = Record({ symbol: '', decimals: 0, logoUri: '', - balance: 0, + balance: { + fiatBalance: '0', + tokenBalance: '0', + }, }) // balance is only set in extendedSafeTokensSelector when we display user's token balances diff --git a/src/logic/tokens/store/reducer/tokens.ts b/src/logic/tokens/store/reducer/tokens.ts index c567e630..bd506670 100644 --- a/src/logic/tokens/store/reducer/tokens.ts +++ b/src/logic/tokens/store/reducer/tokens.ts @@ -2,7 +2,7 @@ import { List, Map } from 'immutable' import { Action, handleActions } from 'redux-actions' import { ADD_TOKEN } from 'src/logic/tokens/store/actions/addToken' -import { ADD_TOKENS } from 'src/logic/tokens/store/actions/saveTokens' +import { ADD_TOKENS } from 'src/logic/tokens/store/actions/addTokens' import { makeToken, Token } from 'src/logic/tokens/store/model/token' import { AppReduxState } from 'src/store' diff --git a/src/logic/tokens/utils/tokenHelpers.ts b/src/logic/tokens/utils/tokenHelpers.ts index 9dc7d305..43135dbd 100644 --- a/src/logic/tokens/utils/tokenHelpers.ts +++ b/src/logic/tokens/utils/tokenHelpers.ts @@ -15,7 +15,9 @@ export const getEthAsToken = (balance: string | number): Token => { const { nativeCoin } = getNetworkInfo() return makeToken({ ...nativeCoin, - balance, + balance: { + tokenBalance: balance.toString(), + }, }) } @@ -73,7 +75,7 @@ export type GetTokenByAddress = { tokens: List } -export type TokenFound = { +type TokenFound = { balance: string | number decimals: string | number } @@ -92,7 +94,7 @@ export const getBalanceAndDecimalsFromToken = ({ tokenAddress, tokens }: GetToke } return { - balance: token.balance ?? 0, + balance: token.balance.tokenBalance ?? 0, decimals: token.decimals ?? 0, } } diff --git a/src/logic/tokens/utils/tokensStorage.ts b/src/logic/tokens/utils/tokensStorage.ts deleted file mode 100644 index 27364f24..00000000 --- a/src/logic/tokens/utils/tokensStorage.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Map } from 'immutable' - -import { loadFromStorage, saveToStorage } from 'src/utils/storage' -import { TokenProps, Token } from './../store/model/token' - -export const ACTIVE_TOKENS_KEY = 'ACTIVE_TOKENS' -export const CUSTOM_TOKENS_KEY = 'CUSTOM_TOKENS' - -// Tokens which are active at least in one of used Safes in the app should be saved to localstorage -// to avoid iterating a large amount of data of tokens from the backend -// Custom tokens should be saved too unless they're deleted (marking them as inactive doesn't count) - -export const saveActiveTokens = async (tokens: Map): Promise => { - try { - await saveToStorage(ACTIVE_TOKENS_KEY, tokens.toJS() as Record) - } catch (err) { - console.error('Error storing tokens in localstorage', err) - } -} - -export const getActiveTokens = async (): Promise | undefined> => { - const data = await loadFromStorage>(ACTIVE_TOKENS_KEY) - - return data -} diff --git a/src/routes/safe/components/Balances/Coins/index.tsx b/src/routes/safe/components/Balances/Coins/index.tsx index 7b72922f..20335252 100644 --- a/src/routes/safe/components/Balances/Coins/index.tsx +++ b/src/routes/safe/components/Balances/Coins/index.tsx @@ -14,11 +14,6 @@ import Table from 'src/components/Table' import { cellWidth } from 'src/components/Table/TableHead' import Button from 'src/components/layout/Button' import Row from 'src/components/layout/Row' -import { - currencyRateSelector, - currentCurrencySelector, - safeFiatBalancesListSelector, -} from 'src/logic/currencyValues/store/selectors' import { BALANCE_ROW_TEST_ID } from 'src/routes/safe/components/Balances' import AssetTableCell from 'src/routes/safe/components/Balances/AssetTableCell' import { @@ -33,6 +28,7 @@ import { extendedSafeTokensSelector, grantedSelector } from 'src/routes/safe/con import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics' import { makeStyles } from '@material-ui/core/styles' import { styles } from './styles' +import { currentCurrencySelector } from 'src/logic/currencyValues/store/selectors' const useStyles = makeStyles(styles) @@ -69,9 +65,7 @@ const Coins = (props: Props): React.ReactElement => { const columns = generateColumns() const autoColumns = columns.filter((c) => !c.custom) const selectedCurrency = useSelector(currentCurrencySelector) - const currencyRate = useSelector(currencyRateSelector) const activeTokens = useSelector(extendedSafeTokensSelector) - const currencyValues = useSelector(safeFiatBalancesListSelector) const granted = useSelector(grantedSelector) const { trackEvent } = useAnalytics() @@ -79,10 +73,10 @@ const Coins = (props: Props): React.ReactElement => { trackEvent({ category: SAFE_NAVIGATION_EVENT, action: 'Coins' }) }, [trackEvent]) - const filteredData: List = useMemo( - () => getBalanceData(activeTokens, selectedCurrency, currencyValues, currencyRate), - [activeTokens, selectedCurrency, currencyValues, currencyRate], - ) + const filteredData: List = useMemo(() => getBalanceData(activeTokens, selectedCurrency), [ + activeTokens, + selectedCurrency, + ]) return ( diff --git a/src/routes/safe/components/Balances/SendModal/screens/SendCollectible/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/SendCollectible/index.tsx index 391e4463..049be1e5 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/SendCollectible/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/SendCollectible/index.tsx @@ -18,7 +18,7 @@ import { ScanQRWrapper } from 'src/components/ScanQRModal/ScanQRWrapper' import WhenFieldChanges from 'src/components/WhenFieldChanges' import { addressBookSelector } from 'src/logic/addressBook/store/selectors' import { getNameFromAddressBook } from 'src/logic/addressBook/utils' -import { nftTokensSelector, safeActiveSelectorMap } from 'src/logic/collectibles/store/selectors' +import { nftAssetsSelector, nftTokensSelector } from 'src/logic/collectibles/store/selectors' import { Erc721Transfer } from 'src/logic/safe/store/models/types/gateway' import SafeInfo from 'src/routes/safe/components/Balances/SendModal/SafeInfo' import { AddressBookInput } from 'src/routes/safe/components/Balances/SendModal/screens/AddressBookInput' @@ -71,7 +71,7 @@ const SendCollectible = ({ selectedToken, }: SendCollectibleProps): React.ReactElement => { const classes = useStyles() - const nftAssets = useSelector(safeActiveSelectorMap) + const nftAssets = useSelector(nftAssetsSelector) const nftTokens = useSelector(nftTokensSelector) const addressBook = useSelector(addressBookSelector) const [selectedEntry, setSelectedEntry] = useState<{ address: string; name: string } | null>(() => { diff --git a/src/routes/safe/components/Balances/SendModal/screens/SendFunds/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/SendFunds/index.tsx index 85aeb91d..f7f2b540 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/SendFunds/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/SendFunds/index.tsx @@ -208,7 +208,7 @@ const SendFunds = ({ const setMaxAllowedAmount = () => { const isSpendingLimit = tokenSpendingLimit && txType === 'spendingLimit' - let maxAmount = selectedToken?.balance ?? 0 + let maxAmount = selectedToken?.balance.tokenBalance ?? 0 if (isSpendingLimit) { const spendingLimitBalance = fromTokenUnit( diff --git a/src/routes/safe/components/Balances/Tokens/index.tsx b/src/routes/safe/components/Balances/Tokens/index.tsx deleted file mode 100644 index b7c01a4a..00000000 --- a/src/routes/safe/components/Balances/Tokens/index.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import IconButton from '@material-ui/core/IconButton' -import { makeStyles } from '@material-ui/core/styles' -import Close from '@material-ui/icons/Close' - -import React from 'react' -import { useSelector } from 'react-redux' - -import { styles } from './style' - -import Hairline from 'src/components/layout/Hairline' -import Paragraph from 'src/components/layout/Paragraph' -import Row from 'src/components/layout/Row' - -import { orderedTokenListSelector } from 'src/logic/tokens/store/selectors' -import { AssetsList } from 'src/routes/safe/components/Balances/Tokens/screens/AssetsList' - -import { extendedSafeTokensSelector } from 'src/routes/safe/container/selector' -import { safeBlacklistedTokensSelector } from 'src/logic/safe/store/selectors' -import { TokenList } from 'src/routes/safe/components/Balances/Tokens/screens/TokenList' - -export const MANAGE_TOKENS_MODAL_CLOSE_BUTTON_TEST_ID = 'manage-tokens-close-modal-btn' - -const useStyles = makeStyles(styles) - -type Props = { - safeAddress: string - modalScreen: string - onClose: () => void -} - -export const Tokens = (props: Props): React.ReactElement => { - const { modalScreen, onClose, safeAddress } = props - const tokens = useSelector(orderedTokenListSelector) - const activeTokens = useSelector(extendedSafeTokensSelector) - const blacklistedTokens = useSelector(safeBlacklistedTokensSelector) - const classes = useStyles() - - return ( - <> - - - Manage List - - - - - - - {modalScreen === 'tokenList' && ( - - )} - {modalScreen === 'assetsList' && } - - ) -} diff --git a/src/routes/safe/components/Balances/Tokens/screens/AssetsList/AssetRow.tsx b/src/routes/safe/components/Balances/Tokens/screens/AssetsList/AssetRow.tsx deleted file mode 100644 index 9a927ad2..00000000 --- a/src/routes/safe/components/Balances/Tokens/screens/AssetsList/AssetRow.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import ListItem from '@material-ui/core/ListItem' -import ListItemIcon from '@material-ui/core/ListItemIcon' -import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction' -import ListItemText from '@material-ui/core/ListItemText' -import Switch from '@material-ui/core/Switch' -import React, { memo } from 'react' - -import { useStyles } from 'src/routes/safe/components/Balances/Tokens/screens/TokenList/style' -import Img from 'src/components/layout/Img' -import { getNetworkInfo } from 'src/config' -import { setCollectibleImageToPlaceholder } from 'src/routes/safe/components/Balances/utils' - -export const TOGGLE_ASSET_TEST_ID = 'toggle-asset-btn' - -const { nativeCoin } = getNetworkInfo() - -const AssetRow = memo(({ data, index, style }: any) => { - const classes = useStyles() - const { activeAssetsAddresses, assets, onSwitch } = data - const asset = assets[index] - const { address, image, name, symbol } = asset - const isActive = activeAssetsAddresses.includes(asset.address) - - return ( -
- - - {name} - - - {address !== nativeCoin.address && ( - - - - )} - -
- ) -}) - -AssetRow.displayName = 'AssetRow' - -export default AssetRow diff --git a/src/routes/safe/components/Balances/Tokens/screens/AssetsList/index.tsx b/src/routes/safe/components/Balances/Tokens/screens/AssetsList/index.tsx deleted file mode 100644 index 5671a662..00000000 --- a/src/routes/safe/components/Balances/Tokens/screens/AssetsList/index.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import MuiList from '@material-ui/core/List' -import CircularProgress from '@material-ui/core/CircularProgress' -import Search from '@material-ui/icons/Search' -import cn from 'classnames' -import SearchBar from 'material-ui-search-bar' -import React, { useState } from 'react' -import { useDispatch, useSelector } from 'react-redux' -import { FixedSizeList } from 'react-window' -import Paragraph from 'src/components/layout/Paragraph' - -import { useStyles } from './style' - -import Block from 'src/components/layout/Block' -import Hairline from 'src/components/layout/Hairline' -import Row from 'src/components/layout/Row' -import { nftAssetsListSelector } from 'src/logic/collectibles/store/selectors' -import AssetRow from 'src/routes/safe/components/Balances/Tokens/screens/AssetsList/AssetRow' -import updateActiveAssets from 'src/logic/safe/store/actions/updateActiveAssets' -import updateBlacklistedAssets from 'src/logic/safe/store/actions/updateBlacklistedAssets' -import { - safeActiveAssetsListSelector, - safeBlacklistedAssetsSelector, - safeParamAddressFromStateSelector, -} from 'src/logic/safe/store/selectors' - -const filterBy = (filter, nfts) => - nfts.filter( - (asset) => - !filter || - asset.description.toLowerCase().includes(filter.toLowerCase()) || - asset.name.toLowerCase().includes(filter.toLowerCase()) || - asset.symbol.toLowerCase().includes(filter.toLowerCase()), - ) - -export const AssetsList = (): React.ReactElement => { - const classes = useStyles() - const searchClasses = { - input: classes.searchInput, - root: classes.searchRoot, - iconButton: classes.searchIcon, - searchContainer: classes.searchContainer, - } - const dispatch = useDispatch() - const activeAssetsList = useSelector(safeActiveAssetsListSelector) - const blacklistedAssets = useSelector(safeBlacklistedAssetsSelector) - const safeAddress = useSelector(safeParamAddressFromStateSelector) - const [filterValue, setFilterValue] = useState('') - const [activeAssetsAddresses, setActiveAssetsAddresses] = useState(activeAssetsList) - const [blacklistedAssetsAddresses, setBlacklistedAssetsAddresses] = useState(blacklistedAssets) - const nftAssetsList = useSelector(nftAssetsListSelector) - - const onCancelSearch = () => { - setFilterValue('') - } - - const onChangeSearchBar = (value) => { - setFilterValue(value) - } - - const getItemKey = (index) => { - return index - } - - const onSwitch = (asset) => () => { - let newActiveAssetsAddresses - let newBlacklistedAssetsAddresses - if (activeAssetsAddresses.has(asset.address)) { - newActiveAssetsAddresses = activeAssetsAddresses.delete(asset.address) - newBlacklistedAssetsAddresses = blacklistedAssetsAddresses.add(asset.address) - } else { - newActiveAssetsAddresses = activeAssetsAddresses.add(asset.address) - newBlacklistedAssetsAddresses = blacklistedAssetsAddresses.delete(asset.address) - } - - // Set local state - setActiveAssetsAddresses(newActiveAssetsAddresses) - setBlacklistedAssetsAddresses(newBlacklistedAssetsAddresses) - // Dispatch to global state - dispatch(updateActiveAssets(safeAddress, newActiveAssetsAddresses)) - dispatch(updateBlacklistedAssets(safeAddress, newBlacklistedAssetsAddresses)) - } - - const createItemData = (assetsList) => { - return { - assets: assetsList, - activeAssetsAddresses, - onSwitch, - } - } - - const nftAssetsFilteredList = filterBy(filterValue, nftAssetsList) - const itemData = createItemData(nftAssetsFilteredList) - - return ( - <> - - - - } - value={filterValue} - /> - - - - {!nftAssetsList?.length && ( - - {!nftAssetsList ? : No collectibles available} - - )} - {nftAssetsFilteredList.length > 0 && ( - - - {AssetRow} - - - )} - - ) -} diff --git a/src/routes/safe/components/Balances/Tokens/screens/AssetsList/style.ts b/src/routes/safe/components/Balances/Tokens/screens/AssetsList/style.ts deleted file mode 100644 index 65c0ed1b..00000000 --- a/src/routes/safe/components/Balances/Tokens/screens/AssetsList/style.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { createStyles, makeStyles } from '@material-ui/core' - -import { md, mediumFontSize, secondaryText, sm, xs } from 'src/theme/variables' - -export const useStyles = makeStyles( - createStyles({ - root: { - minHeight: '52px', - }, - search: { - color: secondaryText, - paddingLeft: sm, - }, - padding: { - padding: `0 ${md}`, - }, - add: { - fontSize: '11px', - fontWeight: 'normal', - paddingRight: md, - paddingLeft: md, - }, - addBtnLabel: { - fontSize: mediumFontSize, - }, - actions: { - height: '50px', - }, - list: { - overflow: 'hidden', - overflowY: 'scroll', - padding: 0, - height: '100%', - }, - tokenIcon: { - marginRight: sm, - height: '28px', - width: '28px', - }, - searchInput: { - backgroundColor: 'transparent', - lineHeight: 'initial', - fontSize: '13px', - padding: 0, - '& > input::placeholder': { - letterSpacing: '-0.5px', - fontSize: mediumFontSize, - color: 'black', - }, - '& > input': { - letterSpacing: '-0.5px', - }, - }, - progressContainer: { - width: '100%', - height: '100%', - alignItems: 'center', - }, - searchContainer: { - marginLeft: xs, - marginRight: xs, - }, - searchRoot: { - letterSpacing: '-0.5px', - fontSize: '13px', - border: 'none', - boxShadow: 'none', - '& > button': { - display: 'none', - }, - flex: 1, - }, - searchIcon: { - '&:hover': { - backgroundColor: 'transparent !important', - }, - }, - }), -) diff --git a/src/routes/safe/components/Balances/Tokens/screens/TokenList/TokenRow.tsx b/src/routes/safe/components/Balances/Tokens/screens/TokenList/TokenRow.tsx deleted file mode 100644 index 9b73f079..00000000 --- a/src/routes/safe/components/Balances/Tokens/screens/TokenList/TokenRow.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import ListItem from '@material-ui/core/ListItem' -import ListItemIcon from '@material-ui/core/ListItemIcon' -import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction' -import ListItemText from '@material-ui/core/ListItemText' -import Switch from '@material-ui/core/Switch' -import React, { CSSProperties, memo, ReactElement } from 'react' - -import { useStyles } from './style' -import Img from 'src/components/layout/Img' -import { getNetworkInfo } from 'src/config' -import { setImageToPlaceholder } from 'src/routes/safe/components/Balances/utils' -import { ItemData } from 'src/routes/safe/components/Balances/Tokens/screens/TokenList/index' - -export const TOGGLE_TOKEN_TEST_ID = 'toggle-token-btn' - -interface TokenRowProps { - data: ItemData - index: number - style: CSSProperties -} - -const { nativeCoin } = getNetworkInfo() - -const TokenRow = memo(({ data, index, style }: TokenRowProps): ReactElement | null => { - const classes = useStyles() - const { activeTokensAddresses, onSwitch, tokens } = data - const token = tokens.get(index) - - if (!token) { - return null - } - - const isActive = activeTokensAddresses.has(token.address) - - return ( -
- - - {token.name} - - - {token.address !== nativeCoin.address && ( - - - - )} - -
- ) -}) - -TokenRow.displayName = 'TokenRow' - -export default TokenRow diff --git a/src/routes/safe/components/Balances/Tokens/screens/TokenList/index.tsx b/src/routes/safe/components/Balances/Tokens/screens/TokenList/index.tsx deleted file mode 100644 index aa7e12cc..00000000 --- a/src/routes/safe/components/Balances/Tokens/screens/TokenList/index.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import CircularProgress from '@material-ui/core/CircularProgress' -import MuiList from '@material-ui/core/List' -import Search from '@material-ui/icons/Search' -import cn from 'classnames' -import { List, Set } from 'immutable' -import SearchBar from 'material-ui-search-bar' -import React, { useState } from 'react' -import { FixedSizeList } from 'react-window' - -import TokenRow from './TokenRow' -import { useStyles } from './style' -import Block from 'src/components/layout/Block' -import Hairline from 'src/components/layout/Hairline' -import Row from 'src/components/layout/Row' -import { Token } from 'src/logic/tokens/store/model/token' -import { useDispatch } from 'react-redux' -import updateBlacklistedTokens from 'src/logic/safe/store/actions/updateBlacklistedTokens' -import updateActiveTokens from 'src/logic/safe/store/actions/updateActiveTokens' - -export const ADD_CUSTOM_TOKEN_BUTTON_TEST_ID = 'add-custom-token-btn' - -const filterBy = (filter: string, tokens: List): List => - tokens.filter( - (token) => - !filter || - token.symbol.toLowerCase().includes(filter.toLowerCase()) || - token.name.toLowerCase().includes(filter.toLowerCase()), - ) - -type Props = { - tokens: List - activeTokens: List - blacklistedTokens: Set - safeAddress: string -} - -export type ItemData = { - tokens: List - activeTokensAddresses: Set - onSwitch: (token: Token) => () => void -} - -export const TokenList = (props: Props): React.ReactElement => { - const classes = useStyles() - const { tokens, activeTokens, blacklistedTokens, safeAddress } = props - const [activeTokensAddresses, setActiveTokensAddresses] = useState(Set(activeTokens.map(({ address }) => address))) - const [blacklistedTokensAddresses, setBlacklistedTokensAddresses] = useState>(blacklistedTokens) - const [filter, setFilter] = useState('') - const dispatch = useDispatch() - - const searchClasses = { - input: classes.searchInput, - root: classes.searchRoot, - iconButton: classes.searchIcon, - searchContainer: classes.searchContainer, - } - - const onCancelSearch = () => { - setFilter('') - } - - const onChangeSearchBar = (value: string) => { - setFilter(value) - } - - const onSwitch = (token: Token) => () => { - let newActiveTokensAddresses - let newBlacklistedTokensAddresses - if (activeTokensAddresses.has(token.address)) { - newActiveTokensAddresses = activeTokensAddresses.delete(token.address) - newBlacklistedTokensAddresses = blacklistedTokensAddresses.add(token.address) - } else { - newActiveTokensAddresses = activeTokensAddresses.add(token.address) - newBlacklistedTokensAddresses = blacklistedTokensAddresses.delete(token.address) - } - - // Set local state - setActiveTokensAddresses(newActiveTokensAddresses) - setBlacklistedTokensAddresses(newBlacklistedTokensAddresses) - // Dispatch to global state - dispatch(updateActiveTokens(safeAddress, newActiveTokensAddresses)) - dispatch(updateBlacklistedTokens(safeAddress, newBlacklistedTokensAddresses)) - } - - const createItemData = (tokens: List, activeTokensAddresses: Set): ItemData => ({ - tokens, - activeTokensAddresses, - onSwitch, - }) - - const getItemKey = (index: number, { tokens }): string => { - return tokens.get(index).address - } - - const filteredTokens = filterBy(filter, tokens) - const itemData = createItemData(filteredTokens, activeTokensAddresses) - - return ( - <> - - - - } - value={filter} - /> - - - - {!tokens.size && ( - - - - )} - {tokens.size > 0 && ( - - - {TokenRow} - - - )} - - ) -} diff --git a/src/routes/safe/components/Balances/Tokens/screens/TokenList/style.ts b/src/routes/safe/components/Balances/Tokens/screens/TokenList/style.ts deleted file mode 100644 index dbf3500b..00000000 --- a/src/routes/safe/components/Balances/Tokens/screens/TokenList/style.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { createStyles, makeStyles } from '@material-ui/core' - -import { border, md, mediumFontSize, secondaryText, sm, xs } from 'src/theme/variables' - -export const useStyles = makeStyles( - createStyles({ - root: { - minHeight: '52px', - }, - search: { - color: secondaryText, - paddingLeft: sm, - }, - padding: { - padding: `0 ${md}`, - }, - add: { - fontSize: '11px', - fontWeight: 'normal', - paddingRight: md, - paddingLeft: md, - }, - addBtnLabel: { - fontSize: mediumFontSize, - }, - actions: { - height: '50px', - }, - list: { - overflow: 'hidden', - overflowY: 'scroll', - padding: 0, - height: '100%', - }, - token: { - minHeight: '50px', - borderBottom: `1px solid ${border}`, - }, - tokenRoot: { - paddingTop: 0, - paddingBottom: 0, - }, - searchInput: { - backgroundColor: 'transparent', - lineHeight: 'initial', - fontSize: '13px', - padding: 0, - '& > input::placeholder': { - letterSpacing: '-0.5px', - fontSize: mediumFontSize, - color: 'black', - }, - '& > input': { - letterSpacing: '-0.5px', - }, - }, - tokenIcon: { - marginRight: md, - height: '28px', - width: '28px', - }, - progressContainer: { - width: '100%', - height: '100%', - alignItems: 'center', - }, - searchContainer: { - marginLeft: xs, - marginRight: xs, - }, - searchRoot: { - letterSpacing: '-0.5px', - fontSize: '13px', - border: 'none', - boxShadow: 'none', - '& > button': { - display: 'none', - }, - flex: 1, - }, - searchIcon: { - '&:hover': { - backgroundColor: 'transparent !important', - }, - }, - }), -) diff --git a/src/routes/safe/components/Balances/Tokens/style.ts b/src/routes/safe/components/Balances/Tokens/style.ts deleted file mode 100644 index b37e7b7f..00000000 --- a/src/routes/safe/components/Balances/Tokens/style.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { lg, md } from 'src/theme/variables' -import { createStyles } from '@material-ui/core' - -export const styles = createStyles({ - heading: { - padding: `${md} ${lg}`, - justifyContent: 'space-between', - maxHeight: '75px', - boxSizing: 'border-box', - }, - close: { - height: '35px', - width: '35px', - }, -}) diff --git a/src/routes/safe/components/Balances/dataFetcher.ts b/src/routes/safe/components/Balances/dataFetcher.ts index 7d48d54a..f9868540 100644 --- a/src/routes/safe/components/Balances/dataFetcher.ts +++ b/src/routes/safe/components/Balances/dataFetcher.ts @@ -1,32 +1,13 @@ -import { BigNumber } from 'bignumber.js' import { List } from 'immutable' import { getNetworkInfo } from 'src/config' import { FIXED } from 'src/components/Table/sorting' import { formatAmountInUsFormat } from 'src/logic/tokens/utils/formatAmount' import { TableColumn } from 'src/components/Table/types.d' -import { BalanceCurrencyList } from 'src/logic/currencyValues/store/model/currencyValues' import { Token } from 'src/logic/tokens/store/model/token' -import { sameAddress } from 'src/logic/wallets/ethAddresses' - export const BALANCE_TABLE_ASSET_ID = 'asset' export const BALANCE_TABLE_BALANCE_ID = 'balance' export const BALANCE_TABLE_VALUE_ID = 'value' -const { nativeCoin } = getNetworkInfo() - -const getTokenValue = (token: Token, currencyValues: BalanceCurrencyList, currencyRate: number): string => { - const currencyValue = currencyValues.find( - ({ tokenAddress }) => sameAddress(token.address, tokenAddress) || sameAddress(token.address, nativeCoin.address), - ) - - if (!currencyValue) { - return '' - } - - const { balanceInBaseCurrency } = currencyValue - return new BigNumber(balanceInBaseCurrency).times(currencyRate).toString() -} - const getTokenPriceInCurrency = (balance: string, currencySelected?: string): string => { if (!currencySelected) { return Number('').toFixed(2) @@ -44,15 +25,10 @@ export interface BalanceData { valueOrder: number } -export const getBalanceData = ( - activeTokens: List, - currencySelected?: string, - currencyValues?: BalanceCurrencyList, - currencyRate?: number, -): List => { +export const getBalanceData = (activeTokens: List, currencySelected?: string): List => { const { nativeCoin } = getNetworkInfo() return activeTokens.map((token) => { - const balance = currencyRate && currencyValues ? getTokenValue(token, currencyValues, currencyRate) : '0' + const { tokenBalance, fiatBalance } = token.balance return { [BALANCE_TABLE_ASSET_ID]: { @@ -62,11 +38,11 @@ export const getBalanceData = ( symbol: token.symbol, }, assetOrder: token.name, - [BALANCE_TABLE_BALANCE_ID]: `${formatAmountInUsFormat(token.balance?.toString() || '0')} ${token.symbol}`, - balanceOrder: Number(token.balance), + [BALANCE_TABLE_BALANCE_ID]: `${formatAmountInUsFormat(tokenBalance?.toString() || '0')} ${token.symbol}`, + balanceOrder: Number(tokenBalance), [FIXED]: token.symbol === nativeCoin.symbol, - [BALANCE_TABLE_VALUE_ID]: getTokenPriceInCurrency(balance, currencySelected), - valueOrder: Number(balance), + [BALANCE_TABLE_VALUE_ID]: getTokenPriceInCurrency(fiatBalance || '0', currencySelected), + valueOrder: Number(tokenBalance), } }) } @@ -78,6 +54,7 @@ export const generateColumns = (): List => { disablePadding: false, label: 'Asset', custom: false, + static: true, width: 250, } @@ -88,6 +65,7 @@ export const generateColumns = (): List => { disablePadding: false, label: 'Balance', custom: false, + static: true, } const actions: TableColumn = { @@ -105,6 +83,7 @@ export const generateColumns = (): List => { order: true, label: 'Value', custom: false, + static: true, disablePadding: false, } diff --git a/src/routes/safe/components/Balances/index.tsx b/src/routes/safe/components/Balances/index.tsx index be80a8ba..d9dd2bdc 100644 --- a/src/routes/safe/components/Balances/index.tsx +++ b/src/routes/safe/components/Balances/index.tsx @@ -3,18 +3,16 @@ import React, { useEffect, useState } from 'react' import { useSelector } from 'react-redux' import ReceiveModal from 'src/components/App/ReceiveModal' -import { Tokens } from './Tokens' import { styles } from './style' import Modal from 'src/components/Modal' -import ButtonLink from 'src/components/layout/ButtonLink' import Col from 'src/components/layout/Col' import Divider from 'src/components/layout/Divider' import Row from 'src/components/layout/Row' import { SAFELIST_ADDRESS } from 'src/routes/routes' import SendModal from 'src/routes/safe/components/Balances/SendModal' -import CurrencyDropdown from 'src/routes/safe/components/CurrencyDropdown' +import { CurrencyDropdown } from 'src/routes/safe/components/CurrencyDropdown' import { safeFeaturesEnabledSelector, safeNameSelector, @@ -35,7 +33,6 @@ export const BALANCE_ROW_TEST_ID = 'balance-row' const INITIAL_STATE = { erc721Enabled: false, showToken: false, - showManageCollectibleModal: false, sendFunds: { isOpen: false, selectedToken: '', @@ -95,17 +92,8 @@ const Balances = (): React.ReactElement => { })) } - const { - assetDivider, - assetTab, - assetTabActive, - assetTabs, - controls, - manageTokensButton, - receiveModal, - tokenControls, - } = classes - const { erc721Enabled, sendFunds, showManageCollectibleModal, showReceive, showToken } = state + const { assetDivider, assetTab, assetTabActive, assetTabs, controls, receiveModal, tokenControls } = classes + const { erc721Enabled, sendFunds, showReceive } = state return ( <> @@ -140,32 +128,7 @@ const Balances = (): React.ReactElement => { path={`${SAFELIST_ADDRESS}/${address}/balances/collectibles`} exact render={() => { - return !erc721Enabled ? ( - - ) : ( - - onShow('ManageCollectibleModal')} - size="lg" - testId="manage-tokens-btn" - > - Manage List - - onHide('ManageCollectibleModal')} - open={showManageCollectibleModal} - title="Manage List" - > - onHide('ManageCollectibleModal')} - safeAddress={address} - /> - - - ) + return !erc721Enabled ? : null }} /> { <> - onShow('Token')} - size="lg" - testId="manage-tokens-btn" - > - Manage List - - onHide('Token')} - open={showToken} - title="Manage List" - > - onHide('Token')} safeAddress={address} /> - ) diff --git a/src/routes/safe/components/CurrencyDropdown/index.tsx b/src/routes/safe/components/CurrencyDropdown/index.tsx index 5aea152a..012ba0e8 100644 --- a/src/routes/safe/components/CurrencyDropdown/index.tsx +++ b/src/routes/safe/components/CurrencyDropdown/index.tsx @@ -13,26 +13,22 @@ import { useDispatch, useSelector } from 'react-redux' import CheckIcon from './img/check.svg' import { setSelectedCurrency } from 'src/logic/currencyValues/store/actions/setSelectedCurrency' -import { AVAILABLE_CURRENCIES } from 'src/logic/currencyValues/store/model/currencyValues' -import { currentCurrencySelector } from 'src/logic/currencyValues/store/selectors' import { useDropdownStyles } from 'src/routes/safe/components/CurrencyDropdown/style' -import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' +import { availableCurrenciesSelector, currentCurrencySelector } from 'src/logic/currencyValues/store/selectors' import { DropdownListTheme } from 'src/theme/mui' -import { setImageToPlaceholder } from '../Balances/utils' +import { setImageToPlaceholder } from 'src/routes/safe/components/Balances/utils' import Img from 'src/components/layout/Img/index' import { getNetworkInfo } from 'src/config' import { sameString } from 'src/utils/strings' const { nativeCoin } = getNetworkInfo() -const CurrencyDropdown = (): React.ReactElement | null => { - const safeAddress = useSelector(safeParamAddressFromStateSelector) as string +export const CurrencyDropdown = (): React.ReactElement | null => { const dispatch = useDispatch() const [anchorEl, setAnchorEl] = useState(null) const selectedCurrency = useSelector(currentCurrencySelector) const [searchParams, setSearchParams] = useState('') - - const currenciesList = Object.values(AVAILABLE_CURRENCIES) + const currenciesList = useSelector(availableCurrenciesSelector) const tokenImage = nativeCoin.logoUri const classes = useDropdownStyles({}) const currenciesListFiltered = currenciesList.filter((currency) => @@ -47,8 +43,8 @@ const CurrencyDropdown = (): React.ReactElement | null => { setAnchorEl(null) } - const onCurrentCurrencyChangedHandler = (newCurrencySelectedName) => { - dispatch(setSelectedCurrency(safeAddress, newCurrencySelectedName)) + const onCurrentCurrencyChangedHandler = (newCurrencySelectedName: string) => { + dispatch(setSelectedCurrency({ selectedCurrency: newCurrencySelectedName })) handleClose() } @@ -80,6 +76,7 @@ const CurrencyDropdown = (): React.ReactElement | null => { horizontal: 'center', vertical: 'top', }} + TransitionProps={{ mountOnEnter: true, unmountOnExit: true }} >
@@ -139,5 +136,3 @@ const CurrencyDropdown = (): React.ReactElement | null => { ) } - -export default CurrencyDropdown diff --git a/src/routes/safe/container/selector.ts b/src/routes/safe/container/selector.ts index db81c979..11420339 100644 --- a/src/routes/safe/container/selector.ts +++ b/src/routes/safe/container/selector.ts @@ -4,12 +4,11 @@ import { createSelector } from 'reselect' import { Token } from 'src/logic/tokens/store/model/token' import { tokensSelector } from 'src/logic/tokens/store/selectors' import { getEthAsToken } from 'src/logic/tokens/utils/tokenHelpers' -import { isUserAnOwner } from 'src/logic/wallets/ethAddresses' +import { isUserAnOwner, sameAddress, ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses' import { userAccountSelector } from 'src/logic/wallets/store/selectors' import { safeActiveTokensSelector, safeBalancesSelector, safeSelector } from 'src/logic/safe/store/selectors' import { SafeRecord } from 'src/logic/safe/store/models/safe' -// import { SPENDING_LIMIT_MODULE_ADDRESS } from 'src/utils/constants' export const grantedSelector = createSelector( userAccountSelector, @@ -37,15 +36,16 @@ export const extendedSafeTokensSelector = createSelector( const tokenBalance = balances?.get(tokenAddress) if (baseToken) { - map.set(tokenAddress, baseToken.set('balance', tokenBalance || '0')) + const updatedBaseToken = baseToken.set('balance', tokenBalance || { tokenBalance: '0', fiatBalance: '0' }) + if (sameAddress(tokenAddress, ZERO_ADDRESS) || sameAddress(tokenAddress, ethAsToken?.address)) { + map.set(tokenAddress, updatedBaseToken.set('logoUri', ethAsToken?.logoUri || baseToken.logoUri)) + } else { + map.set(tokenAddress, updatedBaseToken) + } } }) }) - if (ethAsToken) { - return extendedTokens.set(ethAsToken.address, ethAsToken).toList() - } - return extendedTokens.toList() }, ) diff --git a/src/routes/safe/store/reducer/types/safe.ts b/src/routes/safe/store/reducer/types/safe.ts index c523b830..9e5fc835 100644 --- a/src/routes/safe/store/reducer/types/safe.ts +++ b/src/routes/safe/store/reducer/types/safe.ts @@ -9,12 +9,14 @@ export interface SafeReducerState { defaultSafe: DefaultSafe safes: SafesMap latestMasterContractVersion: string + selectedCurrency: string } interface SafeReducerStateJSON { defaultSafe: 'NOT_ASKED' | string | undefined safes: Record latestMasterContractVersion: string + selectedCurrency: string } export interface SafeReducerMap extends Map { diff --git a/src/store/index.ts b/src/store/index.ts index cb3dba2e..2552f67a 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -13,11 +13,6 @@ import { nftTokensReducer, } from 'src/logic/collectibles/store/reducer/collectibles' import cookies, { COOKIES_REDUCER_ID } from 'src/logic/cookies/store/reducer/cookies' -import currencyValuesStorageMiddleware from 'src/logic/currencyValues/store/middleware' -import currencyValues, { - CURRENCY_VALUES_KEY, - CurrencyValuesState, -} from 'src/logic/currencyValues/store/reducer/currencyValues' import currentSession, { CURRENT_SESSION_REDUCER_ID, CurrentSessionState, @@ -30,11 +25,16 @@ import tokens, { TOKEN_REDUCER_ID, TokenState } from 'src/logic/tokens/store/red import providerWatcher from 'src/logic/wallets/store/middlewares/providerWatcher' import provider, { PROVIDER_REDUCER_ID, ProviderState } from 'src/logic/wallets/store/reducer/provider' import notificationsMiddleware from 'src/logic/safe/store/middleware/notificationsMiddleware' -import safeStorage from 'src/logic/safe/store/middleware/safeStorage' +import { safeStorageMiddleware } from 'src/logic/safe/store/middleware/safeStorage' import safe, { SAFE_REDUCER_ID } from 'src/logic/safe/store/reducer/safe' import { NFTAssets, NFTTokens } from 'src/logic/collectibles/sources/collectibles.d' import { SafeReducerMap } from 'src/routes/safe/store/reducer/types/safe' import { AddressBookState } from 'src/logic/addressBook/model/addressBook' +import currencyValues, { + CURRENCY_VALUES_KEY, + CurrencyValuesState, +} from 'src/logic/currencyValues/store/reducer/currencyValues' +import { currencyValuesStorageMiddleware } from 'src/logic/currencyValues/store/middleware/currencyValuesStorageMiddleware' export const history = createHashHistory() @@ -45,7 +45,7 @@ const finalCreateStore = composeEnhancers( thunk, routerMiddleware(history), notificationsMiddleware, - safeStorage, + safeStorageMiddleware, providerWatcher, addressBookMiddleware, currencyValuesStorageMiddleware, diff --git a/src/test/utils/DOMNavigation/tokens.ts b/src/test/utils/DOMNavigation/tokens.ts index dcf834d4..5961e901 100644 --- a/src/test/utils/DOMNavigation/tokens.ts +++ b/src/test/utils/DOMNavigation/tokens.ts @@ -1,9 +1,7 @@ -// -import { fireEvent, waitForElement, act } from '@testing-library/react' +// +import { fireEvent, act } from '@testing-library/react' import { MANAGE_TOKENS_BUTTON_TEST_ID } from 'src/routes/safe/components/Balances' -import { ADD_CUSTOM_TOKEN_BUTTON_TEST_ID } from 'src/routes/safe/components/Balances/Tokens/screens/TokenList' -import { TOGGLE_TOKEN_TEST_ID } from 'src/routes/safe/components/Balances/Tokens/screens/TokenList/TokenRow' -import { MANAGE_TOKENS_MODAL_CLOSE_BUTTON_TEST_ID } from 'src/routes/safe/components/Balances/Tokens' + export const clickOnManageTokens = (dom) => { const btn = dom.getByTestId(MANAGE_TOKENS_BUTTON_TEST_ID) @@ -13,26 +11,3 @@ export const clickOnManageTokens = (dom) => { }) } -export const clickOnAddCustomToken = (dom) => { - const btn = dom.getByTestId(ADD_CUSTOM_TOKEN_BUTTON_TEST_ID) - - act(() => { - fireEvent.click(btn) - }) -} - -export const toggleToken = async (dom, symbol) => { - const btn = await waitForElement(() => dom.getByTestId(`${symbol}_${TOGGLE_TOKEN_TEST_ID}`)) - - act(() => { - fireEvent.click(btn) - }) -} - -export const closeManageTokensModal = (dom) => { - const btn = dom.getByTestId(MANAGE_TOKENS_MODAL_CLOSE_BUTTON_TEST_ID) - - act(() => { - fireEvent.click(btn) - }) -} diff --git a/src/test/utils/transactions/index.ts b/src/test/utils/transactions/index.ts index 53bc5607..915cb890 100644 --- a/src/test/utils/transactions/index.ts +++ b/src/test/utils/transactions/index.ts @@ -1,3 +1,2 @@ -// +// export * from './moveFunds.helper' -export * from './moveTokens.helper' \ No newline at end of file diff --git a/src/test/utils/transactions/moveTokens.helper.ts b/src/test/utils/transactions/moveTokens.helper.ts deleted file mode 100644 index 35a69c0a..00000000 --- a/src/test/utils/transactions/moveTokens.helper.ts +++ /dev/null @@ -1,37 +0,0 @@ -// -import * as React from 'react' -import { Map } from 'immutable' -import { checkMinedTx, checkPendingTx } from 'src/test/builder/safe.dom.utils' -import { makeToken, } from 'src/logic/tokens/store/model/token' -import addTokens from 'src/logic/tokens/store/actions/saveTokens' - -export const dispatchAddTokenToList = async (store, tokenAddress) => { - const fetchTokensMock = jest.fn() - const tokens = Map().set( - 'TKN', - makeToken({ - address: tokenAddress, - name: 'OmiseGo', - symbol: 'OMG', - decimals: 18, - logoUri: - 'https://github.com/TrustWallet/tokens/blob/master/images/0x6810e776880c02933d47db1b9fc05908e5386b96.png?raw=true', - }), - ) - fetchTokensMock.mockImplementation(() => store.dispatch(addTokens(tokens))) - fetchTokensMock() - fetchTokensMock.mockRestore() -} - -export const checkMinedMoveTokensTx = (Transaction, name) => { - checkMinedTx(Transaction, name) -} - -export const checkPendingMoveTokensTx = async ( - Transaction, - safeThreshold, - name, - statusses, -) => { - await checkPendingTx(Transaction, safeThreshold, name, statusses) -} From a504ad495ab36d86ac6c5dc393465d97c20c2dbc Mon Sep 17 00:00:00 2001 From: Fernando Date: Thu, 18 Mar 2021 08:20:49 -0300 Subject: [PATCH 12/37] use `tokenBalance` for SendFunds modal (#2056) --- .../SendModal/screens/SendFunds/TokenSelectField/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/safe/components/Balances/SendModal/screens/SendFunds/TokenSelectField/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/SendFunds/TokenSelectField/index.tsx index 454cb121..fbd745c4 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/SendFunds/TokenSelectField/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/SendFunds/TokenSelectField/index.tsx @@ -34,7 +34,7 @@ const SelectedToken = ({ tokenAddress, tokens }: SelectTokenProps): ReactElement ) : ( @@ -73,7 +73,7 @@ const TokenSelectField = ({ initialValue, isValid = true, tokens }: TokenSelectF From e2c11133772b1e3850a1e4e1b3ae0c3f6f5ffaaa Mon Sep 17 00:00:00 2001 From: Fernando Date: Thu, 18 Mar 2021 11:33:10 -0300 Subject: [PATCH 13/37] Remove asset and activeAssets references (#2058) --- src/logic/safe/store/actions/fetchSafe.ts | 1 - src/logic/safe/store/models/safe.ts | 2 -- src/logic/safe/store/reducer/safe.ts | 2 -- .../shouldSafeStoreBeUpdated.test.ts | 21 ------------------- .../utils/__tests__/tokenHelpers.test.ts | 2 +- 5 files changed, 1 insertion(+), 27 deletions(-) diff --git a/src/logic/safe/store/actions/fetchSafe.ts b/src/logic/safe/store/actions/fetchSafe.ts index d7dcba4f..5ffa962a 100644 --- a/src/logic/safe/store/actions/fetchSafe.ts +++ b/src/logic/safe/store/actions/fetchSafe.ts @@ -87,7 +87,6 @@ export const buildSafe = async ( featuresEnabled, balances: localSafe?.balances || Map(), latestIncomingTxBlock: 0, - activeAssets: Set(), activeTokens: Set(), modules, spendingLimits, diff --git a/src/logic/safe/store/models/safe.ts b/src/logic/safe/store/models/safe.ts index 38616f17..7fb02abf 100644 --- a/src/logic/safe/store/models/safe.ts +++ b/src/logic/safe/store/models/safe.ts @@ -34,7 +34,6 @@ export type SafeRecordProps = { modules?: ModulePair[] | null spendingLimits?: SpendingLimit[] | null activeTokens: Set - activeAssets: Set balances: Map nonce: number latestIncomingTxBlock: number @@ -54,7 +53,6 @@ const makeSafe = Record({ modules: [], spendingLimits: [], activeTokens: Set(), - activeAssets: Set(), balances: Map(), nonce: 0, latestIncomingTxBlock: 0, diff --git a/src/logic/safe/store/reducer/safe.ts b/src/logic/safe/store/reducer/safe.ts index 44112c4b..c75baef4 100644 --- a/src/logic/safe/store/reducer/safe.ts +++ b/src/logic/safe/store/reducer/safe.ts @@ -25,7 +25,6 @@ export const buildSafe = (storedSafe: SafeRecordProps): SafeRecordProps => { const addresses = storedSafe.owners.map((owner) => checksumAddress(owner.address)) const owners = buildOwnersFrom(Array.from(names), Array.from(addresses)) const activeTokens = Set(storedSafe.activeTokens) - const activeAssets = Set(storedSafe.activeAssets) const balances = Map(storedSafe.balances) return { @@ -33,7 +32,6 @@ export const buildSafe = (storedSafe: SafeRecordProps): SafeRecordProps => { owners, balances, activeTokens, - activeAssets, latestIncomingTxBlock: 0, modules: null, } diff --git a/src/logic/safe/utils/__tests__/shouldSafeStoreBeUpdated.test.ts b/src/logic/safe/utils/__tests__/shouldSafeStoreBeUpdated.test.ts index 51ca8521..0ed862bb 100644 --- a/src/logic/safe/utils/__tests__/shouldSafeStoreBeUpdated.test.ts +++ b/src/logic/safe/utils/__tests__/shouldSafeStoreBeUpdated.test.ts @@ -7,7 +7,6 @@ const getMockedOldSafe = ({ needsUpdate, balances, recurringUser, - assets, activeTokens, owners, featuresEnabled, @@ -30,8 +29,6 @@ const getMockedOldSafe = ({ } const mockedActiveTokenAddress1 = '0x36591cd3DA96b21Ac9ca54cFaf80fe45107294F1' const mockedActiveTokenAddress2 = '0x92aF97cbF10742dD2527ffaBA70e34C03CFFC2c1' - const mockedActiveAssetsAddress1 = '0x503ab2a6A70c6C6ec8b25a4C87C784e1c8f8e8CD' - const mockedActiveAssetsAddress2 = '0xfdd4E685361CB7E89a4D27e03DCd0001448d731F' return { name: name || 'MockedSafe', @@ -42,7 +39,6 @@ const getMockedOldSafe = ({ modules: modules || [], spendingLimits: spendingLimits || [], activeTokens: activeTokens || Set([mockedActiveTokenAddress1, mockedActiveTokenAddress2]), - assets: assets || Set([mockedActiveAssetsAddress1, mockedActiveAssetsAddress2]), balances: balances || Map({ @@ -198,23 +194,6 @@ describe('shouldSafeStoreBeUpdated', () => { // Then expect(expectedResult).toEqual(true) }) - it(`Given an old activeAssets list and a new activeAssets list for the safe, should return true`, () => { - // given - const mockedActiveTokenAddress1 = '0x36591cd3DA96b21Ac9ca54cFaf80fe45107294F1' - const mockedActiveTokenAddress2 = '0x92aF97cbF10742dD2527ffaBA70e34C03CFFC2c1' - const oldActiveAssets = Set([mockedActiveTokenAddress1, mockedActiveTokenAddress2]) - const newActiveAssets = Set([mockedActiveTokenAddress1]) - const oldSafe = getMockedOldSafe({ assets: oldActiveAssets }) - const newSafeProps: Partial = { - assets: newActiveAssets, - } - - // When - const expectedResult = shouldSafeStoreBeUpdated(newSafeProps, oldSafe) - - // Then - expect(expectedResult).toEqual(true) - }) it(`Given an old balances list and a new balances list for the safe, should return true`, () => { // given const mockedActiveTokenAddress1 = '0x36591cd3DA96b21Ac9ca54cFaf80fe45107294F1' diff --git a/src/logic/tokens/utils/__tests__/tokenHelpers.test.ts b/src/logic/tokens/utils/__tests__/tokenHelpers.test.ts index c211e8b4..142c2d6a 100644 --- a/src/logic/tokens/utils/__tests__/tokenHelpers.test.ts +++ b/src/logic/tokens/utils/__tests__/tokenHelpers.test.ts @@ -58,7 +58,7 @@ describe('getERC20DecimalsAndSymbol', () => { symbol, decimals, logoUri: 'https://gnosis-safe-token-logos.s3.amazonaws.com/0x5592EC0cfb4dbc12D3aB100b257153436a1f0FEa.png', - balance: 0, + balance: { tokenBalance: '0', fiatBalance: '0' }, }) const expectedResult = { decimals, From c41ab4eaec513752117c187d660b20db187a396b Mon Sep 17 00:00:00 2001 From: Fernando Date: Thu, 18 Mar 2021 14:02:48 -0300 Subject: [PATCH 14/37] (Chore) Update `nativeCoin` address to ZERO_ADDRESS (#2059) --- docs/networks.md | 4 +-- src/config/networks/energy_web_chain.ts | 2 +- src/config/networks/local.ts | 2 +- src/config/networks/mainnet.ts | 2 +- src/config/networks/rinkeby.ts | 2 +- src/config/networks/volta.ts | 2 +- src/config/networks/xdai.ts | 2 +- src/logic/safe/hooks/useTokenInfo.tsx | 8 ++---- src/logic/safe/utils/spendingLimits.ts | 26 +++---------------- .../tokens/store/actions/fetchSafeTokens.ts | 14 +++------- .../SpendingLimit/NewLimitModal/Review.tsx | 9 ++----- src/routes/safe/container/selector.ts | 4 +-- 12 files changed, 22 insertions(+), 55 deletions(-) diff --git a/docs/networks.md b/docs/networks.md index c048955a..4ec60e0c 100644 --- a/docs/networks.md +++ b/docs/networks.md @@ -286,7 +286,7 @@ const xDai: NetworkConfig = { label: 'xDai', isTestNet: false, nativeCoin: { - address: '0x000', + address: '0x0000000000000000000000000000000000000000', name: 'xDai', symbol: 'xDai', decimals: 18, @@ -343,7 +343,7 @@ const mainnet: NetworkConfig = { label: 'Mainnet', isTestNet: false, nativeCoin: { - address: '0x000', + address: '0x0000000000000000000000000000000000000000', name: 'Ether', symbol: 'ETH', decimals: 18, diff --git a/src/config/networks/energy_web_chain.ts b/src/config/networks/energy_web_chain.ts index 1f908757..7ba62198 100644 --- a/src/config/networks/energy_web_chain.ts +++ b/src/config/networks/energy_web_chain.ts @@ -38,7 +38,7 @@ const mainnet: NetworkConfig = { label: 'EWC', isTestNet: false, nativeCoin: { - address: '0x000', + address: '0x0000000000000000000000000000000000000000', name: 'Energy web token', symbol: 'EWT', decimals: 18, diff --git a/src/config/networks/local.ts b/src/config/networks/local.ts index a18d87bf..a2e9c8f8 100644 --- a/src/config/networks/local.ts +++ b/src/config/networks/local.ts @@ -29,7 +29,7 @@ const local: NetworkConfig = { label: 'LocalRPC', isTestNet: true, nativeCoin: { - address: '0x000', + address: '0x0000000000000000000000000000000000000000', name: 'Ether', symbol: 'ETH', decimals: 18, diff --git a/src/config/networks/mainnet.ts b/src/config/networks/mainnet.ts index cd330a8c..b4333c05 100644 --- a/src/config/networks/mainnet.ts +++ b/src/config/networks/mainnet.ts @@ -38,7 +38,7 @@ const mainnet: NetworkConfig = { label: 'Mainnet', isTestNet: false, nativeCoin: { - address: '0x000', + address: '0x0000000000000000000000000000000000000000', name: 'Ether', symbol: 'ETH', decimals: 18, diff --git a/src/config/networks/rinkeby.ts b/src/config/networks/rinkeby.ts index 0e035ecb..3ce8179f 100644 --- a/src/config/networks/rinkeby.ts +++ b/src/config/networks/rinkeby.ts @@ -38,7 +38,7 @@ const rinkeby: NetworkConfig = { label: 'Rinkeby', isTestNet: true, nativeCoin: { - address: '0x000', + address: '0x0000000000000000000000000000000000000000', name: 'Ether', symbol: 'ETH', decimals: 18, diff --git a/src/config/networks/volta.ts b/src/config/networks/volta.ts index e41c18cf..154ba9e8 100644 --- a/src/config/networks/volta.ts +++ b/src/config/networks/volta.ts @@ -35,7 +35,7 @@ const mainnet: NetworkConfig = { label: 'Volta', isTestNet: true, nativeCoin: { - address: '0x000', + address: '0x0000000000000000000000000000000000000000', name: 'Energy web token', symbol: 'EWT', decimals: 18, diff --git a/src/config/networks/xdai.ts b/src/config/networks/xdai.ts index e0763b78..3241806f 100644 --- a/src/config/networks/xdai.ts +++ b/src/config/networks/xdai.ts @@ -29,7 +29,7 @@ const xDai: NetworkConfig = { label: 'xDai', isTestNet: false, nativeCoin: { - address: '0x000', + address: '0x0000000000000000000000000000000000000000', name: 'xDai', symbol: 'xDai', decimals: 18, diff --git a/src/logic/safe/hooks/useTokenInfo.tsx b/src/logic/safe/hooks/useTokenInfo.tsx index c05f161c..0919611b 100644 --- a/src/logic/safe/hooks/useTokenInfo.tsx +++ b/src/logic/safe/hooks/useTokenInfo.tsx @@ -1,18 +1,14 @@ import { useSelector } from 'react-redux' -import { getNetworkInfo } from 'src/config' import { Token } from 'src/logic/tokens/store/model/token' -import { sameAddress, ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses' +import { sameAddress } from 'src/logic/wallets/ethAddresses' import { safeKnownCoins } from 'src/routes/safe/container/selector' -const { nativeCoin } = getNetworkInfo() - const useTokenInfo = (address: string): Token | undefined => { const tokens = useSelector(safeKnownCoins) if (tokens) { - const tokenAddress = sameAddress(address, ZERO_ADDRESS) ? nativeCoin.address : address - return tokens.find((token) => sameAddress(token.address, tokenAddress)) ?? undefined + return tokens.find((token) => sameAddress(token.address, address)) } } diff --git a/src/logic/safe/utils/spendingLimits.ts b/src/logic/safe/utils/spendingLimits.ts index 1c20e890..cbf438a3 100644 --- a/src/logic/safe/utils/spendingLimits.ts +++ b/src/logic/safe/utils/spendingLimits.ts @@ -1,5 +1,4 @@ import { BigNumber } from 'bignumber.js' -import { getNetworkInfo } from 'src/config' import { AbiItem } from 'web3-utils' import { CreateTransactionArgs } from 'src/logic/safe/store/actions/createTransaction' @@ -9,7 +8,7 @@ import SpendingLimitModule from 'src/logic/contracts/artifacts/AllowanceModule.j import generateBatchRequests from 'src/logic/contracts/generateBatchRequests' import { getSpendingLimitContract, MULTI_SEND_ADDRESS } from 'src/logic/contracts/safeContracts' import { SpendingLimit } from 'src/logic/safe/store/models/safe' -import { sameAddress, ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses' +import { sameAddress } from 'src/logic/wallets/ethAddresses' import { getWeb3, web3ReadOnly } from 'src/logic/wallets/getWeb3' import { SPENDING_LIMIT_MODULE_ADDRESS } from 'src/utils/constants' import { getEncodedMultiSendCallData, MultiSendTx } from './upgradeSafe' @@ -138,16 +137,13 @@ type DeleteAllowanceParams = { } export const getDeleteAllowanceTxData = ({ beneficiary, tokenAddress }: DeleteAllowanceParams): string => { - const { nativeCoin } = getNetworkInfo() - const token = sameAddress(tokenAddress, nativeCoin.address) ? ZERO_ADDRESS : tokenAddress - const web3 = getWeb3() const spendingLimitContract = new web3.eth.Contract( SpendingLimitModule.abi as AbiItem[], SPENDING_LIMIT_MODULE_ADDRESS, ) - return spendingLimitContract.methods.deleteAllowance(beneficiary, token).encodeABI() + return spendingLimitContract.methods.deleteAllowance(beneficiary, tokenAddress).encodeABI() } export const enableSpendingLimitModuleMultiSendTx = (safeAddress: string): MultiSendTx => { @@ -188,20 +184,13 @@ export const setSpendingLimitTx = ({ safeAddress, }: SpendingLimitTxParams): CreateTransactionArgs => { const spendingLimitContract = getSpendingLimitContract() - const { nativeCoin } = getNetworkInfo() const txArgs: CreateTransactionArgs = { safeAddress, to: SPENDING_LIMIT_MODULE_ADDRESS, valueInWei: ZERO_VALUE, txData: spendingLimitContract.methods - .setAllowance( - beneficiary, - token === nativeCoin.address ? ZERO_ADDRESS : token, - spendingLimitInWei, - resetTimeMin, - resetBaseMin, - ) + .setAllowance(beneficiary, token, spendingLimitInWei, resetTimeMin, resetBaseMin) .encodeABI(), operation: CALL, notifiedTransaction: TX_NOTIFICATION_TYPES.NEW_SPENDING_LIMIT_TX, @@ -285,12 +274,5 @@ export const getSpendingLimitByTokenAddress = ({ return } - const { nativeCoin } = getNetworkInfo() - - return spendingLimits.find(({ token: spendingLimitTokenAddress }) => { - spendingLimitTokenAddress = sameAddress(spendingLimitTokenAddress, ZERO_ADDRESS) - ? nativeCoin.address - : spendingLimitTokenAddress - return sameAddress(spendingLimitTokenAddress, tokenAddress) - }) + return spendingLimits.find(({ token }) => sameAddress(token, tokenAddress)) } diff --git a/src/logic/tokens/store/actions/fetchSafeTokens.ts b/src/logic/tokens/store/actions/fetchSafeTokens.ts index 227c49b8..d6414ade 100644 --- a/src/logic/tokens/store/actions/fetchSafeTokens.ts +++ b/src/logic/tokens/store/actions/fetchSafeTokens.ts @@ -11,8 +11,6 @@ import { AppReduxState } from 'src/store' import { humanReadableValue } from 'src/logic/tokens/utils/humanReadableValue' import { safeActiveTokensSelector, safeSelector } from 'src/logic/safe/store/selectors' import { tokensSelector } from 'src/logic/tokens/store/selectors' -import { sameAddress, ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses' -import { getNetworkInfo } from 'src/config' import BigNumber from 'bignumber.js' import { currentCurrencySelector } from 'src/logic/currencyValues/store/selectors' @@ -27,24 +25,20 @@ interface ExtractedData { tokens: List } -const { nativeCoin } = getNetworkInfo() - const extractDataFromResult = (currentTokens: TokenState) => ( acc: ExtractedData, { balance, fiatBalance, tokenInfo }: TokenBalance, ): ExtractedData => { - const { address: tokenAddress, decimals } = tokenInfo - if (sameAddress(tokenAddress, ZERO_ADDRESS) || sameAddress(tokenAddress, nativeCoin.address)) { - acc.ethBalance = humanReadableValue(balance, 18) - } + const { address, decimals } = tokenInfo + acc.balances = acc.balances.merge({ - [tokenAddress]: { + [address]: { fiatBalance, tokenBalance: humanReadableValue(balance, Number(decimals)), }, }) - if (currentTokens && !currentTokens.get(tokenAddress)) { + if (currentTokens && !currentTokens.get(address)) { acc.tokens = acc.tokens.push(makeToken({ ...tokenInfo })) } diff --git a/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Review.tsx b/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Review.tsx index 0eced1a2..9ef63bd4 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Review.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Review.tsx @@ -5,7 +5,6 @@ import { useDispatch, useSelector } from 'react-redux' import Block from 'src/components/layout/Block' import Col from 'src/components/layout/Col' import Row from 'src/components/layout/Row' -import { getNetworkInfo } from 'src/config' import { createTransaction, CreateTransactionArgs } from 'src/logic/safe/store/actions/createTransaction' import { SafeRecordProps, SpendingLimit } from 'src/logic/safe/store/models/safe' import { @@ -20,7 +19,7 @@ import { import { MultiSendTx } from 'src/logic/safe/utils/upgradeSafe' import { makeToken, Token } from 'src/logic/tokens/store/model/token' import { fromTokenUnit, toTokenUnit } from 'src/logic/tokens/utils/humanReadableValue' -import { sameAddress, ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses' +import { sameAddress } from 'src/logic/wallets/ethAddresses' import { RESET_TIME_OPTIONS } from 'src/routes/safe/components/Settings/SpendingLimit/FormFields/ResetTime' import { AddressInfo, ResetTimeInfo, TokenInfo } from 'src/routes/safe/components/Settings/SpendingLimit/InfoDisplay' import Modal from 'src/routes/safe/components/Settings/SpendingLimit/Modal' @@ -34,8 +33,6 @@ import { EditableTxParameters } from 'src/routes/safe/components/Transactions/he import { TransactionFees } from 'src/components/TransactionsFees' import { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas' -const { nativeCoin } = getNetworkInfo() - const useExistentSpendingLimit = ({ spendingLimits, txToken, @@ -51,9 +48,7 @@ const useExistentSpendingLimit = ({ return useMemo(() => { // if `delegate` already exist, check what tokens were delegated to the _beneficiary_ `getTokens(safe, delegate)` const currentDelegate = spendingLimits?.find( - ({ delegate, token }) => - sameAddress(delegate, values.beneficiary) && - sameAddress(token, sameAddress(values.token, nativeCoin.address) ? ZERO_ADDRESS : values.token), + ({ delegate, token }) => sameAddress(delegate, values.beneficiary) && sameAddress(token, values.token), ) // let the user know that is about to replace an existent allowance diff --git a/src/routes/safe/container/selector.ts b/src/routes/safe/container/selector.ts index 11420339..6899dd78 100644 --- a/src/routes/safe/container/selector.ts +++ b/src/routes/safe/container/selector.ts @@ -4,7 +4,7 @@ import { createSelector } from 'reselect' import { Token } from 'src/logic/tokens/store/model/token' import { tokensSelector } from 'src/logic/tokens/store/selectors' import { getEthAsToken } from 'src/logic/tokens/utils/tokenHelpers' -import { isUserAnOwner, sameAddress, ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses' +import { isUserAnOwner, sameAddress } from 'src/logic/wallets/ethAddresses' import { userAccountSelector } from 'src/logic/wallets/store/selectors' import { safeActiveTokensSelector, safeBalancesSelector, safeSelector } from 'src/logic/safe/store/selectors' @@ -37,7 +37,7 @@ export const extendedSafeTokensSelector = createSelector( if (baseToken) { const updatedBaseToken = baseToken.set('balance', tokenBalance || { tokenBalance: '0', fiatBalance: '0' }) - if (sameAddress(tokenAddress, ZERO_ADDRESS) || sameAddress(tokenAddress, ethAsToken?.address)) { + if (sameAddress(tokenAddress, ethAsToken?.address)) { map.set(tokenAddress, updatedBaseToken.set('logoUri', ethAsToken?.logoUri || baseToken.logoUri)) } else { map.set(tokenAddress, updatedBaseToken) From 253137d2ba26bf68338287be184d2491820958a9 Mon Sep 17 00:00:00 2001 From: Mikhail Mikheev Date: Mon, 22 Mar 2021 20:59:12 +0300 Subject: [PATCH 15/37] Bug: Safe app communicator is not properly initialized sometimes, thus app fails to communicate (#2069) * use iframeRef inside app communicator --- src/routes/safe/components/Apps/communicator.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/routes/safe/components/Apps/communicator.ts b/src/routes/safe/components/Apps/communicator.ts index 8422d3d7..00155bec 100644 --- a/src/routes/safe/components/Apps/communicator.ts +++ b/src/routes/safe/components/Apps/communicator.ts @@ -15,12 +15,12 @@ type MessageHandler = ( ) => void | MethodToResponse[Methods] | ErrorResponse | Promise class AppCommunicator { - private iframe: HTMLIFrameElement + private iframeRef: MutableRefObject private handlers = new Map() private app: SafeApp - constructor(iframeRef: MutableRefObject, app: SafeApp) { - this.iframe = iframeRef.current + constructor(iframeRef: MutableRefObject, app: SafeApp) { + this.iframeRef = iframeRef this.app = app window.addEventListener('message', this.handleIncomingMessage) @@ -49,7 +49,7 @@ class AppCommunicator { ? MessageFormatter.makeErrorResponse(requestId, data, sdkVersion) : MessageFormatter.makeResponse(requestId, data, sdkVersion) - this.iframe.contentWindow?.postMessage(msg, this.app.url) + this.iframeRef.current?.contentWindow?.postMessage(msg, this.app.url) } handleIncomingMessage = async (msg: SDKMessageEvent): Promise => { @@ -83,7 +83,6 @@ const useAppCommunicator = ( app?: SafeApp, ): AppCommunicator | undefined => { const [communicator, setCommunicator] = useState(undefined) - useEffect(() => { let communicatorInstance const initCommunicator = (iframeRef: MutableRefObject, app: SafeApp) => { @@ -91,7 +90,7 @@ const useAppCommunicator = ( setCommunicator(communicatorInstance) } - if (app && iframeRef.current !== null) { + if (app) { initCommunicator(iframeRef as MutableRefObject, app) } From da9031568f12d014c2d8fcb0af9c2eea18687300 Mon Sep 17 00:00:00 2001 From: nicolas Date: Tue, 23 Mar 2021 05:01:49 -0300 Subject: [PATCH 16/37] Advanced Options refactor (#2029) * useEstimateTxGas: set the correct value of isOffChainSignature if gas estimation fails --- src/logic/hooks/useEstimateTransactionGas.tsx | 5 +- .../components/ConfirmTransactionModal.tsx | 12 +-- .../ContractInteraction/Review/index.tsx | 3 + .../ReviewCustomTx/index.tsx | 9 +- .../screens/ReviewCollectible/index.tsx | 3 + .../screens/ReviewSendFundsTx/index.tsx | 7 +- .../Settings/Advanced/RemoveModuleModal.tsx | 3 + .../AddOwnerModal/screens/Review/index.tsx | 3 + .../RemoveOwnerModal/screens/Review/index.tsx | 3 + .../screens/Review/index.tsx | 3 + .../SpendingLimit/NewLimitModal/Review.tsx | 3 + .../SpendingLimit/RemoveLimitModal.tsx | 3 + .../ChangeThreshold/index.tsx | 6 +- .../Settings/UpdateSafeModal/index.tsx | 9 +- .../TxList/modals/ApproveTxModal.tsx | 5 +- .../TxList/modals/RejectTxModal.tsx | 3 + .../helpers/EditTxParametersForm/index.tsx | 96 ++++++++++--------- .../helpers/EditableTxParameters.tsx | 7 +- .../helpers/TxParametersDetail/index.tsx | 81 ++++++---------- .../components/Transactions/helpers/utils.ts | 10 +- 20 files changed, 153 insertions(+), 121 deletions(-) diff --git a/src/logic/hooks/useEstimateTransactionGas.tsx b/src/logic/hooks/useEstimateTransactionGas.tsx index 8cd5e268..567d7659 100644 --- a/src/logic/hooks/useEstimateTransactionGas.tsx +++ b/src/logic/hooks/useEstimateTransactionGas.tsx @@ -218,10 +218,9 @@ export const useEstimateTransactionGas = ({ ) const fixedGasCosts = getFixedGasCosts(Number(threshold)) + const isOffChainSignature = checkIfOffChainSignatureIsPossible(isExecution, smartContractWallet, safeVersion) try { - const isOffChainSignature = checkIfOffChainSignatureIsPossible(isExecution, smartContractWallet, safeVersion) - const gasEstimation = await estimateTransactionGas({ safeAddress, txRecipient, @@ -279,7 +278,7 @@ export const useEstimateTransactionGas = ({ gasLimit: '0', isExecution, isCreation, - isOffChainSignature: false, + isOffChainSignature, }) } } diff --git a/src/routes/safe/components/Apps/components/ConfirmTransactionModal.tsx b/src/routes/safe/components/Apps/components/ConfirmTransactionModal.tsx index 784c9e33..4815717f 100644 --- a/src/routes/safe/components/Apps/components/ConfirmTransactionModal.tsx +++ b/src/routes/safe/components/Apps/components/ConfirmTransactionModal.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useMemo, useState } from 'react' import { Icon, ModalFooterConfirmation, Text, Title } from '@gnosis.pm/safe-react-components' import { Transaction } from '@gnosis.pm/safe-apps-sdk-v1' import styled from 'styled-components' -import { useDispatch, useSelector } from 'react-redux' +import { useDispatch } from 'react-redux' import AddressInfo from 'src/components/AddressInfo' import DividerLine from 'src/components/DividerLine' @@ -26,7 +26,6 @@ import GasEstimationInfo from './GasEstimationInfo' import { getNetworkInfo } from 'src/config' import { TransactionParams } from './AppFrame' import { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas' -import { safeThresholdSelector } from 'src/logic/safe/store/selectors' import Modal from 'src/components/Modal' import Row from 'src/components/layout/Row' import Hairline from 'src/components/layout/Hairline' @@ -123,7 +122,6 @@ export const ConfirmTransactionModal = ({ onTxReject, }: OwnProps): React.ReactElement | null => { const [estimatedSafeTxGas, setEstimatedSafeTxGas] = useState(0) - const threshold = useSelector(safeThresholdSelector) || 1 const txRecipient: string | undefined = useMemo(() => (txs.length > 1 ? MULTI_SEND_ADDRESS : txs[0]?.to), [txs]) const txData: string | undefined = useMemo(() => (txs.length > 1 ? encodeMultiSendCall(txs) : txs[0]?.data), [txs]) @@ -174,8 +172,6 @@ export const ConfirmTransactionModal = ({ onClose() } - const getParametersStatus = () => (threshold > 1 ? 'ETH_DISABLED' : 'ENABLED') - const confirmTransactions = async (txParameters: TxParameters) => { await dispatch( createTransaction( @@ -274,9 +270,9 @@ export const ConfirmTransactionModal = ({ {txEstimationExecutionStatus === EstimationStatus.LOADING ? null : ( @@ -297,16 +293,16 @@ export const ConfirmTransactionModal = ({ return ( {(txParameters, toggleEditMode) => ( <> - {body(txParameters, toggleEditMode)} diff --git a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/Review/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/Review/index.tsx index 5311f2e4..9352bcdb 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/Review/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/Review/index.tsx @@ -126,6 +126,8 @@ const ContractInteractionReview = ({ onClose, onPrev, tx }: Props): React.ReactE return (
diff --git a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/ReviewCustomTx/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/ReviewCustomTx/index.tsx index d7e4e736..0a39b8dc 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/ReviewCustomTx/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ContractInteraction/ReviewCustomTx/index.tsx @@ -94,7 +94,13 @@ const ReviewCustomTx = ({ onClose, onPrev, tx }: Props): React.ReactElement => { } return ( - + {(txParameters, toggleEditMode) => ( <> @@ -168,6 +174,7 @@ const ReviewCustomTx = ({ onClose, onPrev, tx }: Props): React.ReactElement => { onEdit={toggleEditMode} isTransactionCreation={isCreation} isTransactionExecution={isExecution} + isOffChainSignature={isOffChainSignature} /> {txEstimationExecutionStatus === EstimationStatus.LOADING ? null : ( diff --git a/src/routes/safe/components/Balances/SendModal/screens/ReviewCollectible/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/ReviewCollectible/index.tsx index f46d897c..ea68ea2a 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ReviewCollectible/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ReviewCollectible/index.tsx @@ -140,6 +140,8 @@ const ReviewCollectible = ({ onClose, onPrev, tx }: Props): React.ReactElement = return (
diff --git a/src/routes/safe/components/Balances/SendModal/screens/ReviewSendFundsTx/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/ReviewSendFundsTx/index.tsx index 21e0075c..1e2ab83c 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ReviewSendFundsTx/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ReviewSendFundsTx/index.tsx @@ -178,6 +178,8 @@ const ReviewSendFundsTx = ({ onClose, onPrev, tx }: ReviewTxProps): React.ReactE return ( - - {/* Disclaimer */} + + {/* Disclaimer */} {txEstimationExecutionStatus !== EstimationStatus.LOADING && (
diff --git a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/index.tsx b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/index.tsx index dd53427a..1acbd6ef 100644 --- a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/index.tsx @@ -101,6 +101,8 @@ export const ReviewAddOwner = ({ onClickBack, onClose, onSubmit, values }: Revie return ( diff --git a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/Review/index.tsx b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/Review/index.tsx index c69aba52..7d77084c 100644 --- a/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/Review/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/RemoveOwnerModal/screens/Review/index.tsx @@ -123,6 +123,8 @@ export const ReviewRemoveOwnerModal = ({ return ( {txEstimationExecutionStatus === EstimationStatus.LOADING ? null : ( diff --git a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review/index.tsx b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review/index.tsx index fa47290b..52327599 100644 --- a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review/index.tsx +++ b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review/index.tsx @@ -120,6 +120,8 @@ export const ReviewReplaceOwnerModal = ({ return ( diff --git a/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Review.tsx b/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Review.tsx index 9ef63bd4..9d803ded 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Review.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/NewLimitModal/Review.tsx @@ -233,6 +233,8 @@ export const ReviewSpendingLimits = ({ onBack, onClose, txToken, values }: Revie return (
diff --git a/src/routes/safe/components/Settings/SpendingLimit/RemoveLimitModal.tsx b/src/routes/safe/components/Settings/SpendingLimit/RemoveLimitModal.tsx index 0c5a5090..6ad0ec6e 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/RemoveLimitModal.tsx +++ b/src/routes/safe/components/Settings/SpendingLimit/RemoveLimitModal.tsx @@ -116,6 +116,8 @@ export const RemoveLimitModal = ({ onClose, spendingLimit, open }: RemoveSpendin description="Remove the selected Spending Limit" > diff --git a/src/routes/safe/components/Settings/ThresholdSettings/ChangeThreshold/index.tsx b/src/routes/safe/components/Settings/ThresholdSettings/ChangeThreshold/index.tsx index 8f6a7455..0560b57a 100644 --- a/src/routes/safe/components/Settings/ThresholdSettings/ChangeThreshold/index.tsx +++ b/src/routes/safe/components/Settings/ThresholdSettings/ChangeThreshold/index.tsx @@ -84,8 +84,6 @@ export const ChangeThresholdModal = ({ } }, [safeAddress, editedThreshold]) - const getParametersStatus = () => (threshold > 1 ? 'ETH_DISABLED' : 'ENABLED') - const handleSubmit = async ({ txParameters }) => { await dispatch( createTransaction({ @@ -120,6 +118,8 @@ export const ChangeThresholdModal = ({ return ( {txEstimationExecutionStatus !== EstimationStatus.LOADING && ( diff --git a/src/routes/safe/components/Settings/UpdateSafeModal/index.tsx b/src/routes/safe/components/Settings/UpdateSafeModal/index.tsx index aea5f0ce..b56ae3d0 100644 --- a/src/routes/safe/components/Settings/UpdateSafeModal/index.tsx +++ b/src/routes/safe/components/Settings/UpdateSafeModal/index.tsx @@ -76,7 +76,13 @@ export const UpdateSafeModal = ({ onClose, safeAddress }: Props): React.ReactEle }) return ( - + {(txParameters, toggleEditMode) => ( <> @@ -116,6 +122,7 @@ export const UpdateSafeModal = ({ onClose, safeAddress }: Props): React.ReactEle compact={false} isTransactionCreation={isCreation} isTransactionExecution={isExecution} + isOffChainSignature={isOffChainSignature} /> {txEstimationExecutionStatus === EstimationStatus.LOADING ? null : ( diff --git a/src/routes/safe/components/Transactions/TxList/modals/ApproveTxModal.tsx b/src/routes/safe/components/Transactions/TxList/modals/ApproveTxModal.tsx index 687dea96..c8b24649 100644 --- a/src/routes/safe/components/Transactions/TxList/modals/ApproveTxModal.tsx +++ b/src/routes/safe/components/Transactions/TxList/modals/ApproveTxModal.tsx @@ -317,6 +317,8 @@ export const ApproveTxModal = ({ return ( )} diff --git a/src/routes/safe/components/Transactions/TxList/modals/RejectTxModal.tsx b/src/routes/safe/components/Transactions/TxList/modals/RejectTxModal.tsx index 48280b8e..8ebf6186 100644 --- a/src/routes/safe/components/Transactions/TxList/modals/RejectTxModal.tsx +++ b/src/routes/safe/components/Transactions/TxList/modals/RejectTxModal.tsx @@ -82,6 +82,8 @@ export const RejectTxModal = ({ isOpen, onClose, gwTransaction }: Props): React. return ( diff --git a/src/routes/safe/components/Transactions/helpers/EditTxParametersForm/index.tsx b/src/routes/safe/components/Transactions/helpers/EditTxParametersForm/index.tsx index 436df795..a497eb2b 100644 --- a/src/routes/safe/components/Transactions/helpers/EditTxParametersForm/index.tsx +++ b/src/routes/safe/components/Transactions/helpers/EditTxParametersForm/index.tsx @@ -15,11 +15,11 @@ import GnoForm from 'src/components/forms/GnoForm' import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters' import { composeValidators, minValue } from 'src/components/forms/validator' -import { ParametersStatus, areSafeParamsEnabled, areEthereumParamsEnabled } from '../utils' +import { ParametersStatus, areSafeParamsEnabled, areEthereumParamsVisible, ethereumTxParametersTitle } from '../utils' import { getNetworkInfo } from 'src/config' const StyledDivider = styled(Divider)` - margin: 0px; + margin: 16px 0; ` const SafeOptions = styled.div` @@ -39,7 +39,7 @@ const EthereumOptions = styled.div` } ` const StyledLink = styled(Link)` - margin: 16px 0; + margin: 16px 0 0 0; display: inline-flex; align-items: center; @@ -65,6 +65,7 @@ interface Props { txParameters: TxParameters onClose: (txParameters?: TxParameters) => void parametersStatus: ParametersStatus + isExecution: boolean } const formValidation = (values) => { @@ -101,6 +102,7 @@ export const EditTxParametersForm = ({ onClose, txParameters, parametersStatus = 'ENABLED', + isExecution, }: Props): React.ReactElement => { const classes = useStyles() const { safeNonce, safeTxGas, ethNonce, ethGasLimit, ethGasPrice } = txParameters @@ -142,7 +144,7 @@ export const EditTxParametersForm = ({ {() => ( <> - Safe transactions parameters + Safe transaction @@ -168,49 +170,53 @@ export const EditTxParametersForm = ({ /> - - Ethereum transactions parameters - + {areEthereumParamsVisible(parametersStatus) && ( + <> + + {ethereumTxParametersTitle(isExecution)} + - - - - - + + + + + - - - How can I configure the gas price manually? - - - + + + How can I configure these parameters manually? + + + + + )} diff --git a/src/routes/safe/components/Transactions/helpers/EditableTxParameters.tsx b/src/routes/safe/components/Transactions/helpers/EditableTxParameters.tsx index a4a29ada..fb9eac57 100644 --- a/src/routes/safe/components/Transactions/helpers/EditableTxParameters.tsx +++ b/src/routes/safe/components/Transactions/helpers/EditableTxParameters.tsx @@ -7,6 +7,8 @@ import { safeThresholdSelector } from 'src/logic/safe/store/selectors' type Props = { children: (txParameters: TxParameters, toggleStatus: (txParameters?: TxParameters) => void) => any + isOffChainSignature: boolean + isExecution: boolean parametersStatus?: ParametersStatus ethGasLimit?: TxParameters['ethGasLimit'] ethGasPrice?: TxParameters['ethGasPrice'] @@ -17,6 +19,8 @@ type Props = { export const EditableTxParameters = ({ children, + isOffChainSignature, + isExecution, parametersStatus, ethGasLimit, ethGasPrice, @@ -27,7 +31,7 @@ export const EditableTxParameters = ({ const [isEditMode, toggleEditMode] = useState(false) const [useManualValues, setUseManualValues] = useState(false) const threshold = useSelector(safeThresholdSelector) || 1 - const defaultParameterStatus = threshold > 1 ? 'ETH_DISABLED' : 'ENABLED' + const defaultParameterStatus = isOffChainSignature && threshold > 1 ? 'ETH_HIDDEN' : 'ENABLED' const txParameters = useTransactionParameters({ parameterStatus: parametersStatus || defaultParameterStatus, initialEthGasLimit: ethGasLimit, @@ -65,6 +69,7 @@ export const EditableTxParameters = ({ return isEditMode ? ( void compact?: boolean parametersStatus?: ParametersStatus - isTransactionExecution: boolean isTransactionCreation: boolean + isTransactionExecution: boolean + isOffChainSignature: boolean } export const TxParametersDetail = ({ @@ -46,11 +47,12 @@ export const TxParametersDetail = ({ parametersStatus, isTransactionCreation, isTransactionExecution, + isOffChainSignature, }: Props): ReactElement | null => { const threshold = useSelector(safeThresholdSelector) || 1 - const defaultParameterStatus = threshold > 1 ? 'ETH_DISABLED' : 'ENABLED' + const defaultParameterStatus = isOffChainSignature && threshold > 1 ? 'ETH_HIDDEN' : 'ENABLED' - if (!isTransactionExecution && !isTransactionCreation) { + if (!isTransactionExecution && !isTransactionCreation && isOffChainSignature) { return null } @@ -62,7 +64,7 @@ export const TxParametersDetail = ({ - Safe transactions parameters + Safe transaction @@ -95,57 +97,30 @@ export const TxParametersDetail = ({ - - - Ethereum transaction parameters - - + {areEthereumParamsVisible(parametersStatus || defaultParameterStatus) && ( + <> + + + {ethereumTxParametersTitle(isTransactionExecution)} + + - - - Ethereum nonce - - - {txParameters.ethNonce} - - + + Nonce + {txParameters.ethNonce} + - - - Ethereum gas limit - - - {txParameters.ethGasLimit} - - - - - - Ethereum gas price - - - {txParameters.ethGasPrice} - - + + Gas limit + {txParameters.ethGasLimit} + + + Gas price + {txParameters.ethGasPrice} + + + )} Edit diff --git a/src/routes/safe/components/Transactions/helpers/utils.ts b/src/routes/safe/components/Transactions/helpers/utils.ts index ce1564e6..ccb3742f 100644 --- a/src/routes/safe/components/Transactions/helpers/utils.ts +++ b/src/routes/safe/components/Transactions/helpers/utils.ts @@ -1,8 +1,8 @@ -export type ParametersStatus = 'ENABLED' | 'DISABLED' | 'SAFE_DISABLED' | 'ETH_DISABLED' | 'CANCEL_TRANSACTION' +export type ParametersStatus = 'ENABLED' | 'DISABLED' | 'SAFE_DISABLED' | 'ETH_HIDDEN' | 'CANCEL_TRANSACTION' -export const areEthereumParamsEnabled = (parametersStatus: ParametersStatus): boolean => { +export const areEthereumParamsVisible = (parametersStatus: ParametersStatus): boolean => { return ( - parametersStatus === 'ENABLED' || (parametersStatus !== 'ETH_DISABLED' && parametersStatus !== 'CANCEL_TRANSACTION') + parametersStatus === 'ENABLED' || (parametersStatus !== 'ETH_HIDDEN' && parametersStatus !== 'CANCEL_TRANSACTION') ) } @@ -12,3 +12,7 @@ export const areSafeParamsEnabled = (parametersStatus: ParametersStatus): boolea (parametersStatus !== 'SAFE_DISABLED' && parametersStatus !== 'CANCEL_TRANSACTION') ) } + +export const ethereumTxParametersTitle = (isExecution: boolean): string => { + return `Owner transaction ${isExecution ? '(Execution)' : '(On-chain approval)'}` +} From 4297671869f931f759500c3f1fa10c198d514910 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Longoni?= Date: Tue, 23 Mar 2021 05:48:49 -0300 Subject: [PATCH 17/37] (Fix) change error message Contract address input (#2060) * change error message Contract address input --- src/components/forms/validator.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/forms/validator.ts b/src/components/forms/validator.ts index 271c27ba..09496afa 100644 --- a/src/components/forms/validator.ts +++ b/src/components/forms/validator.ts @@ -80,9 +80,7 @@ export const mustBeEthereumContractAddress = memoize( async (address: string): Promise => { const contractCode = await getWeb3().eth.getCode(address) - const errorMessage = `Input must be a valid Ethereum contract address${ - isFeatureEnabled(FEATURES.DOMAIN_LOOKUP) ? ', ENS or Unstoppable domain' : '' - }` + const errorMessage = `Must resolve to a valid smart contract address.` return !contractCode || contractCode.replace('0x', '').replace(/0/g, '') === '' ? errorMessage : undefined }, From 6bf81df271e94378211ac1eb8abfc48d82e0668f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Mart=C3=ADnez?= Date: Tue, 23 Mar 2021 13:15:49 +0100 Subject: [PATCH 18/37] Replace Safe logo in the header (#2070) * Set bigger logo size Co-authored-by: Daniel Sanchez --- .../Header/assets/gnosis-safe-multisig-logo.svg | 9 ++++----- src/components/AppLayout/Header/components/Layout.tsx | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/components/AppLayout/Header/assets/gnosis-safe-multisig-logo.svg b/src/components/AppLayout/Header/assets/gnosis-safe-multisig-logo.svg index 26f71351..62ed0fd9 100644 --- a/src/components/AppLayout/Header/assets/gnosis-safe-multisig-logo.svg +++ b/src/components/AppLayout/Header/assets/gnosis-safe-multisig-logo.svg @@ -1,6 +1,5 @@ - - - - - + + horizontal_left_small_black + + diff --git a/src/components/AppLayout/Header/components/Layout.tsx b/src/components/AppLayout/Header/components/Layout.tsx index 5845013f..85051334 100644 --- a/src/components/AppLayout/Header/components/Layout.tsx +++ b/src/components/AppLayout/Header/components/Layout.tsx @@ -38,7 +38,7 @@ const styles = () => ({ zIndex: 1301, }, logo: { - flexBasis: '114px', + flexBasis: '140px', flexShrink: '0', flexGrow: '0', maxWidth: '55px', From ab7643a51192d67e3de4b0b31a44365d8cc8791b Mon Sep 17 00:00:00 2001 From: Daniel Sanchez Date: Tue, 23 Mar 2021 14:15:14 +0100 Subject: [PATCH 19/37] Upgrade walletconnect to latest version --- src/routes/safe/components/Apps/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/safe/components/Apps/utils.ts b/src/routes/safe/components/Apps/utils.ts index 6d8a8c7d..31ae589f 100644 --- a/src/routes/safe/components/Apps/utils.ts +++ b/src/routes/safe/components/Apps/utils.ts @@ -131,7 +131,7 @@ export const staticAppsList: Array = [ }, // Wallet-Connect { - url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmRMGTA5ARMwfhYbdmK83zzMd13NnEUKFJSZEgEjKa8YQm`, + url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmU1pT35yPXxpnABcH3pZ1MxFeyYVtftT5RKhWopQmZHQV`, disabled: false, networks: [ ETHEREUM_NETWORK.MAINNET, From 63d88865e52ba86af29d5415fbba7a488e8e992b Mon Sep 17 00:00:00 2001 From: Mati Dastugue Date: Wed, 24 Mar 2021 05:44:20 -0300 Subject: [PATCH 20/37] Bugfix - Back button in modal resets form (#2075) * Use initialValues on SendFunds modal Co-authored-by: Daniel Sanchez --- .../safe/components/Balances/SendModal/index.tsx | 1 + .../Balances/SendModal/screens/SendFunds/index.tsx | 14 +++++++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/routes/safe/components/Balances/SendModal/index.tsx b/src/routes/safe/components/Balances/SendModal/index.tsx index 68a4e5ea..7623d9fa 100644 --- a/src/routes/safe/components/Balances/SendModal/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/index.tsx @@ -133,6 +133,7 @@ const SendModal = ({ {activeScreen === 'sendFunds' && ( void onReview: (txInfo: unknown) => void recipientAddress?: string @@ -80,6 +81,7 @@ const InputAdornmentChildSymbol = ({ symbol }: { symbol?: string }): ReactElemen } const SendFunds = ({ + initialValues, onClose, onReview, recipientAddress, @@ -93,12 +95,14 @@ const SendFunds = ({ const defaultEntry = { address: recipientAddress || '', name: '' } // if there's nothing to lookup for, we return the default entry - if (!recipientAddress) { + if (!initialValues?.recipientAddress && !recipientAddress) { return defaultEntry } + // if there's something to lookup for, `initialValues` has precedence over `recipientAddress` + const predefinedAddress = initialValues?.recipientAddress ?? recipientAddress const addressBookEntry = addressBook.find(({ address }) => { - return sameAddress(recipientAddress, address) + return sameAddress(predefinedAddress, address) }) // if found in the Address Book, then we return the entry @@ -170,7 +174,11 @@ const SendFunds = ({ From 48b38f550a669c9e380d66089d82f392d71d80a1 Mon Sep 17 00:00:00 2001 From: nicolas Date: Wed, 24 Mar 2021 05:58:28 -0300 Subject: [PATCH 21/37] Add tx-decoding to safe-apps review modal. (#2061) * Modal refactor and txs decoded * Add Stepped modal and split Decoded tx components * fix bytes param and add word-break to modal Co-authored-by: Daniel Sanchez --- package.json | 2 +- src/components/DecodeTxs/index.tsx | 200 +++++++++++ src/components/ModalTitle/index.tsx | 25 +- .../components/Apps/components/AppFrame.tsx | 4 +- .../components/ConfirmTransactionModal.tsx | 324 ------------------ .../ConfirmTxModal/DecodedTxDetail.tsx | 62 ++++ .../ConfirmTxModal/ReviewConfirm.tsx | 260 ++++++++++++++ .../ConfirmTxModal/SafeAppLoadError.tsx | 47 +++ .../Apps/components/ConfirmTxModal/index.tsx | 72 ++++ src/types/transactions/decode.d.ts | 24 ++ src/utils/decodeTx.ts | 18 + yarn.lock | 4 +- 12 files changed, 706 insertions(+), 336 deletions(-) create mode 100644 src/components/DecodeTxs/index.tsx delete mode 100644 src/routes/safe/components/Apps/components/ConfirmTransactionModal.tsx create mode 100644 src/routes/safe/components/Apps/components/ConfirmTxModal/DecodedTxDetail.tsx create mode 100644 src/routes/safe/components/Apps/components/ConfirmTxModal/ReviewConfirm.tsx create mode 100644 src/routes/safe/components/Apps/components/ConfirmTxModal/SafeAppLoadError.tsx create mode 100644 src/routes/safe/components/Apps/components/ConfirmTxModal/index.tsx create mode 100644 src/types/transactions/decode.d.ts create mode 100644 src/utils/decodeTx.ts diff --git a/package.json b/package.json index 825b8173..bddbfe6c 100644 --- a/package.json +++ b/package.json @@ -161,7 +161,7 @@ "@gnosis.pm/safe-apps-sdk": "1.0.3", "@gnosis.pm/safe-apps-sdk-v1": "npm:@gnosis.pm/safe-apps-sdk@0.4.2", "@gnosis.pm/safe-contracts": "1.1.1-dev.2", - "@gnosis.pm/safe-react-components": "https://github.com/gnosis/safe-react-components.git#f610327", + "@gnosis.pm/safe-react-components": "https://github.com/gnosis/safe-react-components.git#80f5db6", "@gnosis.pm/util-contracts": "2.0.6", "@ledgerhq/hw-transport-node-hid-singleton": "5.45.0", "@material-ui/core": "^4.11.0", diff --git a/src/components/DecodeTxs/index.tsx b/src/components/DecodeTxs/index.tsx new file mode 100644 index 00000000..2b49ed61 --- /dev/null +++ b/src/components/DecodeTxs/index.tsx @@ -0,0 +1,200 @@ +import React, { ReactElement } from 'react' +import styled from 'styled-components' +import { Transaction } from '@gnosis.pm/safe-apps-sdk-v1' +import { Text, EthHashInfo, CopyToClipboardBtn, IconText, FixedIcon } from '@gnosis.pm/safe-react-components' +import get from 'lodash.get' + +import { web3ReadOnly as web3 } from 'src/logic/wallets/getWeb3' +import { getExplorerInfo } from 'src/config' +import { DecodedData, DecodedDataBasicParameter, DecodedDataParameterValue } from 'src/types/transactions/decode.d' +import { DecodedTxDetail } from 'src/routes/safe/components/Apps/components/ConfirmTxModal' + +const FlexWrapper = styled.div<{ margin: number }>` + display: flex; + align-items: center; + + > :nth-child(2) { + margin-left: ${({ margin }) => margin}px; + } +` + +const BasicTxInfoWrapper = styled.div` + margin-bottom: 15px; + + > :nth-child(2) { + margin-bottom: 15px; + } +` + +const TxList = styled.div` + width: 100%; + max-height: 260px; + overflow-y: auto; + border-top: 2px solid ${({ theme }) => theme.colors.separator}; +` + +const TxListItem = styled.div` + display: flex; + justify-content: space-between; + + padding: 0 24px; + height: 50px; + border-bottom: 2px solid ${({ theme }) => theme.colors.separator}; + + :hover { + cursor: pointer; + } +` +const ElementWrapper = styled.div` + margin-bottom: 15px; +` + +export const BasicTxInfo = ({ + txRecipient, + txData, + txValue, +}: { + txRecipient: string + txData: string + txValue: string +}): ReactElement => { + return ( + + {/* TO */} + <> + + {`Send ${txValue} ETH to:`} + + + + <> + {/* Data */} + + Data (hex encoded): + + + {web3.utils.hexToBytes(txData).length} bytes + + + + + ) +} + +export const getParameterElement = (parameter: DecodedDataBasicParameter, index: number): ReactElement => { + let valueElement + + if (parameter.type === 'address') { + valueElement = ( + + ) + } + + if (parameter.type.startsWith('bytes')) { + valueElement = ( + + {web3.utils.hexToBytes(parameter.value).length} bytes + + + ) + } + + if (!valueElement) { + let value = parameter.value + if (parameter.type.endsWith('[]')) { + try { + value = JSON.stringify(parameter.value) + } catch (e) {} + } + valueElement = {value} + } + + return ( + + + {parameter.name} ({parameter.type}) + + {valueElement} + + ) +} + +const SingleTx = ({ + decodedData, + onTxItemClick, +}: { + decodedData: DecodedData | null + onTxItemClick: (decodedTxDetails: DecodedData) => void +}): ReactElement | null => { + if (!decodedData) { + return null + } + + return ( + + onTxItemClick(decodedData)}> + + + + {decodedData.method} + + + + + ) +} + +const MultiSendTx = ({ + decodedData, + onTxItemClick, +}: { + decodedData: DecodedData | null + onTxItemClick: (decodedTxDetails: DecodedDataParameterValue) => void +}): ReactElement | null => { + const txs: DecodedDataParameterValue[] | undefined = get(decodedData, 'parameters[0].valueDecoded') + + if (!txs) { + return null + } + + return ( + + {txs.map((tx, index) => ( + onTxItemClick(tx)}> + + + + {tx.dataDecoded && {tx.dataDecoded.method}} + + + + ))} + + ) +} + +type Props = { + txs: Transaction[] + decodedData: DecodedData | null + onTxItemClick: (decodedTxDetails: DecodedTxDetail) => void +} + +export const DecodeTxs = ({ txs, decodedData, onTxItemClick }: Props): ReactElement => { + return txs.length > 1 ? ( + + ) : ( + + ) +} diff --git a/src/components/ModalTitle/index.tsx b/src/components/ModalTitle/index.tsx index f2af9051..4b2a7f64 100644 --- a/src/components/ModalTitle/index.tsx +++ b/src/components/ModalTitle/index.tsx @@ -2,6 +2,7 @@ import React from 'react' import styled from 'styled-components' import IconButton from '@material-ui/core/IconButton' import Close from '@material-ui/icons/Close' +import { Icon } from '@gnosis.pm/safe-react-components' import Paragraph from 'src/components/layout/Paragraph' import { md, lg } from 'src/theme/variables' @@ -33,18 +34,28 @@ const StyledClose = styled(Close)` width: 35px; ` -const ModalTitle = ({ - iconUrl, - title, - onClose, -}: { +const GoBackWrapper = styled.div` + margin-right: 15px; +` + +type Props = { title: string - iconUrl: string + goBack?: () => void + iconUrl?: string onClose?: () => void -}): React.ReactElement => { +} + +const ModalTitle = ({ goBack, iconUrl, title, onClose }: Props): React.ReactElement => { return ( + {goBack && ( + + + + + + )} {iconUrl && } {title} diff --git a/src/routes/safe/components/Apps/components/AppFrame.tsx b/src/routes/safe/components/Apps/components/AppFrame.tsx index aebff2ab..62097c63 100644 --- a/src/routes/safe/components/Apps/components/AppFrame.tsx +++ b/src/routes/safe/components/Apps/components/AppFrame.tsx @@ -32,7 +32,7 @@ import { LoadingContainer } from 'src/components/LoaderContainer/index' import { TIMEOUT } from 'src/utils/constants' import { web3ReadOnly } from 'src/logic/wallets/getWeb3' -import { ConfirmTransactionModal } from '../components/ConfirmTransactionModal' +import { ConfirmTxModal } from '../components/ConfirmTxModal' import { useIframeMessageHandler } from '../hooks/useIframeMessageHandler' import { useLegalConsent } from '../hooks/useLegalConsent' import LegalDisclaimer from './LegalDisclaimer' @@ -354,7 +354,7 @@ const AppFrame = ({ appUrl }: Props): React.ReactElement => { /> )} - { - if (!['string', 'number'].includes(typeof t.value)) { - return false - } - - if (typeof t.value === 'string' && !/^(0x)?[0-9a-f]+$/i.test(t.value)) { - return false - } - - const isAddressValid = mustBeEthereumAddress(t.to) === undefined - return isAddressValid && !!t.data && typeof t.data === 'string' -} - -const Wrapper = styled.div` - margin-bottom: 15px; -` -const CollapseContent = styled.div` - padding: 15px 0; - - .section { - margin-bottom: 15px; - } - - .value-section { - display: flex; - align-items: center; - } -` - -const IconText = styled.div` - display: flex; - align-items: center; - - span { - margin-right: 4px; - } -` -const StyledTextBox = styled(TextBox)` - max-width: 444px; -` - -const Container = styled.div` - max-width: 480px; - padding: ${md} ${lg}; -` - -const ModalFooter = styled(Row)` - padding: ${md} ${lg}; - justify-content: center; -` -const TransactionFeesWrapper = styled.div` - background-color: ${({ theme }) => theme.colors.background}; - padding: ${sm} ${lg}; -` - -type OwnProps = { - isOpen: boolean - app: SafeApp - txs: Transaction[] - params?: TransactionParams - safeAddress: string - safeName: string - ethBalance: string - onUserConfirm: (safeTxHash: string) => void - onTxReject: () => void - onClose: () => void -} - -const { nativeCoin } = getNetworkInfo() - -const parseTxValue = (value: string | number): string => { - return web3ReadOnly.utils.toBN(value).toString() -} - -export const ConfirmTransactionModal = ({ - isOpen, - app, - txs, - safeAddress, - ethBalance, - safeName, - params, - onUserConfirm, - onClose, - onTxReject, -}: OwnProps): React.ReactElement | null => { - const [estimatedSafeTxGas, setEstimatedSafeTxGas] = useState(0) - - const txRecipient: string | undefined = useMemo(() => (txs.length > 1 ? MULTI_SEND_ADDRESS : txs[0]?.to), [txs]) - const txData: string | undefined = useMemo(() => (txs.length > 1 ? encodeMultiSendCall(txs) : txs[0]?.data), [txs]) - const txValue: string | undefined = useMemo( - () => (txs.length > 1 ? '0' : txs[0]?.value && parseTxValue(txs[0]?.value)), - [txs], - ) - const operation = useMemo(() => (txs.length > 1 ? DELEGATE_CALL : CALL), [txs]) - const [manualSafeTxGas, setManualSafeTxGas] = useState(0) - const [manualGasPrice, setManualGasPrice] = useState() - - const { - gasLimit, - gasPriceFormatted, - gasEstimation, - isOffChainSignature, - isCreation, - isExecution, - gasCostFormatted, - txEstimationExecutionStatus, - } = useEstimateTransactionGas({ - txData: txData || '', - txRecipient, - operation, - txAmount: txValue, - safeTxGas: manualSafeTxGas, - manualGasPrice, - }) - - useEffect(() => { - if (params?.safeTxGas) { - setEstimatedSafeTxGas(gasEstimation) - } - }, [params, gasEstimation]) - - const dispatch = useDispatch() - if (!isOpen) { - return null - } - - const handleTxRejection = () => { - onTxReject() - onClose() - } - - const handleUserConfirmation = (safeTxHash: string): void => { - onUserConfirm(safeTxHash) - onClose() - } - - const confirmTransactions = async (txParameters: TxParameters) => { - await dispatch( - createTransaction( - { - safeAddress, - to: txRecipient, - valueInWei: txValue, - txData, - operation, - origin: app.id, - navigateToTransactionsTab: false, - txNonce: txParameters.safeNonce, - safeTxGas: txParameters.safeTxGas - ? Number(txParameters.safeTxGas) - : Math.max(params?.safeTxGas || 0, estimatedSafeTxGas), - ethParameters: txParameters, - notifiedTransaction: TX_NOTIFICATION_TYPES.STANDARD_TX, - }, - handleUserConfirmation, - handleTxRejection, - ), - ) - } - - const closeEditModalCallback = (txParameters: TxParameters) => { - const oldGasPrice = Number(gasPriceFormatted) - const newGasPrice = Number(txParameters.ethGasPrice) - const oldSafeTxGas = Number(gasEstimation) - const newSafeTxGas = Number(txParameters.safeTxGas) - - if (newGasPrice && oldGasPrice !== newGasPrice) { - setManualGasPrice(txParameters.ethGasPrice) - } - - if (newSafeTxGas && oldSafeTxGas !== newSafeTxGas) { - setManualSafeTxGas(newSafeTxGas) - } - } - - const areTxsMalformed = txs.some((t) => !isTxValid(t)) - - const body = areTxsMalformed - ? () => ( - <> - - - Transaction error - - - This Safe App initiated a transaction which cannot be processed. Please get in touch with the developer of - this Safe App for more information. - - - ) - : (txParameters, toggleEditMode) => { - return ( - <> - - - - {txs.map((tx, index) => ( - - } title={`Transaction ${index + 1}`}> - -
- Value -
- Ether - - {fromTokenUnit(tx.value, nativeCoin.decimals)} {nativeCoin.name} - -
-
-
- Data (hex encoded)* - {tx.data} -
- - - - ))} - - {params?.safeTxGas && ( -
- SafeTxGas - {params?.safeTxGas} - -
- )} - - {/* Tx Parameters */} - - - {txEstimationExecutionStatus === EstimationStatus.LOADING ? null : ( - - - - )} - - ) - } - - return ( - - - {(txParameters, toggleEditMode) => ( - <> - - - - {body(txParameters, toggleEditMode)} - - - confirmTransactions(txParameters)} - okDisabled={areTxsMalformed} - okText="Submit" - /> - - - )} - - - ) -} diff --git a/src/routes/safe/components/Apps/components/ConfirmTxModal/DecodedTxDetail.tsx b/src/routes/safe/components/Apps/components/ConfirmTxModal/DecodedTxDetail.tsx new file mode 100644 index 00000000..8bf9bb6a --- /dev/null +++ b/src/routes/safe/components/Apps/components/ConfirmTxModal/DecodedTxDetail.tsx @@ -0,0 +1,62 @@ +import React, { ReactElement } from 'react' +import styled from 'styled-components' + +import { getNetworkInfo } from 'src/config' +import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue' +import { md, lg } from 'src/theme/variables' +import ModalTitle from 'src/components/ModalTitle' +import Hairline from 'src/components/layout/Hairline' +import { DecodedDataParameterValue, DecodedData } from 'src/types/transactions/decode.d' +import { BasicTxInfo, getParameterElement } from 'src/components/DecodeTxs' + +const { nativeCoin } = getNetworkInfo() + +const Container = styled.div` + max-width: 480px; + padding: ${md} ${lg}; + word-break: break-word; +` + +function isDataDecodedParameterValue(arg: any): arg is DecodedDataParameterValue { + return arg.operation !== undefined +} + +type Props = { + hideDecodedTxData: () => void + onClose: () => void + decodedTxData: DecodedDataParameterValue | DecodedData +} + +export const DecodedTxDetail = ({ hideDecodedTxData, onClose, decodedTxData: tx }: Props): ReactElement => { + let body + // If we are dealing with a multiSend + // decodedTxData is of type DataDecodedParameter + if (isDataDecodedParameterValue(tx)) { + const txValue = fromTokenUnit(tx.value, nativeCoin.decimals) + + body = ( + <> + + {tx.dataDecoded?.parameters.map((p, index) => getParameterElement(p, index))} + + ) + } else { + // If we are dealing with a single tx + // decodedTxData is of type DecodedData + body = <>{tx.parameters.map((p, index) => getParameterElement(p, index))} + } + + return ( + <> + + + + + {body} + + ) +} diff --git a/src/routes/safe/components/Apps/components/ConfirmTxModal/ReviewConfirm.tsx b/src/routes/safe/components/Apps/components/ConfirmTxModal/ReviewConfirm.tsx new file mode 100644 index 00000000..0a775471 --- /dev/null +++ b/src/routes/safe/components/Apps/components/ConfirmTxModal/ReviewConfirm.tsx @@ -0,0 +1,260 @@ +import React, { useEffect, useMemo, useState } from 'react' +import { ModalFooterConfirmation } from '@gnosis.pm/safe-react-components' +import styled from 'styled-components' +import { useDispatch } from 'react-redux' + +import DividerLine from 'src/components/DividerLine' +import TextBox from 'src/components/TextBox' +import ModalTitle from 'src/components/ModalTitle' +import Hairline from 'src/components/layout/Hairline' +import Heading from 'src/components/layout/Heading' +import { createTransaction } from 'src/logic/safe/store/actions/createTransaction' +import { MULTI_SEND_ADDRESS } from 'src/logic/contracts/safeContracts' +import { DELEGATE_CALL, TX_NOTIFICATION_TYPES, CALL } from 'src/logic/safe/transactions' +import { encodeMultiSendCall } from 'src/logic/safe/transactions/multisend' +import { getNetworkInfo } from 'src/config' +import { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas' +import { TransactionFees } from 'src/components/TransactionsFees' +import { EditableTxParameters } from 'src/routes/safe/components/Transactions/helpers/EditableTxParameters' +import { TxParametersDetail } from 'src/routes/safe/components/Transactions/helpers/TxParametersDetail' +import { md, lg, sm } from 'src/theme/variables' +import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters' +import AddressInfo from 'src/components/AddressInfo' +import { DecodeTxs, BasicTxInfo } from 'src/components/DecodeTxs' +import { fetchTxDecoder } from 'src/utils/decodeTx' +import { DecodedData } from 'src/types/transactions/decode.d' +import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue' + +import GasEstimationInfo from '../GasEstimationInfo' +import { ConfirmTxModalProps, DecodedTxDetail } from '.' + +const { nativeCoin } = getNetworkInfo() + +const StyledTextBox = styled(TextBox)` + max-width: 444px; +` + +const Container = styled.div` + max-width: 480px; + padding: ${md} ${lg} 0; +` +const TransactionFeesWrapper = styled.div` + background-color: ${({ theme }) => theme.colors.background}; + padding: ${sm} ${lg}; + margin-bottom: 15px; +` + +const FooterWrapper = styled.div` + margin-bottom: 15px; +` + +const DecodeTxsWrapper = styled.div` + margin: 24px -24px; +` + +type Props = ConfirmTxModalProps & { + areTxsMalformed: boolean + showDecodedTxData: (decodedTxDetails: DecodedTxDetail) => void + hidden: boolean // used to prevent re-rendering the modal each time a tx is inspected +} + +export const ReviewConfirm = ({ + app, + txs, + safeAddress, + ethBalance, + safeName, + params, + hidden, + onUserConfirm, + onClose, + onTxReject, + areTxsMalformed, + showDecodedTxData, +}: Props): React.ReactElement => { + const [estimatedSafeTxGas, setEstimatedSafeTxGas] = useState(0) + const isMultiSend = txs.length > 1 + const [decodedData, setDecodedData] = useState(null) + const dispatch = useDispatch() + + const txRecipient: string | undefined = useMemo(() => (isMultiSend ? MULTI_SEND_ADDRESS : txs[0]?.to), [ + txs, + isMultiSend, + ]) + const txData: string | undefined = useMemo(() => (isMultiSend ? encodeMultiSendCall(txs) : txs[0]?.data), [ + txs, + isMultiSend, + ]) + const txValue: string | undefined = useMemo( + () => (isMultiSend ? '0' : txs[0]?.value && fromTokenUnit(txs[0]?.value, nativeCoin.decimals)), + [txs, isMultiSend], + ) + + const operation = useMemo(() => (isMultiSend ? DELEGATE_CALL : CALL), [isMultiSend]) + const [manualSafeTxGas, setManualSafeTxGas] = useState(0) + const [manualGasPrice, setManualGasPrice] = useState() + + const { + gasLimit, + gasPriceFormatted, + gasEstimation, + isOffChainSignature, + isCreation, + isExecution, + gasCostFormatted, + txEstimationExecutionStatus, + } = useEstimateTransactionGas({ + txData: txData || '', + txRecipient, + operation, + txAmount: txValue, + safeTxGas: manualSafeTxGas, + manualGasPrice, + }) + + useEffect(() => { + if (params?.safeTxGas) { + setEstimatedSafeTxGas(gasEstimation) + } + }, [params, gasEstimation]) + + // Decode tx data. + useEffect(() => { + const decodeTxData = async () => { + const res = await fetchTxDecoder(txData) + setDecodedData(res) + } + + decodeTxData() + }, [txData]) + + const handleTxRejection = () => { + onTxReject() + onClose() + } + + const handleUserConfirmation = (safeTxHash: string): void => { + onUserConfirm(safeTxHash) + onClose() + } + + const confirmTransactions = async (txParameters: TxParameters) => { + await dispatch( + createTransaction( + { + safeAddress, + to: txRecipient, + valueInWei: txValue, + txData, + operation, + origin: app.id, + navigateToTransactionsTab: false, + txNonce: txParameters.safeNonce, + safeTxGas: txParameters.safeTxGas + ? Number(txParameters.safeTxGas) + : Math.max(params?.safeTxGas || 0, estimatedSafeTxGas), + ethParameters: txParameters, + notifiedTransaction: TX_NOTIFICATION_TYPES.STANDARD_TX, + }, + handleUserConfirmation, + handleTxRejection, + ), + ) + } + + const closeEditModalCallback = (txParameters: TxParameters) => { + const oldGasPrice = Number(gasPriceFormatted) + const newGasPrice = Number(txParameters.ethGasPrice) + const oldSafeTxGas = Number(gasEstimation) + const newSafeTxGas = Number(txParameters.safeTxGas) + + if (newGasPrice && oldGasPrice !== newGasPrice) { + setManualGasPrice(txParameters.ethGasPrice) + } + + if (newSafeTxGas && oldSafeTxGas !== newSafeTxGas) { + setManualSafeTxGas(newSafeTxGas) + } + } + + return ( + + {(txParameters, toggleEditMode) => ( + + )} + + ) +} diff --git a/src/routes/safe/components/Apps/components/ConfirmTxModal/SafeAppLoadError.tsx b/src/routes/safe/components/Apps/components/ConfirmTxModal/SafeAppLoadError.tsx new file mode 100644 index 00000000..feddb412 --- /dev/null +++ b/src/routes/safe/components/Apps/components/ConfirmTxModal/SafeAppLoadError.tsx @@ -0,0 +1,47 @@ +import React, { ReactElement } from 'react' +import { Icon, Text, Title, ModalFooterConfirmation } from '@gnosis.pm/safe-react-components' +import styled from 'styled-components' +import { ConfirmTxModalProps } from '.' + +const IconText = styled.div` + display: flex; + align-items: center; + + span { + margin-right: 4px; + } +` + +const FooterWrapper = styled.div` + margin-top: 15px; +` + +export const SafeAppLoadError = ({ onTxReject, onClose }: ConfirmTxModalProps): ReactElement => { + const handleTxRejection = () => { + onTxReject() + onClose() + } + + return ( + <> + + + Transaction error + + + This Safe App initiated a transaction which cannot be processed. Please get in touch with the developer of this + Safe App for more information. + + + + handleTxRejection()} + handleOk={() => {}} + okDisabled={true} + okText="Submit" + /> + + + ) +} diff --git a/src/routes/safe/components/Apps/components/ConfirmTxModal/index.tsx b/src/routes/safe/components/Apps/components/ConfirmTxModal/index.tsx new file mode 100644 index 00000000..ebadeded --- /dev/null +++ b/src/routes/safe/components/Apps/components/ConfirmTxModal/index.tsx @@ -0,0 +1,72 @@ +import React, { ReactElement, useState } from 'react' +import { Transaction } from '@gnosis.pm/safe-apps-sdk-v1' + +import Modal from 'src/components/Modal' +import { SafeApp } from 'src/routes/safe/components/Apps/types.d' +import { TransactionParams } from 'src/routes/safe/components/Apps/components/AppFrame' +import { mustBeEthereumAddress } from 'src/components/forms/validator' +import { SafeAppLoadError } from './SafeAppLoadError' +import { ReviewConfirm } from './ReviewConfirm' +import { DecodedDataParameterValue, DecodedData } from 'src/types/transactions/decode' +import { DecodedTxDetail } from './DecodedTxDetail' + +export type ConfirmTxModalProps = { + isOpen: boolean + app: SafeApp + txs: Transaction[] + params?: TransactionParams + safeAddress: string + safeName: string + ethBalance: string + onUserConfirm: (safeTxHash: string) => void + onTxReject: () => void + onClose: () => void +} + +const isTxValid = (t: Transaction): boolean => { + if (!['string', 'number'].includes(typeof t.value)) { + return false + } + + if (typeof t.value === 'string' && !/^(0x)?[0-9a-f]+$/i.test(t.value)) { + return false + } + + const isAddressValid = mustBeEthereumAddress(t.to) === undefined + return isAddressValid && !!t.data && typeof t.data === 'string' +} + +export type DecodedTxDetail = DecodedDataParameterValue | DecodedData | undefined + +export const ConfirmTxModal = (props: ConfirmTxModalProps): ReactElement | null => { + const [decodedTxDetails, setDecodedTxDetails] = useState() + const areTxsMalformed = props.txs.some((t) => !isTxValid(t)) + + const showDecodedTxData = setDecodedTxDetails + const hideDecodedTxData = () => setDecodedTxDetails(undefined) + + const closeDecodedTxDetail = () => { + hideDecodedTxData() + props.onClose() + } + + return ( + + {areTxsMalformed && } + {decodedTxDetails && ( + + )} + + + ) +} diff --git a/src/types/transactions/decode.d.ts b/src/types/transactions/decode.d.ts new file mode 100644 index 00000000..8beb92e7 --- /dev/null +++ b/src/types/transactions/decode.d.ts @@ -0,0 +1,24 @@ +export type DecodedDataBasicParameter = { + name: string + type: string + value: string +} +export type DecodedDataParameterValue = { + operation: 0 | 1 + to: string + value: string + data: string + dataDecoded: { + method: string + parameters: DecodedDataBasicParameter[] + } | null +} + +export type DecodedDataParameter = { + valueDecoded?: DecodedDataParameterValue[] +} & DecodedDataBasicParameter + +export type DecodedData = { + method: string + parameters: DecodedDataParameter[] +} diff --git a/src/utils/decodeTx.ts b/src/utils/decodeTx.ts new file mode 100644 index 00000000..1ab6f3c0 --- /dev/null +++ b/src/utils/decodeTx.ts @@ -0,0 +1,18 @@ +import axios from 'axios' + +import { getTxServiceUrl } from 'src/config' +import { DecodedData } from 'src/types/transactions/decode.d' + +export const fetchTxDecoder = async (txData: string): Promise => { + if (!txData?.length || txData === '0x') { + return null + } + + const url = `${getTxServiceUrl()}/data-decoder/` + try { + const res = await axios.post(url, { data: txData }) + return res.data + } catch (error) { + return null + } +} diff --git a/yarn.lock b/yarn.lock index 71abef68..00a9ec29 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1596,9 +1596,9 @@ solc "0.5.14" truffle "^5.1.21" -"@gnosis.pm/safe-react-components@https://github.com/gnosis/safe-react-components.git#f610327": +"@gnosis.pm/safe-react-components@https://github.com/gnosis/safe-react-components.git#80f5db6": version "0.5.0" - resolved "https://github.com/gnosis/safe-react-components.git#f610327c109810547513079196514b05cda63844" + resolved "https://github.com/gnosis/safe-react-components.git#80f5db672d417ea410d58c8d713e46e16e3c7e7f" dependencies: classnames "^2.2.6" react-media "^1.10.0" From 1c3b1fb37b3cc1243b2251c2739309132455d3dc Mon Sep 17 00:00:00 2001 From: Mikhail Mikheev Date: Wed, 24 Mar 2021 14:01:37 +0400 Subject: [PATCH 22/37] Feature: Update 1inch app (#2076) * update 1inch hash --- src/routes/safe/components/Apps/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/safe/components/Apps/utils.ts b/src/routes/safe/components/Apps/utils.ts index 6d8a8c7d..88cb8184 100644 --- a/src/routes/safe/components/Apps/utils.ts +++ b/src/routes/safe/components/Apps/utils.ts @@ -26,7 +26,7 @@ export type StaticAppInfo = { export const staticAppsList: Array = [ // 1inch { - url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmRWtuktjfU6WMAEJFgzBC4cUfqp3FF5uN9QoWb55SdGG5`, + url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmUXF1yVGdqUfMbhNyfM3jpP6Bw66cYnKPoWq6iHkhd3Aw`, disabled: false, networks: [ETHEREUM_NETWORK.MAINNET], }, From 8bea5cfc8818c095d051efdd45c56db4c6204d73 Mon Sep 17 00:00:00 2001 From: lukasschor Date: Wed, 24 Mar 2021 11:12:34 +0100 Subject: [PATCH 23/37] (Fix) Update to new background color everywhere (#2068) * change to new background color everywhere --- src/routes/safe/components/AddressBook/style.ts | 2 +- src/routes/safe/components/Balances/Coins/styles.ts | 2 +- src/routes/safe/components/Settings/Advanced/style.ts | 2 +- .../Settings/ManageOwners/AddOwnerModal/screens/Review/style.ts | 2 +- .../ManageOwners/ReplaceOwnerModal/screens/Review/style.ts | 2 +- src/routes/safe/components/Settings/ManageOwners/style.ts | 2 +- src/routes/safe/components/Settings/SpendingLimit/style.ts | 2 +- src/theme/mui.ts | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/routes/safe/components/AddressBook/style.ts b/src/routes/safe/components/AddressBook/style.ts index f0e6c980..a8314a31 100644 --- a/src/routes/safe/components/AddressBook/style.ts +++ b/src/routes/safe/components/AddressBook/style.ts @@ -14,7 +14,7 @@ export const styles = createStyles({ }, hide: { '&:hover': { - backgroundColor: '#fff3e2', + backgroundColor: '#f7f5f5', }, '&:hover $actions': { visibility: 'initial', diff --git a/src/routes/safe/components/Balances/Coins/styles.ts b/src/routes/safe/components/Balances/Coins/styles.ts index 3c10059d..ae4ae6ac 100644 --- a/src/routes/safe/components/Balances/Coins/styles.ts +++ b/src/routes/safe/components/Balances/Coins/styles.ts @@ -12,7 +12,7 @@ export const styles = createStyles({ }, hide: { '&:hover': { - backgroundColor: '#fff3e2', + backgroundColor: '#f7f5f5', }, '&:hover $actions': { visibility: 'initial', diff --git a/src/routes/safe/components/Settings/Advanced/style.ts b/src/routes/safe/components/Settings/Advanced/style.ts index 272453bb..69925f4f 100644 --- a/src/routes/safe/components/Settings/Advanced/style.ts +++ b/src/routes/safe/components/Settings/Advanced/style.ts @@ -8,7 +8,7 @@ export const styles = createStyles({ }, hide: { '&:hover': { - backgroundColor: '#fff3e2', + backgroundColor: '#f7f5f5', }, '&:hover $actions': { visibility: 'initial', diff --git a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/style.ts b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/style.ts index 1a94ae9b..78644036 100644 --- a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/style.ts +++ b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/Review/style.ts @@ -64,7 +64,7 @@ export const styles = createStyles({ selectedOwner: { padding: sm, alignItems: 'center', - backgroundColor: '#fff3e2', + backgroundColor: '#f7f5f5', }, user: { justifyContent: 'left', diff --git a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review/style.ts b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review/style.ts index 0cf8e781..ffa58763 100644 --- a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review/style.ts +++ b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review/style.ts @@ -69,7 +69,7 @@ export const styles = createStyles({ selectedOwnerAdded: { padding: sm, alignItems: 'center', - backgroundColor: '#fff3e2', + backgroundColor: '#f7f5f5', }, user: { justifyContent: 'left', diff --git a/src/routes/safe/components/Settings/ManageOwners/style.ts b/src/routes/safe/components/Settings/ManageOwners/style.ts index 957a20d0..0e9c4109 100644 --- a/src/routes/safe/components/Settings/ManageOwners/style.ts +++ b/src/routes/safe/components/Settings/ManageOwners/style.ts @@ -14,7 +14,7 @@ export const styles = createStyles({ }, hide: { '&:hover': { - backgroundColor: '#fff3e2', + backgroundColor: '#f7f5f5', }, '&:hover $actions': { visibility: 'initial', diff --git a/src/routes/safe/components/Settings/SpendingLimit/style.ts b/src/routes/safe/components/Settings/SpendingLimit/style.ts index 41a54264..0b8a9568 100644 --- a/src/routes/safe/components/Settings/SpendingLimit/style.ts +++ b/src/routes/safe/components/Settings/SpendingLimit/style.ts @@ -21,7 +21,7 @@ export const useStyles = makeStyles( }, hide: { '&:hover': { - backgroundColor: '#fff3e2', + backgroundColor: '#f7f5f5', }, '&:hover $actions': { visibility: 'initial', diff --git a/src/theme/mui.ts b/src/theme/mui.ts index 54562d69..4d793d54 100644 --- a/src/theme/mui.ts +++ b/src/theme/mui.ts @@ -455,7 +455,7 @@ export const DropdownListTheme = { }, button: { '&:hover': { - backgroundColor: '#fff3e2', + backgroundColor: '#f7f5f5', }, }, }, From 1b44f71485ddaba161e3cf6a77a64f4412333850 Mon Sep 17 00:00:00 2001 From: Daniel Sanchez Date: Wed, 24 Mar 2021 11:39:35 +0100 Subject: [PATCH 24/37] Update Compound safe app (#2078) --- src/routes/safe/components/Apps/utils.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/routes/safe/components/Apps/utils.ts b/src/routes/safe/components/Apps/utils.ts index a2e25269..5ff31606 100644 --- a/src/routes/safe/components/Apps/utils.ts +++ b/src/routes/safe/components/Apps/utils.ts @@ -3,7 +3,6 @@ import memoize from 'lodash.memoize' import { SafeApp, SAFE_APP_FETCH_STATUS } from './types.d' -import { getGnosisSafeAppsUrl } from 'src/config' import { getContentFromENS } from 'src/logic/wallets/getWeb3' import appsIconSvg from 'src/assets/icons/apps.svg' import { ETHEREUM_NETWORK } from 'src/config/networks/network.d' @@ -17,7 +16,6 @@ const removeLastTrailingSlash = (url) => { return url } -const gnosisAppsUrl = removeLastTrailingSlash(getGnosisSafeAppsUrl()) export type StaticAppInfo = { url: string disabled: boolean @@ -56,7 +54,11 @@ export const staticAppsList: Array = [ networks: [ETHEREUM_NETWORK.RINKEBY, ETHEREUM_NETWORK.XDAI], }, // Compound - { url: `${gnosisAppsUrl}/compound`, disabled: false, networks: [ETHEREUM_NETWORK.MAINNET, ETHEREUM_NETWORK.RINKEBY] }, + { + url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmX31xCdhFDmJzoVG33Y6kJtJ5Ujw8r5EJJBrsp8Fbjm7k`, + disabled: false, + networks: [ETHEREUM_NETWORK.MAINNET, ETHEREUM_NETWORK.RINKEBY], + }, // dHedge { url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmaiemnumMaaK9wE1pbMfm3YSBUpcFNgDh3Bf6VZCZq57Q`, From 8b35069e15e970dd8e27c9d44f527e3cd001c4a1 Mon Sep 17 00:00:00 2001 From: Mikhail Mikheev Date: Wed, 24 Mar 2021 14:50:24 +0400 Subject: [PATCH 25/37] Remove padding for iframe container (#1937) * remove padding for iframe container * update idle safe app url --- src/routes/safe/components/Apps/components/AppFrame.tsx | 1 + src/routes/safe/components/Apps/utils.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/routes/safe/components/Apps/components/AppFrame.tsx b/src/routes/safe/components/Apps/components/AppFrame.tsx index 62097c63..02d41f29 100644 --- a/src/routes/safe/components/Apps/components/AppFrame.tsx +++ b/src/routes/safe/components/Apps/components/AppFrame.tsx @@ -56,6 +56,7 @@ const AppWrapper = styled.div` const StyledCard = styled(Card)` flex-grow: 1; + padding: 0; ` const StyledIframe = styled.iframe` diff --git a/src/routes/safe/components/Apps/utils.ts b/src/routes/safe/components/Apps/utils.ts index 5ff31606..52ca818f 100644 --- a/src/routes/safe/components/Apps/utils.ts +++ b/src/routes/safe/components/Apps/utils.ts @@ -67,7 +67,7 @@ export const staticAppsList: Array = [ }, // Idle { - url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmVkGHm6gfQumJhnRfFCh7m2oSYwLXb51EKHzChpcV9J3N`, + url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmTvrLwJtyjG8QFHgvqdPhcV5DBMQ7oZceSU4uvPw9vQaj`, disabled: false, networks: [ETHEREUM_NETWORK.MAINNET, ETHEREUM_NETWORK.RINKEBY], }, From ccd54721cf67ea7c08cba4ff04ef1fa3159f050f Mon Sep 17 00:00:00 2001 From: Daniel Sanchez Date: Wed, 24 Mar 2021 11:57:52 +0100 Subject: [PATCH 26/37] Use production services in Rinkeby for PRs to master --- .github/workflows/deploy-rinkeby.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy-rinkeby.yml b/.github/workflows/deploy-rinkeby.yml index 9bc51a03..f0f5b7c7 100644 --- a/.github/workflows/deploy-rinkeby.yml +++ b/.github/workflows/deploy-rinkeby.yml @@ -68,7 +68,7 @@ jobs: # Set production flag - name: Set production flag for tag build run: echo "REACT_APP_ENV=production" >> $GITHUB_ENV - if: startsWith(github.ref, 'refs/tags/v') + if: startsWith(github.ref, 'refs/tags/v') || github.base_ref == 'master' - name: Build ${{ env.REACT_APP_NETWORK }} app ${{ env.REACT_APP_ENV }} run: yarn build From 9c0e8f4f685a4d0f4b1346ef7aa69e26c8ab0c36 Mon Sep 17 00:00:00 2001 From: Daniel Sanchez Date: Wed, 24 Mar 2021 12:16:21 +0100 Subject: [PATCH 27/37] Set production services for release PRs in all networks --- .github/workflows/deploy-ewc.yml | 7 +++---- .github/workflows/deploy-mainnet.yml | 7 +++---- .github/workflows/deploy-rinkeby.yml | 3 +-- .github/workflows/deploy-volta.yml | 7 +++---- .github/workflows/deploy-xdai.yml | 7 +++---- 5 files changed, 13 insertions(+), 18 deletions(-) diff --git a/.github/workflows/deploy-ewc.yml b/.github/workflows/deploy-ewc.yml index 8286a554..161b6f97 100644 --- a/.github/workflows/deploy-ewc.yml +++ b/.github/workflows/deploy-ewc.yml @@ -37,7 +37,7 @@ jobs: steps: - name: Remove broken apt repos [Ubuntu] - if: matrix.os == 'ubuntu-latest' + if: ${{ matrix.os }} == 'ubuntu-latest' run: | for apt_file in `grep -lr microsoft /etc/apt/sources.list.d/`; do sudo rm $apt_file; done - uses: actions/checkout@v2 @@ -65,9 +65,9 @@ jobs: yarn cache clean # Set production flag - - name: Set production flag for tag build + - name: Set production flag for release PR or tagged build run: echo "REACT_APP_ENV=production" >> $GITHUB_ENV - if: startsWith(github.ref, 'refs/tags/v') + if: startsWith(github.ref, 'refs/tags/v') || github.base_ref == 'master' - name: Build ${{ env.REACT_APP_NETWORK }} app run: yarn build @@ -101,7 +101,6 @@ jobs: * [Safe Multisig app ${{ env.REACT_APP_NETWORK }}](${{ env.REVIEW_FEATURE_URL }}/${{ env.REACT_APP_NETWORK }}/app/) repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token-user-login: 'github-actions[bot]' - allow-repeats: true if: success() && github.event.number env: REVIEW_FEATURE_URL: https://pr${{ github.event.number }}--${{ env.REPO_NAME_ALPHANUMERIC }}.review.gnosisdev.com diff --git a/.github/workflows/deploy-mainnet.yml b/.github/workflows/deploy-mainnet.yml index 4314804e..f9010c3f 100644 --- a/.github/workflows/deploy-mainnet.yml +++ b/.github/workflows/deploy-mainnet.yml @@ -36,7 +36,7 @@ jobs: steps: - name: Remove broken apt repos [Ubuntu] - if: matrix.os == 'ubuntu-latest' + if: ${{ matrix.os }} == 'ubuntu-latest' run: | for apt_file in `grep -lr microsoft /etc/apt/sources.list.d/`; do sudo rm $apt_file; done - uses: actions/checkout@v2 @@ -64,9 +64,9 @@ jobs: yarn cache clean # Set production flag - - name: Set production flag for tag build + - name: Set production flag for release PR or tagged build run: echo "REACT_APP_ENV=production" >> $GITHUB_ENV - if: startsWith(github.ref, 'refs/tags/v') + if: startsWith(github.ref, 'refs/tags/v') || github.base_ref == 'master' - name: Build ${{ env.REACT_APP_NETWORK }} app run: yarn build @@ -103,7 +103,6 @@ jobs: * [Safe Multisig app ${{ env.REACT_APP_NETWORK }}](${{ env.REVIEW_FEATURE_URL }}/${{ env.REACT_APP_NETWORK }}/app/) repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token-user-login: 'github-actions[bot]' - allow-repeats: true if: success() && github.event.number env: REVIEW_FEATURE_URL: https://pr${{ github.event.number }}--${{ env.REPO_NAME_ALPHANUMERIC }}.review.gnosisdev.com diff --git a/.github/workflows/deploy-rinkeby.yml b/.github/workflows/deploy-rinkeby.yml index f0f5b7c7..9adb828d 100644 --- a/.github/workflows/deploy-rinkeby.yml +++ b/.github/workflows/deploy-rinkeby.yml @@ -66,7 +66,7 @@ jobs: yarn cache clean # Set production flag - - name: Set production flag for tag build + - name: Set production flag for release PR or tagged build run: echo "REACT_APP_ENV=production" >> $GITHUB_ENV if: startsWith(github.ref, 'refs/tags/v') || github.base_ref == 'master' @@ -103,7 +103,6 @@ jobs: * [Safe Multisig app ${{ env.REACT_APP_NETWORK }}](${{ env.REVIEW_FEATURE_URL }}/${{ env.REACT_APP_NETWORK }}/app/) repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token-user-login: 'github-actions[bot]' - allow-repeats: true if: success() && github.event.number env: REVIEW_FEATURE_URL: https://pr${{ github.event.number }}--${{ env.REPO_NAME_ALPHANUMERIC }}.review.gnosisdev.com diff --git a/.github/workflows/deploy-volta.yml b/.github/workflows/deploy-volta.yml index 23f517c3..c6f27664 100644 --- a/.github/workflows/deploy-volta.yml +++ b/.github/workflows/deploy-volta.yml @@ -37,7 +37,7 @@ jobs: steps: - name: Remove broken apt repos [Ubuntu] - if: matrix.os == 'ubuntu-latest' + if: ${{ matrix.os }} == 'ubuntu-latest' run: | for apt_file in `grep -lr microsoft /etc/apt/sources.list.d/`; do sudo rm $apt_file; done - uses: actions/checkout@v2 @@ -65,9 +65,9 @@ jobs: yarn cache clean # Set production flag - - name: Set production flag for tag build + - name: Set production flag for release PR or tagged build run: echo "REACT_APP_ENV=production" >> $GITHUB_ENV - if: startsWith(github.ref, 'refs/tags/v') + if: startsWith(github.ref, 'refs/tags/v') || github.base_ref == 'master' - name: Build ${{ env.REACT_APP_NETWORK }} app run: yarn build @@ -101,7 +101,6 @@ jobs: * [Safe Multisig app ${{ env.REACT_APP_NETWORK }}](${{ env.REVIEW_FEATURE_URL }}/${{ env.REACT_APP_NETWORK }}/app/) repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token-user-login: 'github-actions[bot]' - allow-repeats: true if: success() && github.event.number env: REVIEW_FEATURE_URL: https://pr${{ github.event.number }}--${{ env.REPO_NAME_ALPHANUMERIC }}.review.gnosisdev.com diff --git a/.github/workflows/deploy-xdai.yml b/.github/workflows/deploy-xdai.yml index bedc557c..1f3f4b49 100644 --- a/.github/workflows/deploy-xdai.yml +++ b/.github/workflows/deploy-xdai.yml @@ -37,7 +37,7 @@ jobs: steps: - name: Remove broken apt repos [Ubuntu] - if: matrix.os == 'ubuntu-latest' + if: ${{ matrix.os }} == 'ubuntu-latest' run: | for apt_file in `grep -lr microsoft /etc/apt/sources.list.d/`; do sudo rm $apt_file; done - uses: actions/checkout@v2 @@ -65,9 +65,9 @@ jobs: yarn cache clean # Set production flag - - name: Set production flag for tag build + - name: Set production flag for release PR or tagged build run: echo "REACT_APP_ENV=production" >> $GITHUB_ENV - if: startsWith(github.ref, 'refs/tags/v') + if: startsWith(github.ref, 'refs/tags/v') || github.base_ref == 'master' - name: Build ${{ env.REACT_APP_NETWORK }} app run: yarn build @@ -101,7 +101,6 @@ jobs: * [Safe Multisig app ${{ env.REACT_APP_NETWORK }}](${{ env.REVIEW_FEATURE_URL }}/${{ env.REACT_APP_NETWORK }}/app/) repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token-user-login: 'github-actions[bot]' - allow-repeats: true if: success() && github.event.number env: REVIEW_FEATURE_URL: https://pr${{ github.event.number }}--${{ env.REPO_NAME_ALPHANUMERIC }}.review.gnosisdev.com From 6fbf5b237f4ceaaad53899becf64a9c54e0acde0 Mon Sep 17 00:00:00 2001 From: Fernando Date: Fri, 26 Mar 2021 12:23:03 -0300 Subject: [PATCH 28/37] (Fix) Delete the correct owner during Safe creation (#2084) * fix owner lookup by index number * fix tests for `calculateValuesAfterRemoving` --- .../SafeOwnersConfirmationsForm/index.tsx | 7 ++-- .../calculateValuesAfterRemoving.test.ts | 32 +++++++++---------- src/routes/open/components/fields.ts | 5 +-- src/routes/open/utils/padOwnerIndex.ts | 3 ++ 4 files changed, 26 insertions(+), 21 deletions(-) create mode 100644 src/routes/open/utils/padOwnerIndex.ts diff --git a/src/routes/open/components/SafeOwnersConfirmationsForm/index.tsx b/src/routes/open/components/SafeOwnersConfirmationsForm/index.tsx index 0e87697e..d5a868c1 100644 --- a/src/routes/open/components/SafeOwnersConfirmationsForm/index.tsx +++ b/src/routes/open/components/SafeOwnersConfirmationsForm/index.tsx @@ -4,9 +4,10 @@ import { Icon, Link, Text } from '@gnosis.pm/safe-react-components' import { makeStyles } from '@material-ui/core/styles' import CheckCircle from '@material-ui/icons/CheckCircle' import * as React from 'react' -import { styles } from './style' import styled from 'styled-components' +import { styles } from './style' +import { padOwnerIndex } from 'src/routes/open/utils/padOwnerIndex' import QRIcon from 'src/assets/icons/qrcode.svg' import trash from 'src/assets/icons/trash.svg' import { ScanQRModal } from 'src/components/ScanQRModal' @@ -88,7 +89,7 @@ export const calculateValuesAfterRemoving = (index: number, values: Record index) { // reduce by one the order of the owner - newValues[`owner${Number(ownerOrder) - 1}${ownerField}`] = values[key] + newValues[`owner${padOwnerIndex(Number(ownerOrder) - 1)}${ownerField}`] = values[key] } else { // previous owners to the deleted row newValues[key] = values[key] diff --git a/src/routes/open/components/SafeOwnersConfirmationsForm/tests/calculateValuesAfterRemoving.test.ts b/src/routes/open/components/SafeOwnersConfirmationsForm/tests/calculateValuesAfterRemoving.test.ts index a305388c..1c343a76 100644 --- a/src/routes/open/components/SafeOwnersConfirmationsForm/tests/calculateValuesAfterRemoving.test.ts +++ b/src/routes/open/components/SafeOwnersConfirmationsForm/tests/calculateValuesAfterRemoving.test.ts @@ -5,10 +5,10 @@ describe('calculateValuesAfterRemoving', () => { // Given const formContent = { name: 'My Safe', - owner0Name: 'Owner 0', - owner0Address: '0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1', - owner1Name: 'Owner 1', - owner1Address: '0xFFcf8FDEE72ac11b5c542428B35EEF5769C409f0', + owner0000Name: 'Owner 0', + owner0000Address: '0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1', + owner0001Name: 'Owner 1', + owner0001Address: '0xFFcf8FDEE72ac11b5c542428B35EEF5769C409f0', } // When @@ -17,8 +17,8 @@ describe('calculateValuesAfterRemoving', () => { // Then expect(newFormContent).toStrictEqual({ name: 'My Safe', - owner0Name: 'Owner 0', - owner0Address: '0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1', + owner0000Name: 'Owner 0', + owner0000Address: '0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1', }) }) @@ -26,12 +26,12 @@ describe('calculateValuesAfterRemoving', () => { // Given const formContent = { name: 'My Safe', - owner0Name: 'Owner 0', - owner0Address: '0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1', - owner1Name: 'Owner 1', - owner1Address: '0xFFcf8FDEE72ac11b5c542428B35EEF5769C409f0', - owner2Name: 'Owner 2', - owner2Address: '0x22d491Bde2303f2f43325b2108D26f1eAbA1e32b', + owner0000Name: 'Owner 0', + owner0000Address: '0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1', + owner0001Name: 'Owner 1', + owner0001Address: '0xFFcf8FDEE72ac11b5c542428B35EEF5769C409f0', + owner0002Name: 'Owner 2', + owner0002Address: '0x22d491Bde2303f2f43325b2108D26f1eAbA1e32b', } // When @@ -40,10 +40,10 @@ describe('calculateValuesAfterRemoving', () => { // Then expect(newFormContent).toStrictEqual({ name: 'My Safe', - owner0Name: 'Owner 0', - owner0Address: '0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1', - owner1Name: 'Owner 2', - owner1Address: '0x22d491Bde2303f2f43325b2108D26f1eAbA1e32b', + owner0000Name: 'Owner 0', + owner0000Address: '0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1', + owner0001Name: 'Owner 2', + owner0001Address: '0x22d491Bde2303f2f43325b2108D26f1eAbA1e32b', }) }) }) diff --git a/src/routes/open/components/fields.ts b/src/routes/open/components/fields.ts index eb9cba20..539076e2 100644 --- a/src/routes/open/components/fields.ts +++ b/src/routes/open/components/fields.ts @@ -1,4 +1,5 @@ import { LoadFormValues } from 'src/routes/load/container/Load' +import { padOwnerIndex } from 'src/routes/open/utils/padOwnerIndex' import { CreateSafeValues } from 'src/routes/open/utils/safeDataExtractor' export const FIELD_NAME = 'name' @@ -7,8 +8,8 @@ export const FIELD_OWNERS = 'owners' export const FIELD_SAFE_NAME = 'safeName' export const FIELD_CREATION_PROXY_SALT = 'safeCreationSalt' -export const getOwnerNameBy = (index: number): string => `owner${index.toString().padStart(4, '0')}Name` -export const getOwnerAddressBy = (index: number): string => `owner${index.toString().padStart(4, '0')}Address` +export const getOwnerNameBy = (index: number): string => `owner${padOwnerIndex(index)}Name` +export const getOwnerAddressBy = (index: number): string => `owner${padOwnerIndex(index)}Address` export const getNumOwnersFrom = (values: CreateSafeValues | LoadFormValues): number => { const accounts = Object.keys(values) diff --git a/src/routes/open/utils/padOwnerIndex.ts b/src/routes/open/utils/padOwnerIndex.ts new file mode 100644 index 00000000..b010fc6f --- /dev/null +++ b/src/routes/open/utils/padOwnerIndex.ts @@ -0,0 +1,3 @@ +export const padOwnerIndex = (index: number | string): string => { + return index.toString().padStart(4, '0') +} From eb201e2621e53db60eed653ef90994d37056f48c Mon Sep 17 00:00:00 2001 From: Daniel Sanchez Date: Fri, 26 Mar 2021 16:23:28 +0100 Subject: [PATCH 29/37] Fix balance value in send funds form (#2085) --- src/components/App/index.tsx | 6 +++--- src/logic/safe/hooks/useLoadSafe.tsx | 4 ++-- src/logic/safe/hooks/useSafeScheduledUpdates.tsx | 14 +++----------- src/logic/safe/store/actions/fetchSafe.ts | 8 +++++--- src/logic/safe/store/selectors/index.ts | 4 ++-- src/logic/tokens/store/actions/fetchSafeTokens.ts | 6 ++++++ 6 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/components/App/index.tsx b/src/components/App/index.tsx index 189000d3..ae64a97d 100644 --- a/src/components/App/index.tsx +++ b/src/components/App/index.tsx @@ -21,7 +21,7 @@ import { ETHEREUM_NETWORK } from 'src/config/networks/network.d' import { networkSelector } from 'src/logic/wallets/store/selectors' import { SAFELIST_ADDRESS, WELCOME_ADDRESS } from 'src/routes/routes' import { - safeFiatBalancesTotalSelector, + safeTotalFiatBalanceSelector, safeNameSelector, safeParamAddressFromStateSelector, } from 'src/logic/safe/store/selectors' @@ -79,7 +79,7 @@ const App: React.FC = ({ children }) => { const safeAddress = useSelector(safeParamAddressFromStateSelector) const safeName = useSelector(safeNameSelector) ?? '' const { safeActionsState, onShow, onHide, showSendFunds, hideSendFunds } = useSafeActions() - const currentSafeBalance = useSelector(safeFiatBalancesTotalSelector) + const currentSafeBalance = useSelector(safeTotalFiatBalanceSelector) const currentCurrency = useSelector(currentCurrencySelector) const granted = useSelector(grantedSelector) const sidebarItems = useSidebarItems() @@ -88,7 +88,7 @@ const App: React.FC = ({ children }) => { useSafeScheduledUpdates(safeLoaded, safeAddress) const sendFunds = safeActionsState.sendFunds - const formattedTotalBalance = currentSafeBalance ? formatAmountInUsFormat(currentSafeBalance) : '' + const formattedTotalBalance = currentSafeBalance ? formatAmountInUsFormat(currentSafeBalance.toString()) : '' const balance = !!formattedTotalBalance && !!currentCurrency ? `${formattedTotalBalance} ${currentCurrency}` : undefined diff --git a/src/logic/safe/hooks/useLoadSafe.tsx b/src/logic/safe/hooks/useLoadSafe.tsx index bf07f60b..6adea9d6 100644 --- a/src/logic/safe/hooks/useLoadSafe.tsx +++ b/src/logic/safe/hooks/useLoadSafe.tsx @@ -21,8 +21,8 @@ export const useLoadSafe = (safeAddress?: string): boolean => { await dispatch(fetchSafe(safeAddress)) setIsSafeLoaded(true) await dispatch(fetchSafeTokens(safeAddress)) - dispatch(updateAvailableCurrencies()) - dispatch(fetchTransactions(safeAddress)) + await dispatch(updateAvailableCurrencies()) + await dispatch(fetchTransactions(safeAddress)) dispatch(addViewedSafe(safeAddress)) } } diff --git a/src/logic/safe/hooks/useSafeScheduledUpdates.tsx b/src/logic/safe/hooks/useSafeScheduledUpdates.tsx index aba374b4..fc14e5e8 100644 --- a/src/logic/safe/hooks/useSafeScheduledUpdates.tsx +++ b/src/logic/safe/hooks/useSafeScheduledUpdates.tsx @@ -3,7 +3,6 @@ import { batch, useDispatch } from 'react-redux' import { fetchCollectibles } from 'src/logic/collectibles/store/actions/fetchCollectibles' import { fetchSafeTokens } from 'src/logic/tokens/store/actions/fetchSafeTokens' -import { fetchEtherBalance } from 'src/logic/safe/store/actions/fetchEtherBalance' import { checkAndUpdateSafe } from 'src/logic/safe/store/actions/fetchSafe' import fetchTransactions from 'src/logic/safe/store/actions/transactions/fetchTransactions' import { TIMEOUT } from 'src/utils/constants' @@ -17,25 +16,18 @@ export const useSafeScheduledUpdates = (safeLoaded: boolean, safeAddress?: strin // has to run again let mounted = true const fetchSafeData = async (address: string): Promise => { - await batch(async () => { + batch(async () => { await Promise.all([ - dispatch(fetchEtherBalance(address)), dispatch(fetchSafeTokens(address)), dispatch(fetchTransactions(address)), dispatch(fetchCollectibles(address)), dispatch(checkAndUpdateSafe(address)), ]) }) - - if (mounted) { - timer.current = window.setTimeout(() => { - fetchSafeData(address) - }, TIMEOUT * 3) - } } - if (safeAddress && safeLoaded) { - fetchSafeData(safeAddress) + if (safeAddress && safeLoaded && mounted && !timer.current) { + timer.current = window.setTimeout(() => fetchSafeData(safeAddress), TIMEOUT * 3) } return () => { diff --git a/src/logic/safe/store/actions/fetchSafe.ts b/src/logic/safe/store/actions/fetchSafe.ts index 5ffa962a..d8a014e6 100644 --- a/src/logic/safe/store/actions/fetchSafe.ts +++ b/src/logic/safe/store/actions/fetchSafe.ts @@ -15,7 +15,7 @@ import { makeOwner } from 'src/logic/safe/store/models/owner' import { checksumAddress } from 'src/utils/checksumAddress' import { SafeOwner, SafeRecordProps } from 'src/logic/safe/store/models/safe' import { AppReduxState } from 'src/store' -import { latestMasterContractVersionSelector } from 'src/logic/safe/store/selectors' +import { latestMasterContractVersionSelector, safeTotalFiatBalanceSelector } from 'src/logic/safe/store/selectors' import { getSafeInfo } from 'src/logic/safe/utils/safeInformation' import { getModules } from 'src/logic/safe/utils/modules' import { getSpendingLimits } from 'src/logic/safe/utils/spendingLimits' @@ -46,6 +46,7 @@ export const buildSafe = async ( safeAdd: string, safeName: string, latestMasterContractVersion?: string, + totalFiatBalance?: number, ): Promise => { const safeAddress = checksumAddress(safeAdd) @@ -80,7 +81,7 @@ export const buildSafe = async ( threshold, owners, ethBalance, - totalFiatBalance: 0, + totalFiatBalance: totalFiatBalance || 0, nonce, currentVersion: currentVersion ?? '', needsUpdate, @@ -160,7 +161,8 @@ export default (safeAdd: string) => async ( const safeAddress = checksumAddress(safeAdd) const safeName = (await getSafeName(safeAddress)) || 'LOADED SAFE' const latestMasterContractVersion = latestMasterContractVersionSelector(getState()) - const safeProps = await buildSafe(safeAddress, safeName, latestMasterContractVersion) + const totalFiatBalance = safeTotalFiatBalanceSelector(getState()) + const safeProps = await buildSafe(safeAddress, safeName, latestMasterContractVersion, totalFiatBalance) // `updateSafe`, as `loadSafesFromStorage` will populate the store previous to this call // and `addSafe` will only add a newly non-existent safe diff --git a/src/logic/safe/store/selectors/index.ts b/src/logic/safe/store/selectors/index.ts index 2c9ebd7d..48728cf5 100644 --- a/src/logic/safe/store/selectors/index.ts +++ b/src/logic/safe/store/selectors/index.ts @@ -127,6 +127,6 @@ export const getActiveTokensAddressesForAllSafes = createSelector(safesListSelec return addresses }) -export const safeFiatBalancesTotalSelector = createSelector(safeSelector, (currentSafe) => { - return currentSafe?.totalFiatBalance.toString() +export const safeTotalFiatBalanceSelector = createSelector(safeSelector, (currentSafe) => { + return currentSafe?.totalFiatBalance }) diff --git a/src/logic/tokens/store/actions/fetchSafeTokens.ts b/src/logic/tokens/store/actions/fetchSafeTokens.ts index d6414ade..0aebb5a6 100644 --- a/src/logic/tokens/store/actions/fetchSafeTokens.ts +++ b/src/logic/tokens/store/actions/fetchSafeTokens.ts @@ -13,6 +13,7 @@ import { safeActiveTokensSelector, safeSelector } from 'src/logic/safe/store/sel import { tokensSelector } from 'src/logic/tokens/store/selectors' import BigNumber from 'bignumber.js' import { currentCurrencySelector } from 'src/logic/currencyValues/store/selectors' +import { ZERO_ADDRESS, sameAddress } from 'src/logic/wallets/ethAddresses' export type BalanceRecord = { tokenBalance: string @@ -38,6 +39,11 @@ const extractDataFromResult = (currentTokens: TokenState) => ( }, }) + // Extract network token balance from backend balances + if (sameAddress(address, ZERO_ADDRESS)) { + acc.ethBalance = humanReadableValue(balance, Number(decimals)) + } + if (currentTokens && !currentTokens.get(address)) { acc.tokens = acc.tokens.push(makeToken({ ...tokenInfo })) } From c25946ab8f12296043add7995d7e7481fc40a2e4 Mon Sep 17 00:00:00 2001 From: Daniel Sanchez Date: Fri, 26 Mar 2021 16:23:52 +0100 Subject: [PATCH 30/37] Remove validator to check that safeTxGas is lower than gasLimit (#2089) --- .../helpers/EditTxParametersForm/index.tsx | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/src/routes/safe/components/Transactions/helpers/EditTxParametersForm/index.tsx b/src/routes/safe/components/Transactions/helpers/EditTxParametersForm/index.tsx index a497eb2b..2d21fcab 100644 --- a/src/routes/safe/components/Transactions/helpers/EditTxParametersForm/index.tsx +++ b/src/routes/safe/components/Transactions/helpers/EditTxParametersForm/index.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { ReactElement } from 'react' import IconButton from '@material-ui/core/IconButton' import Close from '@material-ui/icons/Close' import { makeStyles } from '@material-ui/core/styles' @@ -13,10 +13,9 @@ import Row from 'src/components/layout/Row' import { styles } from './style' import GnoForm from 'src/components/forms/GnoForm' import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters' -import { composeValidators, minValue } from 'src/components/forms/validator' +import { minValue } from 'src/components/forms/validator' import { ParametersStatus, areSafeParamsEnabled, areEthereumParamsVisible, ethereumTxParametersTitle } from '../utils' -import { getNetworkInfo } from 'src/config' const StyledDivider = styled(Divider)` margin: 16px 0; @@ -59,8 +58,6 @@ const StyledTextMt = styled(Text)` const useStyles = makeStyles(styles) -const { label } = getNetworkInfo() - interface Props { txParameters: TxParameters onClose: (txParameters?: TxParameters) => void @@ -79,15 +76,7 @@ const formValidation = (values) => { const safeNonceValidation = minValue(0, true)(safeNonce) - const safeTxGasValidation = composeValidators(minValue(0, true), (value: string) => { - if (!value) { - return - } - - if (Number(value) > Number(ethGasLimit)) { - return `Bigger than ${label} gas limit.` - } - })(safeTxGas) + const safeTxGasValidation = minValue(0, true)(safeTxGas) return { ethGasLimit: ethGasLimitValidation, @@ -103,7 +92,7 @@ export const EditTxParametersForm = ({ txParameters, parametersStatus = 'ENABLED', isExecution, -}: Props): React.ReactElement => { +}: Props): ReactElement => { const classes = useStyles() const { safeNonce, safeTxGas, ethNonce, ethGasLimit, ethGasPrice } = txParameters From 500ca4b083e8c45d789606e0834488eaced06161 Mon Sep 17 00:00:00 2001 From: Daniel Sanchez Date: Fri, 26 Mar 2021 16:31:14 +0100 Subject: [PATCH 31/37] Cancel running workflows when you add changes to the same branch --- .github/workflows/deploy-ewc.yml | 4 ++++ .github/workflows/deploy-mainnet.yml | 4 ++++ .github/workflows/deploy-rinkeby.yml | 4 ++++ .github/workflows/deploy-volta.yml | 4 ++++ .github/workflows/deploy-xdai.yml | 4 ++++ .github/workflows/test.yml | 4 ++++ 6 files changed, 24 insertions(+) diff --git a/.github/workflows/deploy-ewc.yml b/.github/workflows/deploy-ewc.yml index 161b6f97..ea89aa90 100644 --- a/.github/workflows/deploy-ewc.yml +++ b/.github/workflows/deploy-ewc.yml @@ -36,6 +36,10 @@ jobs: runs-on: ubuntu-latest steps: + - name: Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.8.0 + with: + access_token: ${{ github.token }} - name: Remove broken apt repos [Ubuntu] if: ${{ matrix.os }} == 'ubuntu-latest' run: | diff --git a/.github/workflows/deploy-mainnet.yml b/.github/workflows/deploy-mainnet.yml index f9010c3f..b6159262 100644 --- a/.github/workflows/deploy-mainnet.yml +++ b/.github/workflows/deploy-mainnet.yml @@ -35,6 +35,10 @@ jobs: runs-on: ubuntu-latest steps: + - name: Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.8.0 + with: + access_token: ${{ github.token }} - name: Remove broken apt repos [Ubuntu] if: ${{ matrix.os }} == 'ubuntu-latest' run: | diff --git a/.github/workflows/deploy-rinkeby.yml b/.github/workflows/deploy-rinkeby.yml index 9adb828d..4d35a09a 100644 --- a/.github/workflows/deploy-rinkeby.yml +++ b/.github/workflows/deploy-rinkeby.yml @@ -37,6 +37,10 @@ jobs: name: Deployment runs-on: ubuntu-latest steps: + - name: Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.8.0 + with: + access_token: ${{ github.token }} - name: Remove broken apt repos [Ubuntu] if: ${{ matrix.os }} == 'ubuntu-latest' run: | diff --git a/.github/workflows/deploy-volta.yml b/.github/workflows/deploy-volta.yml index c6f27664..f22869e7 100644 --- a/.github/workflows/deploy-volta.yml +++ b/.github/workflows/deploy-volta.yml @@ -36,6 +36,10 @@ jobs: runs-on: ubuntu-latest steps: + - name: Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.8.0 + with: + access_token: ${{ github.token }} - name: Remove broken apt repos [Ubuntu] if: ${{ matrix.os }} == 'ubuntu-latest' run: | diff --git a/.github/workflows/deploy-xdai.yml b/.github/workflows/deploy-xdai.yml index 1f3f4b49..74862320 100644 --- a/.github/workflows/deploy-xdai.yml +++ b/.github/workflows/deploy-xdai.yml @@ -36,6 +36,10 @@ jobs: runs-on: ubuntu-latest steps: + - name: Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.8.0 + with: + access_token: ${{ github.token }} - name: Remove broken apt repos [Ubuntu] if: ${{ matrix.os }} == 'ubuntu-latest' run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 147dbfda..e251648e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,6 +10,10 @@ jobs: test: runs-on: ubuntu-latest steps: + - name: Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.8.0 + with: + access_token: ${{ github.token }} - uses: actions/checkout@v2 - name: Setup Node.js uses: actions/setup-node@v2 From 23bfaf432c6fd7810a6c26c16eab421d091705fd Mon Sep 17 00:00:00 2001 From: Daniel Sanchez Date: Fri, 26 Mar 2021 16:40:11 +0100 Subject: [PATCH 32/37] Update wallet connect to correctly show the banner (#2094) --- src/routes/safe/components/Apps/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/safe/components/Apps/utils.ts b/src/routes/safe/components/Apps/utils.ts index 52ca818f..fa66d807 100644 --- a/src/routes/safe/components/Apps/utils.ts +++ b/src/routes/safe/components/Apps/utils.ts @@ -133,7 +133,7 @@ export const staticAppsList: Array = [ }, // Wallet-Connect { - url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmU1pT35yPXxpnABcH3pZ1MxFeyYVtftT5RKhWopQmZHQV`, + url: `${process.env.REACT_APP_IPFS_GATEWAY}/QmX9B982ZAaBzbm6yBoZUS3uLgcizYA6wW65RCXVRZkG6f`, disabled: false, networks: [ ETHEREUM_NETWORK.MAINNET, From e20c6dc03eaf08df3b9105d9ca616c57e2f4f48f Mon Sep 17 00:00:00 2001 From: Daniel Sanchez Date: Mon, 29 Mar 2021 10:46:15 +0200 Subject: [PATCH 33/37] Restore fetchSafe timer to previous configuration --- src/logic/safe/hooks/useSafeScheduledUpdates.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/logic/safe/hooks/useSafeScheduledUpdates.tsx b/src/logic/safe/hooks/useSafeScheduledUpdates.tsx index fc14e5e8..c0d65ab6 100644 --- a/src/logic/safe/hooks/useSafeScheduledUpdates.tsx +++ b/src/logic/safe/hooks/useSafeScheduledUpdates.tsx @@ -3,7 +3,7 @@ import { batch, useDispatch } from 'react-redux' import { fetchCollectibles } from 'src/logic/collectibles/store/actions/fetchCollectibles' import { fetchSafeTokens } from 'src/logic/tokens/store/actions/fetchSafeTokens' -import { checkAndUpdateSafe } from 'src/logic/safe/store/actions/fetchSafe' +import fetchSafe, { checkAndUpdateSafe } from 'src/logic/safe/store/actions/fetchSafe' import fetchTransactions from 'src/logic/safe/store/actions/transactions/fetchTransactions' import { TIMEOUT } from 'src/utils/constants' @@ -24,10 +24,14 @@ export const useSafeScheduledUpdates = (safeLoaded: boolean, safeAddress?: strin dispatch(checkAndUpdateSafe(address)), ]) }) + + if (mounted) { + timer.current = window.setTimeout(() => fetchSafeData(address), TIMEOUT * 3) + } } - if (safeAddress && safeLoaded && mounted && !timer.current) { - timer.current = window.setTimeout(() => fetchSafeData(safeAddress), TIMEOUT * 3) + if (safeAddress && safeLoaded) { + fetchSafeData(safeAddress) } return () => { From 8902f37e96058a9c31779cbcde69246e0aa43e47 Mon Sep 17 00:00:00 2001 From: Daniel Sanchez Date: Mon, 29 Mar 2021 10:46:51 +0200 Subject: [PATCH 34/37] Remove unused parameter --- src/logic/safe/hooks/useSafeScheduledUpdates.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/logic/safe/hooks/useSafeScheduledUpdates.tsx b/src/logic/safe/hooks/useSafeScheduledUpdates.tsx index c0d65ab6..99d8bffe 100644 --- a/src/logic/safe/hooks/useSafeScheduledUpdates.tsx +++ b/src/logic/safe/hooks/useSafeScheduledUpdates.tsx @@ -3,7 +3,7 @@ import { batch, useDispatch } from 'react-redux' import { fetchCollectibles } from 'src/logic/collectibles/store/actions/fetchCollectibles' import { fetchSafeTokens } from 'src/logic/tokens/store/actions/fetchSafeTokens' -import fetchSafe, { checkAndUpdateSafe } from 'src/logic/safe/store/actions/fetchSafe' +import { checkAndUpdateSafe } from 'src/logic/safe/store/actions/fetchSafe' import fetchTransactions from 'src/logic/safe/store/actions/transactions/fetchTransactions' import { TIMEOUT } from 'src/utils/constants' From 15d98e2d846f5194647fbb8452310245732b6236 Mon Sep 17 00:00:00 2001 From: Daniel Sanchez Date: Tue, 30 Mar 2021 09:39:15 +0200 Subject: [PATCH 35/37] Change hardcoded ETH for nativeCoin symbol --- src/components/DecodeTxs/index.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/DecodeTxs/index.tsx b/src/components/DecodeTxs/index.tsx index 2b49ed61..8b242e99 100644 --- a/src/components/DecodeTxs/index.tsx +++ b/src/components/DecodeTxs/index.tsx @@ -5,7 +5,7 @@ import { Text, EthHashInfo, CopyToClipboardBtn, IconText, FixedIcon } from '@gno import get from 'lodash.get' import { web3ReadOnly as web3 } from 'src/logic/wallets/getWeb3' -import { getExplorerInfo } from 'src/config' +import { getExplorerInfo, getNetworkInfo } from 'src/config' import { DecodedData, DecodedDataBasicParameter, DecodedDataParameterValue } from 'src/types/transactions/decode.d' import { DecodedTxDetail } from 'src/routes/safe/components/Apps/components/ConfirmTxModal' @@ -58,12 +58,14 @@ export const BasicTxInfo = ({ txData: string txValue: string }): ReactElement => { + const { nativeCoin } = getNetworkInfo() + return ( {/* TO */} <> - {`Send ${txValue} ETH to:`} + {`Send ${txValue} ${nativeCoin.symbol} to:`} Date: Tue, 30 Mar 2021 10:01:36 +0200 Subject: [PATCH 36/37] Add fix to swap symbol and name for sidechains --- .../safe/api/fetchTokenCurrenciesBalances.ts | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/logic/safe/api/fetchTokenCurrenciesBalances.ts b/src/logic/safe/api/fetchTokenCurrenciesBalances.ts index d1185a3d..2fcc4a93 100644 --- a/src/logic/safe/api/fetchTokenCurrenciesBalances.ts +++ b/src/logic/safe/api/fetchTokenCurrenciesBalances.ts @@ -1,9 +1,11 @@ import axios from 'axios' -import { getSafeClientGatewayBaseUrl } from 'src/config' +import { getSafeClientGatewayBaseUrl, getNetworkInfo } from 'src/config' import { TokenProps } from 'src/logic/tokens/store/model/token' import { checksumAddress } from 'src/utils/checksumAddress' +import { ZERO_ADDRESS, sameAddress } from 'src/logic/wallets/ethAddresses' + export type TokenBalance = { tokenInfo: TokenProps balance: string @@ -33,5 +35,24 @@ export const fetchTokenCurrenciesBalances = async ({ checksumAddress(safeAddress), )}/balances/${selectedCurrency}/?trusted=${trustedTokens}&exclude_spam=${excludeSpamTokens}` - return axios.get(url).then(({ data }) => data) + return axios.get(url).then(({ data }) => { + // Currently the client-gateway is not returning the balance using network token symbol and name + // FIXME remove this logic and return data directly once this is fixed + const { nativeCoin } = getNetworkInfo() + + if (data.items && data.items.length) { + data.items = data.items.map((element) => { + const { tokenInfo } = element + if (sameAddress(ZERO_ADDRESS, tokenInfo.address)) { + // If it's native coin we swap symbol and name + tokenInfo.symbol = nativeCoin.symbol + tokenInfo.name = nativeCoin.name + } + + return element + }) + } + + return data + }) } From 4b8e14347b5c19370d008cde80dc18d4ac241072 Mon Sep 17 00:00:00 2001 From: Daniel Sanchez Date: Tue, 30 Mar 2021 11:04:33 +0200 Subject: [PATCH 37/37] Set v3.3.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bddbfe6c..dd954f4a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "safe-react", - "version": "3.2.0", + "version": "3.3.0", "description": "Allowing crypto users manage funds in a safer way", "website": "https://github.com/gnosis/safe-react#readme", "bugs": {