From f92ebbaa70259591b0ea601aac03fcc843ea1431 Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Fri, 29 Oct 2021 22:28:00 +0200 Subject: [PATCH 01/30] Port Table and related components from Celo Wallet Set up skeleton for granda proposal list page --- src/components/Chevron.tsx | 51 +++++++++ src/components/animation/Spinner.module.css | 38 +++++++ src/components/animation/Spinner.tsx | 16 +++ src/components/table/Table.module.css | 0 src/components/table/Table.tsx | 118 ++++++++++++++++++++ src/features/granda/ProposalList.tsx | 67 +++++++++++ src/images/icons/support-coin.png | Bin 13602 -> 0 bytes src/pages/granda.tsx | 18 +-- 8 files changed, 292 insertions(+), 16 deletions(-) create mode 100644 src/components/Chevron.tsx create mode 100644 src/components/animation/Spinner.module.css create mode 100644 src/components/animation/Spinner.tsx create mode 100644 src/components/table/Table.module.css create mode 100644 src/components/table/Table.tsx create mode 100644 src/features/granda/ProposalList.tsx delete mode 100644 src/images/icons/support-coin.png diff --git a/src/components/Chevron.tsx b/src/components/Chevron.tsx new file mode 100644 index 0000000..5a4c0cb --- /dev/null +++ b/src/components/Chevron.tsx @@ -0,0 +1,51 @@ +import { memo } from 'react' + +interface Props { + width?: string | number + height?: string | number + direction: 'n' | 'e' | 's' | 'w' + color?: string + classes?: string +} + +function _ChevronIcon({ width, height, direction, color, classes }: Props) { + let className: string + switch (direction) { + case 'n': + className = 'rotate-180' + break + case 'e': + className = 'rotate-270' + break + case 's': + className = '' + break + case 'w': + className = 'rotate-90' + break + default: + throw new Error(`Invalid chevron direction ${direction}`) + } + + return ( + + + + ) +} + +export const ChevronIcon = memo(_ChevronIcon) diff --git a/src/components/animation/Spinner.module.css b/src/components/animation/Spinner.module.css new file mode 100644 index 0000000..864bbc8 --- /dev/null +++ b/src/components/animation/Spinner.module.css @@ -0,0 +1,38 @@ +.spinner { + display: inline-block; + position: relative; + width: 80px; + height: 80px; +} + +.spinner div { + box-sizing: border-box; + display: block; + position: absolute; + width: 64px; + height: 64px; + margin: 8px; + border: 8px solid #2e3338; + border-radius: 50%; + animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite; + border-color: #2e3338 transparent transparent transparent; +} + +.spinner div:nth-of-type(1) { + animation-delay: -0.45s; +} +.spinner div:nth-of-type(2) { + animation-delay: -0.3s; +} +.spinner div:nth-of-type(3) { + animation-delay: -0.15s; +} + +@keyframes lds-ring { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} diff --git a/src/components/animation/Spinner.tsx b/src/components/animation/Spinner.tsx new file mode 100644 index 0000000..b0b527d --- /dev/null +++ b/src/components/animation/Spinner.tsx @@ -0,0 +1,16 @@ +import { memo } from 'react' +import styles from 'src/components/animation/Spinner.module.css' + +// From https://loading.io/css/ +function _Spinner() { + return ( +
+
+
+
+
+
+ ) +} + +export const Spinner = memo(_Spinner) diff --git a/src/components/table/Table.module.css b/src/components/table/Table.module.css new file mode 100644 index 0000000..e69de29 diff --git a/src/components/table/Table.tsx b/src/components/table/Table.tsx new file mode 100644 index 0000000..02050c4 --- /dev/null +++ b/src/components/table/Table.tsx @@ -0,0 +1,118 @@ +import { ReactElement, useMemo, useState } from 'react' +import { Spinner } from 'src/components/animation/Spinner' +import { ChevronIcon } from 'src/components/Chevron' + +export interface TableColumn { + id: string // its key in the data + header: string + renderer?: (dataCell: any) => string | ReactElement +} + +type DataElement = { id: string } & Record + +interface Props { + columns: TableColumn[] + data: T[] + onRowClick?: (id: string) => void + initialSortBy?: string // column id + isLoading?: boolean +} + +export function Table(props: Props) { + const { columns, data, onRowClick, initialSortBy, isLoading } = props + + const [sortBy, setSortBy] = useState(initialSortBy ?? columns[0].id) + const [sortDesc, setSortDesc] = useState(true) + + const sortedData = useMemo(() => { + return sortDataBy(data, sortBy, sortDesc) + }, [data, sortBy, sortDesc]) + + const onColumnClick = (columnId: string) => { + if (columnId === sortBy) { + setSortDesc(!sortDesc) + } else { + setSortBy(columnId) + setSortDesc(true) + } + } + + return ( +
+ + + + {columns.map((column) => { + const isSelected = column.id === sortBy + return ( + + ) + })} + + + + + + + {sortedData.map((row, i) => { + return ( + onRowClick(row.id) : undefined} + key={`table-row-${i}`} + > + {columns.map((column, j) => { + return ( + + ) + })} + + ) + })} + +
onColumnClick(column.id)} + className={`font-medium text-center px-4 pb-3 border-b border-gray-400 cursor-pointer ${ + isSelected && 'pr-0' + }`} + > + <> + {column.header} + {isSelected && ( + + )} + +
+
+ {column.renderer ? column.renderer(row) : row[column.id]} +
+
+ {isLoading && ( +
+ +
+ )} +
+ ) +} + +function sortDataBy(data: T[], columnId: string, descending: boolean) { + return [...data].sort((a, b) => { + let aVal = a[columnId] + let bVal = b[columnId] + if (typeof aVal === 'string') { + aVal = aVal.toLowerCase() + bVal = bVal.toLowerCase() + } + const order = descending ? aVal > bVal : aVal <= bVal + return order ? -1 : 1 + }) +} diff --git a/src/features/granda/ProposalList.tsx b/src/features/granda/ProposalList.tsx new file mode 100644 index 0000000..fd6c78e --- /dev/null +++ b/src/features/granda/ProposalList.tsx @@ -0,0 +1,67 @@ +import { useAppDispatch } from 'src/app/hooks' +import { IconButton } from 'src/components/buttons/IconButton' +import { Table, TableColumn } from 'src/components/table/Table' +import { setShowSlippage } from 'src/features/swap/swapSlice' +import Sliders from 'src/images/icons/sliders.svg' +import { FloatingBox } from 'src/layout/FloatingBox' + +export function ProposalList() { + return ( + +
+

Granda Mento

+ +
+ columns={tableColumns} data={[]} initialSortBy="id" isLoading={false} /> +
+ ) +} + +const tableColumns: TableColumn[] = [ + { + header: '#', + id: 'id', + }, + { + header: 'Sell Amount', + id: 'amount', + renderer: (group) => `${group.amount.toFixed(2)}`, + }, + { + header: 'Token', + id: 'stableToken', + }, + { + header: 'Rate', + id: 'rate', + }, + { + header: 'Status', + id: 'status', + renderer: renderStatus, + }, +] + +function renderStatus(group: any) { + return group.status +} + +function NewButton() { + const dispatch = useAppDispatch() + const onClickCreate = () => { + // TODO + dispatch(setShowSlippage(true)) + } + + return ( +
+ +
+ ) +} diff --git a/src/images/icons/support-coin.png b/src/images/icons/support-coin.png deleted file mode 100644 index c37f815f4ece3854bdc83ae31e0c4d4f5d872a7f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13602 zcmeHuWmlU`7jB@qO9@c4xKpH1ihGK?ySr;~DDLj=4#6FYySuv;cRzXFzi_^tbw1oH zYbBYv_nz5$%{8HNGUBL6UyuL*0O~K0hynlr74zSN00;TTw~J#8@_}dvQg;LZ7$p9C zpj>}DNdo|+fL|hlN^TivnVMF{s%eX=g>h9*aZ&3{#}iW#L>O#RVfSUk~U?6vOW9g{&w!u0G6qvlklt)<2A{QZc% z<8EB}P)k>+HUl#_&&{68ZsP1|QBV#5M29H|xLx2(ru_2ha7{wmGzBDu79YPm8`!!9 zKt_U|`O3O5AQSmFR-9}x88<$lO4rp_6c*|jq2o^4)_To+dl@Cuv2A$x$_69|@!b3+6ZN2B#{iH6 zz)%P!1bN6Qa40JhLiB>XzE1S_1)voM8VesHxb@*00nyLGwy@Z(1?UhI7z(oAr|BDK zH-%_fY3TLUU&#RWe!e2UJ49_nIME3rOpF#F02vgNIak@)j}n$q3yNBa@`Y2hnN6y0 zGz@rlMJpDGXE?tDxi|ozo{%9XiL7Z}UwS9js3M9DanQIUNTh<<(x|*> zjM)~}dV2!ej1vJQNm~o{ED-gf@w?oqVH`DKtgn$}YN%(~&E>mL-8=c42|Hzg5d%~( zC_9(L3a|HS(|w&RFX@P>u_{MbO;s78znz6PqecLYLbmW3_VLbN+?k>l)W4e}<0?1o z)zS{zAQ8qa@S>Na6fU=BSiFT9+2)m&Hp@acjHN#F9Y5hx10Y9cq~8!8;BUPYt%1cx zV`-)=AukW>i_{Hmj#$RZ{3XPOK}F8Kb~nz)YfTMs1`TD7z7Y7J^RuaaWZ;j)q4ung zDx2UFPQTVjU47(TVHOJ#T^k0OpZ1wDQbS1 zrRBg(nrt>>?=?j7qJDo<{L`J_Oh@MWJq70(uZCC@73cNY==}&|&QNl%H@S<&?SKv6 z#z#4B1kjBdW5|odB&yC2*7SJ&-Z2 z_$9C>0$)TH1~muB-B?6;lrmDzU2GfPRwC>(j!Bxf|QKTGkJq|^zUB6CkHW#NzDdb=gK*A+=gWP^TZ{7|L zYuR_gsou)w2Q&JAZf3>Pl<*Swb5jO#jO#R+s`nwH7)a6J7?6(ZUn%M$QqTujN^Y(y z6WsrcYN{5nWY}SuX;_w&iOL^tT>-&qVxf&X^C>uvDt4%v#xuYnDxHK$aU#|I68x3?-u`e8L-gP;3AsDBn z2toJ^u@K+5)y$^MY=}A^Hn=h(;nzI8qciLp%!H9Z0XhygV%$;x5MO+l0;q`Nw%Til zu?4XaF;5mw&$iM!gurSU8*Y7l1>0%QclRZTIKypu)e zpjiiZ(j8>)fR)xQ;8-oV3F?-59giu31dX$5r|WsCMl~O_rcFx+=T;^wo>nx7!ULqg zD<~kbMI&Oc3h7#%O~VuET!x{vHf&Rhj>fAr;)bTSq04p>+zZ;h_xEN0bKp~7y2P+A ze7NR(xTbpdT-u^wP;t%sBCjo4ZY&f!C2r54FM0` zMs4;@CC=%Qo~?6<_JscM&h}^K+d-F-s_7{#$Tk-k_uQshE7Z5GeJ89VX;}S@Dc^3W zGMYSN2rHfAm|4FB-vZtPmkIs2UW%@i6in`cKBn5VpXea4GZDY3PfUKvVSbQCsGR&U z%1{y?M{{4YVf@j1zM|=-c!``FM0SUaIOcDvKi}MWhclt7~YEA%kaCs^*e-H^ANWR=$>! zf&Aeo){V${L|22csfiB#J*=OQu+o`A+o+Mm>|M~$7#rTtmJ&OHcVdE=)sov!o{-2< zs5gNT9c#f2{42dP=}6v^fy1hE0cPvhUJhC&gP9?KECFJe;J?C7?JiJoqw^Hn$9Ol} z%Jo%fcSTakjKg56Fi#`U&SXGG#;0+ug2$S4MXqa)px*B~Qc@xzl|RN;>8?eh zX^rNz2Ns_46LapQ#D+A$xa6r=Q)`XzC$X^A^&3Qz;o;gQaI$jAZ_VsOoF77xJ%RHq z?IDn`6(Bo;9I(SuH*}LYDV+a)f*im><(Nw)MJU@6*664bL-I?Pc0wiEqFCS6FMsAo*&^{)=Z|dNQ!6 zad*H1;)&^M7Ql_mfU3Q`-zZ(Z@os8mk<1?Ef?Wikq}c{4rRcyV^VkX^2DLtr_+m$?Z^JsgjSh z^XA-P;+mFpTWcZ$a~2G`>c-%Vh?63g{UB9AK>!WGC>DIF|2`Z%wDd^x_1Ui#IfO;A zYZwaYGz9IO2jpg^hlEU6MmVTrrhue59d_0(eR6y$q%`VV`LwZsjV*?1V&QQdVO*yh zXATo(=LTJMOF@C3VGWix!x3pEA98Et%$j2)vR%EAn4@_W4`WvQ>Tu%nu{!(+px{_~ z`f6=gZIqLVPKA!af~A2*m!)r+?GIL?7)xlHyZlsxcu0SaOm^2MVkgAY7PBzP7nL_W!}cHn zel4FAY$m*G5fIaO3g%-Vh)y41nue@^5!6(`hk|s{kPEiDs-l)^ei8H&5qV`Tf|soq zS#LQMnSpg8Se85fSx^871B=)68Jm`#n|}4z9i=n0P1)f*#p1kgv-me`OF^XOu=Wtb zG5LR{(y!gT%K&yoEz-|ZGUcP_4M$Ztc#^G2KqQHrFIU3!{p=TnQN*`!QkBT28au+V zsQy0Dozt3|gyuc6^b?bJt%qNjF-A&yH1Y%CpwtD7PWZ2JA0YYn(9=%*S~mOSVV0>( zn!i0>OZ8Tq4&UbjV&uUCQDd}{*gpx?v@6;tQT=>dwJY~*Hr#3w7m0RNmghr-?&D;X*M&qm zf^qXJ^WmDuL`yZ-VV29pTb1Lv&*^Dbn%AY0r}M^MH#K>@mXaDt|1bd{@v!rsEIzgt zCG%I}GXPX6K7WDvh;KgYW!^1yQOCs6*e&|2*OcbwYn0=;8uDL$m7Lp}Nc5A>r@Ksk zHzUms+HXa|Ea0D_6m9}IO7PGM{MM3OQWt)v8IC?0B>c(Vbxoa51QRW`#@DwUqBr81 zjvs@XY!fZIR;0~Yrd|<9D9rwY!#J7k&@mytoYxfTOUd~`0)UlAtm5y{7M7!Gb}xF3&IU+DN zK4fSG=8!?p$CRP#lovAQw%(=DVZ0@0+KUwp1fq zE4{`zxP1aSk!vHZ3JI<0)SZzo(C4J?(5$Yny{3Al_=l^%(QQpC1`2DdhUxJw2R~j~ zo5YmNkYjyep{)zE)!VNPNW4v@2re9kC9hLxNW&~|DS0$uP5=A|9c!$R)tC_h06{NL zsLEa+jdXwC@}BsWnv<{f;U~FI!6TYWHGC2xDmqz!J*1G|B~KR}xy{P*S0MG+keFIL zC^gbMRwRa+YcHRwl-rf3H_^kj;ta=+MXm?kOh^90ymBX+Ia9AH=_23(i|9ih6KYX!R5h$LOSm+RzNSxD*l zAz`rp6#x7HpW(>Ao$-R1ZFrI4(ZlsnJ9hEGf4GKzZ2K;}PLS<7;Uxo{xcgGA6gT`j zjGT4IWF;5Ue(Kr(Ea%%UaW&+i?zy75xh5+Ug|E3Pgc0*Q)%D&jtki**i)%s!@L_Op zGw5=rfs&LM&_$_#J=lESX?y$%uE_8?%^IN5{(jdFwo9vRx6@UQSW|UB zZgYPa{jXu%m*jiH#WW{D0JRFe@;D*tj(`$X%k8Aq&LJ@fN9NhueN8TSkq@E>-oAwFHod2!yyqQuUK5R^8pMG{tOkrYyV}yYlIk5&Yi_XzHD`t zr9>KiqkX%dc2*fs)^vHl<+a2_07aS&60hVsQSP`9X~HE3qo-BLn3wmQ*J)8v(i$<2 zvaZ(Q78WO6O+jPCJlCpy<;0naLOl)eF2qg3+kf2e8LGd3Jf;SFj}l@fmPo&xbu)FS zhXmBaz!Z2_a3A5(J2I*pg?g^JLknHDYhA;5v%6%5Gc1rJ2HK8$OOsey%$~pK+}*v& zsnD=n!6Sg))Y!`C!UG-wrJLq-Caev-ofYVopQd8d$Yrc!N}!ao z+1vS`XCQK=O`oqAv}V(%s3T#a@E30jMgH5Eaq4E(7V&UUACsn%X0zS?>!plk5uJ_8 zGYMB($utyu5_Zs#mi#F@lpLmjh?I0(&%XZ=-lHi3U|{RzDC-~F1bQJgrseh;cV9vR z5jI~K@|JxxLtjNJ3~R1M&sbKZXzF+x0d;tuzZ#l3XA{D$QJ1H(+nZs)6yP%;Z)dIj zwEbKwk}%0{sedW>62Qtp+`_zYu%!RIB+h?xvrCA>3w%T;Ps8<=OuG zOcUMS{pjZ58SYDnQ}THeneb_Bs}$(#%Pu}0k;iEy{RiWO^E^3`@1Lj$lrpmf^YQNf z-kzJXI@j{LQI=ipDg%@{iF?)PRp2HWsmViGMN*EbzkKt!Fn#<12-hb(o2!sSEtTNe zcT<7#)jgFr0kJIou6v-( z5n7zXm`3~#d6t&>#4|1m`FJsqmkAc131o*gwva@(nsXE+lwVyVouOdepHHHEd> zI$6ExXP{RnAR2^7*;#(Kk@yMcWEqvyG{2`SFV|5U{Jm1zS7=YYT(tsS__{9=AC(Q> zaf3V9k09_2y2qOa3Z}rlQP`5w!OeL>+Xyqe*430qg^}!fIvRV0AEioXF%!|5MD;u! z$pD%DhT%4i2M;WN1Piy3YlIOKG;BnJqfNma5W@z1%*&Kw)?IC*pmRx&6t1ZUoVX`8 z^?)13IIrd%a$t73gtfFx>Xj58rw`>crkvA^~HS0or zGkgBnw6>^w;uv!(@B0^!bwCCsnApYkY`a0)?y zp}$!P@_za^G4^nqfK4kYoE|(d=Vmh>3 z<6tY#_2G8Z+ks)^uq6D;3kr$lamyaQjKt-FDp)V$y?xk?!~M>4S?mGKhB<`qLk;2K z;eotQOVgj4@&PUcEG?`Yae^DtPdgDSfq|m-(M{ADACmDV!{y$l%0NTR?aM11eufG; z`BIa+;IXgxl+=TLgJ1smDPZQKB(vX`%h~-Z=b)^2BKQ1hbO8oY>3!9Xpm$2M0 zF=4!%=A6YG_kHE_)sZYhWAHWD9_}}B?y`#-32Fdx`6nYSO+GP)>H-c*nVqX-WEZ4P zTK%?}WJ@G>A;HCY7S^3uQ{Kmfb$WHVcV4-#$LW-We><@Hd+|Bw5J0{EHM&AOuFo6R zPUBpzZN4ce``*uBmmpC?rY7jJh#MfBgy1}36)6=Kgc z9#?3nofjm|Al}8w*H;~Qq_WHyLQnNeI8{; z*LY8C&l9!!+$eB|3neYdTTo!4i%Xa?2rPr#w1AdT@Z{^%UwZ~~VfMiYwMPM_CB#|5qy4AloSj~8%lsdM*_!WH{MSoY z{70z3B2E?I9hNyNqw=W0mZr`YZhadMgr-rnf4x>1F(5h8`sxT;OJg@;;g)c?1vocK zMu4i@L+`_yt=9$pdFdq2q4$yPJO0D%NbxL*yZb~7P(HtfIKHgE94q?8yhBFxDvEw+ zUPVlQSNl6rV48q{hD_vJRglE)U0ZX29LsT~ZB`P4RdV~y`ldaJj^|O<+w^3cY3HN= z%S82Sl*hYx*CoN^`|0LeYFv4aW`Dv@^kiyc2U;6?+FX!sx$MHyuLn%XNdb>*A*|xZ z_ZmTrj=+-jzD9@L&9YMUov_!b4Q+=aF^{haEPsxWvc0dLMwz`HGcQ_9eQseN*0&ee zFJ8mr`Cre2FFNKmH``;7E7=|?{37>eTGKRn`g9Q2oxv>8c1<}_sH6u$>B@vfVi7RO~@W547MIu4AqpVgN)`X<^hmAS>dh5T8 zUrgn}UH8rpm#SUw(}y2t?1|G|*?Tvb>~)o?Hc zCalNJqA$O`2l|jM3c2JvG)aAF`N^s4^|6-eb6H8x|9;y*aE>hNKUZe(^bLw!U;5om^54-uT%Go$zsZ~3bcZmEQYcoa z8r3M}huaBan)ksj8kdMvBRsv(n_uMiZ;7^G>m=}R`gGEV>!~V!c-D9*=UZKjN^p%h zX?TfNWlTqOaYhQCwVzQtI(RZ+YK%v$XO3LQPinVTTwk(aW!LV*4h%{|tHteq!?FGT zga+=!(&5c8z=*Clw4R2opGLA(=U;f7PY&BCzx<&_iX-R1qC_0HhJdd+8bv&iXJ~=k z1|ArOSu1Y!FH$EXz8KNyd|xZvf@yB3`Uhs~C>pUbQU4z8vQDW~!3q3?Uo5cAr~(Ql zC8bmpESk^fm{-Q>hc(%e$^*-kqz1*cFzR?p?Uj>66loCX@MLwpN4)ginGPbO-?;tL zn!~lC{`;?G5I725+e~3?KT{+{wfhtkt4xLf3N=gqEobnJTfRuKP}@{8V!mB#zg8jx z-aR~Ma^m^z!fU<5_3C|W@ror*R_mkrWvQWfEs~lqiJB(bWrea{ygFUJD0)6$tX2b< z0swp-`i~0`-|C_wS(iA!)@nP6a)DYWECCHgi+QFrdi2S;K=ibtilCzaF`%LOhf{^7 z19fH5-$TZjYM;yTHG^LDwdVVTwxXD=;*Xi=x0P#I&ns7wtoQfpmye?}yftO=b>`yu z3IXA^+gR_;`BcdPQ#4MMUyxPF6=mdaa^k~c+!}S}s2){_qf~gY`HszemFF<4Nv`{} z(eiTM@6l6z2T2BNC!0^Sha2xS& z&9}>rtR}I}x6Abw-D~Vq`txTt5=UHqR#!m-<%rLYH2JW>lpGDf-fpJ#x;DQ`KNSNr zF#e~4t0eKZg{mNZ?2KZ>jo{Z5X_BB z`Y8?1%>E81#f{Sa26v(BIdzliwdwVkeA#imt!xWM2x zKU%x;g)% z=25D)IYmJm{nLfwnkuC)tO6FS1#OTy3Drk;qqH1Sflp(jyLOTDNIsCx-N1B6D@}wB zFZfxr;h$%udDXN!$;85aCh&sN*Ed0=XsOapFbk}^qEusCrY9p4RjuqOG9OQi;4Fs8 zB_Wz$nic?|14QYckI`^(+0<)?;xr_sCNC(>526U(#!XdS3m}VQo|kkf>As)NMXytQ zk`&P9-lB;W1sC<-Qr@Aol|OJ+t1f~zZ;}?jlYF{vfJ4QtP_ObKcT|utz`JDa0P?6# z2f!+@Qr2e>A9nsJt^WI0Y)wdvzTlpv#5s=Rz|!)kSq`!d#b}drzRzJlXEVxJ(7)R^ zb$FO*Nip~1LrSEf$_h;>^x>AWs8c6Lbpy7{jqQeZArG#pyGA9~HyPMJNC^FZV0!|U zQI$7RZ0Xbf$Qm*4^QF_oveQg;IXuT9Et4sUZ~Leu63m^iws?k=9zM>^*dR?>X$qgZ`Pl z+;G~KtO3q%@6sib5)QO49bTZq=p$<-NJUnFBnUFrU14DbwWH%>jX*HDNvtUW% zWL5s82N%r1tZ`|mp}%Dq%w#KGB#l-!F0wt3u``acpIF<<(r(c94Qi=cF@zD;D?~KQ z=io46!sN>i%f4WvX;4u}?)JJtP`ZbCF;9h+s{Xy~=a><6UeJp&CJJNV)VAJx+J~aG z+ks7Kes*?!xNg#Xy3u)h{p14f?Nbvg$>cA!~7q#$XOS!HFJqv&Cv5!3y0&U!iD_ZB`+^pmmI}(`kj`P}!JVrl0 zR3T9J*R4s^hL+_?wE}PQS~@@3Kx5g#hRSFT>k$9YmUS5d0f8^1iUhUuLghvoxxl#5 zB*GvI-Q>j$?eLk6&#o!x3lF&CUxpbbxEYs?Pz-TH%}Gi7A;qdMaF(cWGYV3ISTcf@ zbY`pO40yH8wQxyAs)sA*%`HWM{W9Ty48IBpg!%c_r$nTa6-Ya4y0do=PEjtrOf{|Y za2Fytsu{{bKsXlAt~JY-$rIf5?28dD+M_Qk2JOXjhKi_?>GDm!!hQLeCv^8`3@JofgEHAQCOs$g z2+&yY(-+Nb%Sy$R&1edi6bD3m97*E4gwEfqR3MB1LN%M~kL5THIwpF&ks>13ycvWYNC#{)a549}MP!O?8^E03mai#u7dNqQ986>4wzy#l1B>{j; zJStOKz2YxUK|KhcrVNfRb=xS@=X_l%Q9@je`oe{#Bw$WC5SVj}R`cMze(%KahW|Da zg!EW_Y?{pax30`*_F<`OBRSJ?^7OmU3YTj~(M?)EW|DXdDXG1ly4|bQ{r<0Y=Q|8- z4Qi}-r>}_S-JIz8zs^9fERR-nMB>xVGDm>v;*kQF2`?NxRbNOtC)721BLXCXp&<6R zg!dF^9~r|A{gvx2QzngwNK+gEq$#H|$*u2Ws}X>(utI||IJ;~96a_l#qo%~g;ln2x z$ZxEINa3*1zrMeZ9=i~2<}%NaVH+2vf~$q_6{<1O5kNeDj0m3+hX2bQStc^|_?F7| zWX5{^tvMR!ylbkM5c3y4VsiSGs%9-4s>J~lYTmo9?WRi(4HXK*m=dpGT0DK9R}o;< zJIJLUd(70{gei!@i#2D^H*=ETP{*o7{;fP#m-sWrfZ)-A@sABR>=jfXh++u3ww}q> z5#S=5ho?YY6C${9=4)H=8aT*i0%FUNoa zxzXe$@jw5KDor&z9a0Uqw3c6KG7WVwWpbUP*;2LB6@@r6uJ-RC27~61wz(8j!#^Rs zOce-%0%U)GZ01*T-%sHU$tgYoBPk6fW5nd)|G3wk1|fhlNJ-a=ox`JlUIs~(IDVO2 z#d*LHyc$&3Z=u)})lx@p^q?TbytaDb zv)eJ-%|13eG86t8sk%UTWC}h8bPY|H_Rhn30=S37LU=rOUybuW8rWk3(Vt-`_zX5m z&)|w&ivDnDT5(54Q&q+I_5AHC#HDSn*)iOf@-n@s7%OrhGwE3hZfYi>&BH7{oeKyaVIhVh0;8^W zrN2DP*@2-6TVQ$IR<+CLLsH#p_^QoJ5e!MwpZcF(dE3VS{B;R{#lk%KiX#;tzp>W| zhWX3Q)!Mj=Y%uK>bP;-227Tq7?*ud?9~Om}DBuI5l(#QEDBt=g>cY$qOfr#W?3KdLlQ5(nRKqvNi~)%Muk zH%S1cAn@obKFpY6irB2*Fq`3NlqiArDo&V{i^=x7MiV4ELwJ)Yaq))d@&7@*{$*}& z=T575G}$7vktYm%A09BWB>9z&XE#6Qd`nn4_~X@P1_>bK&aUYd2>R8QVmJNUPXBv^ zthSQpt{h7gYkzNp9<{AKms3k~#cbjtmUkEC@0LBcA&PdnbqI_@jAd6%!VnV_)6XKe z@JpFcG;Mg!80z?z0plSyB!f1f%=Vv;$#xO+xi}bPXFU&qRv^cUB=S;vlZ%XK`D=MmrrQ*i9+F2?i13{uCjKUfss;)$R{}KP3zWi(4 zZWq^<-ed$NLLHC9il%C8ESCm4XDn8gt#-s@I@kVa`1L>T?D=xC)PQKi`jqg6t1Gv3 zlWnqFmWGKBB!y*evOtU|fTVSA?@{Bs_$(n-oVPU7dcxP^u6;k`a+ri)iNM!&a}ZB^ zwr4qN9e+Zgp*qITF7Cd2$kOb$!E zxH7Tv*dvQOBgS&<0s03IZSHx8PB-IOveWt=C7B0P%uVttmUqIi?(Equ}DEp zE@%**Xk#>I!$KCrh_D3~#znDiUm9HhZR6)+yt%dYr=<7$tnFC6$T2DcuRGM;pv-^ZJamq2AK7+7Gx2`_su;s0 z1;W=ggLL00Y~qiH$d;_lpbxxf&jQl)^kEAQ27-3o!t*!#)5rA!AD2mx<2lSf$Ha!6 z>D}LHtK@X(=H$$efco2!;~N_6pyP(iEG$$WyjseB9zLixh9TD<*f48oBJ#J9O(|%G z0GW-+S(eN71}4~n5+XpV$flwv0@@b~!>sp8_5u4Td+es;uvayNOhlox5U7^dXYjL& z1r}BpEz#roYB&b&al7x(&Qc{yH-~~sEN%rZZa;O|DdC8nVERfXEHGbTNCm*S7yP~G zz$5pXoY1A!Ce(=|auI$G5(K>AuU*EM@g*IiiI`&C7zkd}7%f!fOJ3ssm1A8_=4<&e zFMe|!sOEwSS`gf|jMDy!Ml5%^ zGh^^b9yWBIxsN5PN@O&9u%Wyu3gH(xK}wY9wD7iDgEBaGgy+;RHFT`39&|i}^?E~^ z$0CfRsGOWUas1AyGT>yvjTm@l`H5FDcon@_EK=}I-;#&+tOhNWh?^0rVd zkhT_{xMOi66HhVzEd{n>VB*WGzlE^fiH(MnXU=#vy@90e9s0b#G7PF2k}hPblZE%* zgh3m@^hO$iRfN=T6Al@I9RdkW&d23F(9rdMPP^=WcVh3js4J%9DF~#Py>gPa={8>g zLAYtj=DaS@cbMGCln!(cgS1I3^_eNKMjI_i!i;voeH>-NLSWuP4(iypmfB(8@sF8_-#IGx?dSj^vJ(|5bGj1NuM4d6JEqqS*qbHUY{sUH z%x*s4d?)HI*`{+n_jGuQFR^k~|94d+ukKSJ0Fa~c|L>9fzrF$jKPFl}(5D8w3ZXv? SJ0Mqz0KY_KM5=`J{QnPOoE-iD diff --git a/src/pages/granda.tsx b/src/pages/granda.tsx index a943b8f..2e6b141 100644 --- a/src/pages/granda.tsx +++ b/src/pages/granda.tsx @@ -1,23 +1,9 @@ -import Image from 'next/image' -import { TextLink } from 'src/components/buttons/TextLink' -import { config } from 'src/config/config' -import Support from 'src/images/icons/support-coin.png' -import { FloatingBox } from 'src/layout/FloatingBox' +import { ProposalList } from 'src/features/granda/ProposalList' export default function GrandaPage() { return (
- -

Granda Mento Coming Soon!

-

- Suport for Granda Mento is in progress! Check back here in a few weeks or join the{' '} - - Discord - {' '} - for updates. -

- Building -
+
) } From 8f34b72514536eab379ed14fca35e081cb2fd59b Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Sat, 30 Oct 2021 19:11:29 +0200 Subject: [PATCH 02/30] Set up subpages for granda flows --- src/app/store.ts | 4 ++- src/components/buttons/BackButton.tsx | 6 ++++ src/components/buttons/IconButton.tsx | 4 +-- src/components/buttons/RefreshButton.tsx | 6 ++++ src/features/granda/ProposalConfirm.tsx | 24 +++++++++++++++ src/features/granda/ProposalForm.tsx | 24 +++++++++++++++ src/features/granda/ProposalList.tsx | 23 ++++++++++---- src/features/granda/ProposalView.tsx | 38 +++++++++++++++++++++++ src/features/granda/grandaSlice.ts | 39 ++++++++++++++++++++++++ src/features/granda/types.ts | 14 +++++++++ src/features/swap/SwapConfirm.tsx | 21 +++---------- src/images/icons/plus-circle-fill.svg | 3 ++ src/pages/granda.tsx | 23 +++++++++++++- 13 files changed, 202 insertions(+), 27 deletions(-) create mode 100644 src/components/buttons/BackButton.tsx create mode 100644 src/components/buttons/RefreshButton.tsx create mode 100644 src/features/granda/ProposalConfirm.tsx create mode 100644 src/features/granda/ProposalForm.tsx create mode 100644 src/features/granda/ProposalView.tsx create mode 100644 src/features/granda/grandaSlice.ts create mode 100644 src/features/granda/types.ts create mode 100644 src/images/icons/plus-circle-fill.svg diff --git a/src/app/store.ts b/src/app/store.ts index 9df3581..8033bd2 100644 --- a/src/app/store.ts +++ b/src/app/store.ts @@ -3,15 +3,17 @@ import { config } from 'src/config/config' import { accountReducer } from 'src/features/accounts/accountSlice' import { blockReducer } from 'src/features/blocks/blockSlice' import { tokenPriceReducer } from 'src/features/chart/tokenPriceSlice' +import { grandaReducer } from 'src/features/granda/grandaSlice' import { swapReducer } from 'src/features/swap/swapSlice' export function createStore() { return configureStore({ reducer: { account: accountReducer, + block: blockReducer, + granda: grandaReducer, swap: swapReducer, tokenPrice: tokenPriceReducer, - block: blockReducer, }, devTools: config.debug, }) diff --git a/src/components/buttons/BackButton.tsx b/src/components/buttons/BackButton.tsx new file mode 100644 index 0000000..aa17b8f --- /dev/null +++ b/src/components/buttons/BackButton.tsx @@ -0,0 +1,6 @@ +import { IconButton, IconButtonProps } from 'src/components/buttons/IconButton' +import LeftArrow from 'src/images/icons/arrow-left-circle.svg' + +export function BackButton(props: IconButtonProps) { + return +} diff --git a/src/components/buttons/IconButton.tsx b/src/components/buttons/IconButton.tsx index df71ac7..26dd84e 100644 --- a/src/components/buttons/IconButton.tsx +++ b/src/components/buttons/IconButton.tsx @@ -1,7 +1,7 @@ import Image from 'next/image' import { PropsWithChildren } from 'react' -interface ButtonProps { +export interface IconButtonProps { width: number height: number classes?: string @@ -12,7 +12,7 @@ interface ButtonProps { passThruProps?: any } -export function IconButton(props: PropsWithChildren) { +export function IconButton(props: PropsWithChildren) { const { width, height, classes, onClick, imgSrc, disabled, title, passThruProps } = props const base = 'flex items-center justify-center transition-all' diff --git a/src/components/buttons/RefreshButton.tsx b/src/components/buttons/RefreshButton.tsx new file mode 100644 index 0000000..110f558 --- /dev/null +++ b/src/components/buttons/RefreshButton.tsx @@ -0,0 +1,6 @@ +import { IconButton, IconButtonProps } from 'src/components/buttons/IconButton' +import RepeatArrow from 'src/images/icons/arrow-repeat.svg' + +export function RefreshButton(props: IconButtonProps) { + return +} diff --git a/src/features/granda/ProposalConfirm.tsx b/src/features/granda/ProposalConfirm.tsx new file mode 100644 index 0000000..80f5c82 --- /dev/null +++ b/src/features/granda/ProposalConfirm.tsx @@ -0,0 +1,24 @@ +import { useAppDispatch } from 'src/app/hooks' +import { BackButton } from 'src/components/buttons/BackButton' +import { setSubpage } from 'src/features/granda/grandaSlice' +import { GrandaSubpage } from 'src/features/granda/types' +import { FloatingBox } from 'src/layout/FloatingBox' + +export function ProposalConfirm() { + const dispatch = useAppDispatch() + + const onClickBack = () => { + dispatch(setSubpage(GrandaSubpage.List)) + } + + return ( + +
+ +

Confirm Proposal

+
+
+
TODO show confirmation
+
+ ) +} diff --git a/src/features/granda/ProposalForm.tsx b/src/features/granda/ProposalForm.tsx new file mode 100644 index 0000000..ed2e9c3 --- /dev/null +++ b/src/features/granda/ProposalForm.tsx @@ -0,0 +1,24 @@ +import { useAppDispatch } from 'src/app/hooks' +import { BackButton } from 'src/components/buttons/BackButton' +import { setSubpage } from 'src/features/granda/grandaSlice' +import { GrandaSubpage } from 'src/features/granda/types' +import { FloatingBox } from 'src/layout/FloatingBox' + +export function ProposalForm() { + const dispatch = useAppDispatch() + + const onClickBack = () => { + dispatch(setSubpage(GrandaSubpage.List)) + } + + return ( + +
+ +

Create Proposal

+
+
+
TODO show form
+
+ ) +} diff --git a/src/features/granda/ProposalList.tsx b/src/features/granda/ProposalList.tsx index fd6c78e..575b801 100644 --- a/src/features/granda/ProposalList.tsx +++ b/src/features/granda/ProposalList.tsx @@ -1,18 +1,30 @@ import { useAppDispatch } from 'src/app/hooks' import { IconButton } from 'src/components/buttons/IconButton' import { Table, TableColumn } from 'src/components/table/Table' -import { setShowSlippage } from 'src/features/swap/swapSlice' -import Sliders from 'src/images/icons/sliders.svg' +import { setSubpage, viewProposal } from 'src/features/granda/grandaSlice' +import { GrandaSubpage } from 'src/features/granda/types' +import PlusCircle from 'src/images/icons/plus-circle-fill.svg' import { FloatingBox } from 'src/layout/FloatingBox' export function ProposalList() { + const dispatch = useAppDispatch() + const onRowClick = (id: string) => { + dispatch(viewProposal(id)) + } + return (

Granda Mento

- columns={tableColumns} data={[]} initialSortBy="id" isLoading={false} /> + + columns={tableColumns} + data={[]} + initialSortBy="id" + isLoading={false} + onRowClick={onRowClick} + />
) } @@ -49,14 +61,13 @@ function renderStatus(group: any) { function NewButton() { const dispatch = useAppDispatch() const onClickCreate = () => { - // TODO - dispatch(setShowSlippage(true)) + dispatch(setSubpage(GrandaSubpage.Form)) } return (
s.granda.viewProposalId) + const dispatch = useAppDispatch() + + useEffect(() => { + // TODO check if proposal id is in data + if (!proposalId) dispatch(clearProposal()) + }, [proposalId, dispatch]) + + const onClickBack = () => { + dispatch(clearProposal()) + } + + const { kit, initialised } = useContractKit() + const onClickRefresh = () => { + if (!kit || !initialised) return + console.log('TODO') + } + + return ( + +
+ +

{`Proposal ${proposalId}`}

+ +
+
TODO show proposal
+
+ ) +} diff --git a/src/features/granda/grandaSlice.ts b/src/features/granda/grandaSlice.ts new file mode 100644 index 0000000..4ca9f46 --- /dev/null +++ b/src/features/granda/grandaSlice.ts @@ -0,0 +1,39 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit' +import { GrandaFormValues, GrandaSubpage } from 'src/features/granda/types' + +export interface GrandaState { + subpage: GrandaSubpage + formValues: GrandaFormValues | null + viewProposalId: string | null +} + +const initialState: GrandaState = { + subpage: GrandaSubpage.List, + formValues: null, + viewProposalId: null, +} + +export const grandaSlice = createSlice({ + name: 'granda', + initialState, + reducers: { + setSubpage: (state, action: PayloadAction) => { + state.subpage = action.payload + }, + setFormValues: (state, action: PayloadAction) => { + state.formValues = action.payload + }, + viewProposal: (state, action: PayloadAction) => { + state.viewProposalId = action.payload + state.subpage = GrandaSubpage.View + }, + clearProposal: (state) => { + state.viewProposalId = null + state.subpage = GrandaSubpage.List + }, + reset: () => initialState, + }, +}) + +export const { setFormValues, setSubpage, viewProposal, clearProposal, reset } = grandaSlice.actions +export const grandaReducer = grandaSlice.reducer diff --git a/src/features/granda/types.ts b/src/features/granda/types.ts new file mode 100644 index 0000000..2d29011 --- /dev/null +++ b/src/features/granda/types.ts @@ -0,0 +1,14 @@ +import { NativeTokenId } from 'src/config/tokens' + +export enum GrandaSubpage { + List = 'list', + View = 'view', + Form = 'form', + Confirm = 'confirm', +} + +export interface GrandaFormValues { + fromTokenId: NativeTokenId + toTokenId: NativeTokenId + fromAmount: number | string +} diff --git a/src/features/swap/SwapConfirm.tsx b/src/features/swap/SwapConfirm.tsx index 9289cde..b712ec7 100644 --- a/src/features/swap/SwapConfirm.tsx +++ b/src/features/swap/SwapConfirm.tsx @@ -4,7 +4,8 @@ import BigNumber from 'bignumber.js' import { useEffect } from 'react' import { toast } from 'react-toastify' import { useAppDispatch, useAppSelector } from 'src/app/hooks' -import { IconButton } from 'src/components/buttons/IconButton' +import { BackButton } from 'src/components/buttons/BackButton' +import { RefreshButton } from 'src/components/buttons/RefreshButton' import { SolidButton } from 'src/components/buttons/SolidButton' import { TextLink } from 'src/components/buttons/TextLink' import { config } from 'src/config/config' @@ -21,8 +22,6 @@ import { fetchExchangeRates } from 'src/features/swap/fetchExchangeRates' import { setFormValues } from 'src/features/swap/swapSlice' import { SwapFormValues } from 'src/features/swap/types' import { getMinBuyAmount, useExchangeValues } from 'src/features/swap/utils' -import LeftArrow from 'src/images/icons/arrow-left-circle.svg' -import RepeatArrow from 'src/images/icons/arrow-repeat.svg' import { TokenIcon } from 'src/images/tokens/TokenIcon' import { FloatingBox } from 'src/layout/FloatingBox' import { Color } from 'src/styles/Color' @@ -137,21 +136,9 @@ export function SwapConfirm(props: Props) { return (
- +

Confirm Swap

- +
diff --git a/src/images/icons/plus-circle-fill.svg b/src/images/icons/plus-circle-fill.svg new file mode 100644 index 0000000..7934781 --- /dev/null +++ b/src/images/icons/plus-circle-fill.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/pages/granda.tsx b/src/pages/granda.tsx index 2e6b141..d969010 100644 --- a/src/pages/granda.tsx +++ b/src/pages/granda.tsx @@ -1,9 +1,30 @@ +import { useEffect } from 'react' +import { useAppDispatch, useAppSelector } from 'src/app/hooks' +import { setSubpage } from 'src/features/granda/grandaSlice' +import { ProposalConfirm } from 'src/features/granda/ProposalConfirm' +import { ProposalForm } from 'src/features/granda/ProposalForm' import { ProposalList } from 'src/features/granda/ProposalList' +import { ProposalView } from 'src/features/granda/ProposalView' +import { GrandaSubpage } from 'src/features/granda/types' export default function GrandaPage() { + const subpage = useAppSelector((s) => s.granda.subpage) + const dispatch = useAppDispatch() + + useEffect(() => { + // Restore subpage to list if users leaves granda + return () => { + dispatch(setSubpage(GrandaSubpage.List)) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + return (
- + {subpage === GrandaSubpage.List && } + {subpage === GrandaSubpage.View && } + {subpage === GrandaSubpage.Form && } + {subpage === GrandaSubpage.Confirm && }
) } From 77861c0be6f3b4741f18dea89ab471a2257898d8 Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Sat, 30 Oct 2021 20:19:23 +0200 Subject: [PATCH 03/30] Create proposal state and fetcher (not finished) --- src/components/table/Table.tsx | 11 +++--- src/config/consts.ts | 1 + src/features/granda/ProposalList.tsx | 2 ++ src/features/granda/fetchProposals.ts | 46 ++++++++++++++++++++++++++ src/features/granda/grandaSlice.ts | 19 +++++++++-- src/features/granda/types.ts | 19 +++++++++++ src/features/polling/PollingWorker.tsx | 5 +++ src/images/icons/plus-circle-fill.svg | 2 +- 8 files changed, 95 insertions(+), 10 deletions(-) create mode 100644 src/features/granda/fetchProposals.ts diff --git a/src/components/table/Table.tsx b/src/components/table/Table.tsx index 02050c4..0331f99 100644 --- a/src/components/table/Table.tsx +++ b/src/components/table/Table.tsx @@ -48,21 +48,20 @@ export function Table(props: Props) { onColumnClick(column.id)} - className={`font-medium text-center px-4 pb-3 border-b border-gray-400 cursor-pointer ${ - isSelected && 'pr-0' - }`} + className={`font-medium text-center px-2 pb-2 border-b + border-gray-400 cursor-pointer first:pl-0 last:pr-0`} > - <> + {column.header} {isSelected && ( )} - + ) })} diff --git a/src/config/consts.ts b/src/config/consts.ts index 556340d..21bd49a 100644 --- a/src/config/consts.ts +++ b/src/config/consts.ts @@ -3,6 +3,7 @@ export const NULL_ADDRESS = '0x0000000000000000000000000000000000000000' export const AVG_BLOCK_TIMES = 5000 // 5 seconds export const STALE_BLOCK_TIME = 25000 // 25 seconds export const EXCHANGE_RATE_STALE_TIME = 5000 // 5 second +export const GRANDA_PROPOSAL_STALE_TIME = 30000 // 30 second export const BALANCE_STALE_TIME = 5000 // 5 seconds export const STATUS_POLLER_DELAY = 10000 // 10 seconds export const SIGN_OPERATION_TIMEOUT = 90000 // 90 seconds diff --git a/src/features/granda/ProposalList.tsx b/src/features/granda/ProposalList.tsx index 575b801..9808175 100644 --- a/src/features/granda/ProposalList.tsx +++ b/src/features/granda/ProposalList.tsx @@ -7,6 +7,8 @@ import PlusCircle from 'src/images/icons/plus-circle-fill.svg' import { FloatingBox } from 'src/layout/FloatingBox' export function ProposalList() { + const { address, kit, initialised } = useContractKit() + const dispatch = useAppDispatch() const onRowClick = (id: string) => { dispatch(viewProposal(id)) diff --git a/src/features/granda/fetchProposals.ts b/src/features/granda/fetchProposals.ts new file mode 100644 index 0000000..fe396e5 --- /dev/null +++ b/src/features/granda/fetchProposals.ts @@ -0,0 +1,46 @@ +import type { ContractKit } from '@celo/contractkit' +import { createAsyncThunk } from '@reduxjs/toolkit' +import type { AppDispatch, AppState } from 'src/app/store' +import { GRANDA_PROPOSAL_STALE_TIME } from 'src/config/consts' +import { NativeTokenId } from 'src/config/tokens' +import { GrandaProposal } from 'src/features/granda/types' +import { isStale } from 'src/utils/time' + +interface FetchProposalsParams { + kit: ContractKit +} + +export type AccountBalances = Record + +export const fetchProposals = createAsyncThunk< + Record | null, + FetchProposalsParams, + { dispatch: AppDispatch; state: AppState } +>('granda/fetchProposals', async (params, thunkAPI) => { + const { kit } = params + const { proposalsLastUpdated } = thunkAPI.getState().granda + if (isStale(proposalsLastUpdated, GRANDA_PROPOSAL_STALE_TIME)) { + return _fetchProposals(kit) + } else { + return null + } +}) + +async function _fetchProposals(kit: ContractKit): Promise> { + console.log('===fetching proposals') + const contract = await kit.contracts.getGrandaMento() + const propCount = await contract.exchangeProposalCount() + console.log('propCount', propCount.toNumber()) + const prop0 = await contract.getExchangeProposal(0) + console.log(prop0) + const propn = await contract.getExchangeProposal(propCount.toNumber() - 1) + console.log(propn) + const propn1 = await contract.getExchangeProposal(propCount.toNumber()) + console.log(propn1) + // @ts-ignore + // const proposals = await rawContract.methods.exchangeProposals() + // console.log('props', proposals) + // const proposal0 = await rawContract.methods.exchangeProposals(0) + // console.log('prop0', proposal0) + return {} +} diff --git a/src/features/granda/grandaSlice.ts b/src/features/granda/grandaSlice.ts index 4ca9f46..4a0ed76 100644 --- a/src/features/granda/grandaSlice.ts +++ b/src/features/granda/grandaSlice.ts @@ -1,16 +1,21 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' -import { GrandaFormValues, GrandaSubpage } from 'src/features/granda/types' +import { fetchProposals } from 'src/features/granda/fetchProposals' +import { GrandaFormValues, GrandaProposal, GrandaSubpage } from 'src/features/granda/types' export interface GrandaState { subpage: GrandaSubpage - formValues: GrandaFormValues | null + proposals: Record + proposalsLastUpdated: number | null viewProposalId: string | null + formValues: GrandaFormValues | null } const initialState: GrandaState = { subpage: GrandaSubpage.List, - formValues: null, + proposals: {}, + proposalsLastUpdated: null, viewProposalId: null, + formValues: null, } export const grandaSlice = createSlice({ @@ -33,6 +38,14 @@ export const grandaSlice = createSlice({ }, reset: () => initialState, }, + extraReducers: (builder) => { + builder.addCase(fetchProposals.fulfilled, (state, action) => { + const proposals = action.payload + if (!proposals) return + state.proposals = proposals + state.proposalsLastUpdated = Date.now() + }) + }, }) export const { setFormValues, setSubpage, viewProposal, clearProposal, reset } = grandaSlice.actions diff --git a/src/features/granda/types.ts b/src/features/granda/types.ts index 2d29011..3a2042d 100644 --- a/src/features/granda/types.ts +++ b/src/features/granda/types.ts @@ -12,3 +12,22 @@ export interface GrandaFormValues { toTokenId: NativeTokenId fromAmount: number | string } + +export enum GrandaProposalState { + Proposed = 'proposed', + Approved = 'approved', + Executed = 'executed', + Cancelled = 'cancelled', +} + +export interface GrandaProposal { + id: string + state: GrandaProposalState + exchanger: string + stableToken: string + sellAmount: string + buyAmount: string + sellCelo: boolean + vetoPeriodSeconds: string + approvalTimestamp: string +} diff --git a/src/features/polling/PollingWorker.tsx b/src/features/polling/PollingWorker.tsx index 531d6ac..1d4520e 100644 --- a/src/features/polling/PollingWorker.tsx +++ b/src/features/polling/PollingWorker.tsx @@ -5,6 +5,7 @@ import { useAppDispatch } from 'src/app/hooks' import { STATUS_POLLER_DELAY } from 'src/config/consts' import { fetchBalances } from 'src/features/accounts/fetchBalances' import { fetchLatestBlock } from 'src/features/blocks/fetchLatestBlock' +import { fetchProposals } from 'src/features/granda/fetchProposals' import { fetchExchangeRates } from 'src/features/swap/fetchExchangeRates' import { logger } from 'src/utils/logger' import { useInterval } from 'src/utils/timeout' @@ -23,6 +24,10 @@ export function PollingWorker() { toast.warn('Error retrieving latest block') logger.error('Failed to retrieve latest block', err) }) + dispatch(fetchProposals({ kit })).catch((err) => { + toast.warn('Error retrieving Granda proposals') + logger.error('Failed to granda proposals', err) + }) if (address) { dispatch(fetchBalances({ address, kit })).catch((err) => { toast.error('Error retrieving account balances') diff --git a/src/images/icons/plus-circle-fill.svg b/src/images/icons/plus-circle-fill.svg index 7934781..4908445 100644 --- a/src/images/icons/plus-circle-fill.svg +++ b/src/images/icons/plus-circle-fill.svg @@ -1,3 +1,3 @@ - + \ No newline at end of file From ed728cf822a1b633238507bb00d26a6321757296 Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Sun, 31 Oct 2021 16:07:45 +0100 Subject: [PATCH 04/30] Finish fetchProposal implementation --- src/features/granda/fetchProposals.ts | 53 +++++++++++++++++++-------- src/features/granda/types.ts | 12 +++--- 2 files changed, 43 insertions(+), 22 deletions(-) diff --git a/src/features/granda/fetchProposals.ts b/src/features/granda/fetchProposals.ts index fe396e5..95231f6 100644 --- a/src/features/granda/fetchProposals.ts +++ b/src/features/granda/fetchProposals.ts @@ -1,9 +1,10 @@ import type { ContractKit } from '@celo/contractkit' +import { ExchangeProposalState } from '@celo/contractkit/lib/wrappers/GrandaMento' import { createAsyncThunk } from '@reduxjs/toolkit' import type { AppDispatch, AppState } from 'src/app/store' import { GRANDA_PROPOSAL_STALE_TIME } from 'src/config/consts' import { NativeTokenId } from 'src/config/tokens' -import { GrandaProposal } from 'src/features/granda/types' +import { GrandaProposal, GrandaProposalState } from 'src/features/granda/types' import { isStale } from 'src/utils/time' interface FetchProposalsParams { @@ -27,20 +28,40 @@ export const fetchProposals = createAsyncThunk< }) async function _fetchProposals(kit: ContractKit): Promise> { - console.log('===fetching proposals') const contract = await kit.contracts.getGrandaMento() - const propCount = await contract.exchangeProposalCount() - console.log('propCount', propCount.toNumber()) - const prop0 = await contract.getExchangeProposal(0) - console.log(prop0) - const propn = await contract.getExchangeProposal(propCount.toNumber() - 1) - console.log(propn) - const propn1 = await contract.getExchangeProposal(propCount.toNumber()) - console.log(propn1) - // @ts-ignore - // const proposals = await rawContract.methods.exchangeProposals() - // console.log('props', proposals) - // const proposal0 = await rawContract.methods.exchangeProposals(0) - // console.log('prop0', proposal0) - return {} + const propCountBN = await contract.exchangeProposalCount() + const propCount = propCountBN.toNumber() + const proposals: Record = {} + for (let i = 0; i < propCount; i++) { + const proposal = await contract.getExchangeProposal(i) + const id = proposal.id.toString() + // TODO validate proposal + proposals[id] = { + id, + state: toGrandaProposalState(proposal.state), + exchanger: proposal.exchanger, + stableToken: proposal.stableToken, + sellAmount: proposal.sellAmount.toString(), + buyAmount: proposal.buyAmount.toString(), + sellCelo: proposal.sellCelo, + vetoPeriodSeconds: proposal.vetoPeriodSeconds.toNumber(), + approvalTimestamp: proposal.approvalTimestamp.toNumber(), + } + } + return proposals +} + +function toGrandaProposalState(state: ExchangeProposalState) { + switch (state) { + case ExchangeProposalState.Proposed: + return GrandaProposalState.Proposed + case ExchangeProposalState.Approved: + return GrandaProposalState.Approved + case ExchangeProposalState.Executed: + return GrandaProposalState.Executed + case ExchangeProposalState.Cancelled: + return GrandaProposalState.Cancelled + default: + throw new Error(`Invalid proposal state: ${state}`) + } } diff --git a/src/features/granda/types.ts b/src/features/granda/types.ts index 3a2042d..a58ca6f 100644 --- a/src/features/granda/types.ts +++ b/src/features/granda/types.ts @@ -14,10 +14,10 @@ export interface GrandaFormValues { } export enum GrandaProposalState { - Proposed = 'proposed', - Approved = 'approved', - Executed = 'executed', - Cancelled = 'cancelled', + Proposed = 'Proposed', + Approved = 'Approved', + Executed = 'Executed', + Cancelled = 'Cancelled', } export interface GrandaProposal { @@ -28,6 +28,6 @@ export interface GrandaProposal { sellAmount: string buyAmount: string sellCelo: boolean - vetoPeriodSeconds: string - approvalTimestamp: string + vetoPeriodSeconds: number + approvalTimestamp: number } From 57b28ceccd8faee11550201b61744f33f368165e Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Sun, 31 Oct 2021 17:15:01 +0100 Subject: [PATCH 05/30] Fetch size limits for granda exchanges Remove dead code from config and tokens --- src/config/config.ts | 18 ------------- src/config/tokens.ts | 15 ----------- src/features/granda/ProposalList.tsx | 2 -- src/features/granda/fetchProposals.ts | 3 --- src/features/granda/fetchSizeLimits.ts | 37 ++++++++++++++++++++++++++ src/features/granda/grandaSlice.ts | 35 +++++++++++++++++++++--- src/features/granda/types.ts | 2 ++ src/features/polling/PollingWorker.tsx | 20 +++++++++----- src/features/swap/contracts.ts | 9 ++++++- src/pages/granda.tsx | 4 ++- 10 files changed, 96 insertions(+), 49 deletions(-) create mode 100644 src/features/granda/fetchSizeLimits.ts diff --git a/src/config/config.ts b/src/config/config.ts index 96fc88b..6dcbcc2 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -1,12 +1,3 @@ -export enum CeloContract { - Exchange = 'Exchange', - ExchangeEUR = 'ExchangeEUR', - GoldToken = 'GoldToken', - SortedOracles = 'SortedOracles', - StableToken = 'StableToken', - StableTokenEUR = 'StableTokenEUR', -} - interface Config { debug: boolean version: string | null @@ -14,7 +5,6 @@ interface Config { blockscoutUrl?: string discordUrl: string chainId: number - contractAddresses: Record showPriceChart: boolean } @@ -28,14 +18,6 @@ const configMainnet: Config = { blockscoutUrl: 'https://explorer.celo.org', discordUrl: 'https://discord.gg/E9AqUQnWQE', chainId: 42220, - contractAddresses: { - [CeloContract.Exchange]: '0x67316300f17f063085Ca8bCa4bd3f7a5a3C66275', - [CeloContract.ExchangeEUR]: '0xE383394B913d7302c49F794C7d3243c429d53D1d', - [CeloContract.GoldToken]: '0x471EcE3750Da237f93B8E339c536989b8978a438', - [CeloContract.SortedOracles]: '0xefB84935239dAcdecF7c5bA76d8dE40b077B7b33', - [CeloContract.StableToken]: '0x765DE816845861e75A25fCA122bb6898B8B1282a', - [CeloContract.StableTokenEUR]: '0xD8763CBa276a3738E6DE85b4b3bF5FDed6D6cA73', - }, showPriceChart: false, } diff --git a/src/config/tokens.ts b/src/config/tokens.ts index 8e2ef04..9b6e092 100644 --- a/src/config/tokens.ts +++ b/src/config/tokens.ts @@ -1,5 +1,4 @@ import { config } from 'src/config/config' -import { NULL_ADDRESS } from 'src/config/consts' import { Color } from 'src/styles/Color' export interface Token { @@ -7,7 +6,6 @@ export interface Token { symbol: string // usually the same as id name: string color: string - address: string // contract address decimals: number chainId: number } @@ -36,7 +34,6 @@ export const NativeTokens: INativeTokens = { symbol: NativeTokenId.CELO, name: 'Celo Native', color: Color.celoGold, - address: config.contractAddresses.GoldToken, decimals: 18, chainId: config.chainId, }, @@ -45,7 +42,6 @@ export const NativeTokens: INativeTokens = { symbol: NativeTokenId.cUSD, name: 'Celo Dollar', color: Color.celoGreen, - address: config.contractAddresses.StableToken, decimals: 18, chainId: config.chainId, }, @@ -54,7 +50,6 @@ export const NativeTokens: INativeTokens = { symbol: NativeTokenId.cEUR, name: 'Celo Euro', color: Color.celoGreen, - address: config.contractAddresses.StableTokenEUR, decimals: 18, chainId: config.chainId, }, @@ -67,16 +62,6 @@ export const CELO = NativeTokens.CELO export const cUSD = NativeTokens.cUSD export const cEUR = NativeTokens.cEUR -export const UnknownToken: Token = { - id: 'unknown', - symbol: 'unknown', - name: 'Unknown Token', - color: '#818181', - address: NULL_ADDRESS, - decimals: 18, - chainId: config.chainId, -} - export function isNativeToken(tokenId: string) { return Object.keys(NativeTokens).includes(tokenId) } diff --git a/src/features/granda/ProposalList.tsx b/src/features/granda/ProposalList.tsx index 9808175..575b801 100644 --- a/src/features/granda/ProposalList.tsx +++ b/src/features/granda/ProposalList.tsx @@ -7,8 +7,6 @@ import PlusCircle from 'src/images/icons/plus-circle-fill.svg' import { FloatingBox } from 'src/layout/FloatingBox' export function ProposalList() { - const { address, kit, initialised } = useContractKit() - const dispatch = useAppDispatch() const onRowClick = (id: string) => { dispatch(viewProposal(id)) diff --git a/src/features/granda/fetchProposals.ts b/src/features/granda/fetchProposals.ts index 95231f6..3b19bc0 100644 --- a/src/features/granda/fetchProposals.ts +++ b/src/features/granda/fetchProposals.ts @@ -3,7 +3,6 @@ import { ExchangeProposalState } from '@celo/contractkit/lib/wrappers/GrandaMent import { createAsyncThunk } from '@reduxjs/toolkit' import type { AppDispatch, AppState } from 'src/app/store' import { GRANDA_PROPOSAL_STALE_TIME } from 'src/config/consts' -import { NativeTokenId } from 'src/config/tokens' import { GrandaProposal, GrandaProposalState } from 'src/features/granda/types' import { isStale } from 'src/utils/time' @@ -11,8 +10,6 @@ interface FetchProposalsParams { kit: ContractKit } -export type AccountBalances = Record - export const fetchProposals = createAsyncThunk< Record | null, FetchProposalsParams, diff --git a/src/features/granda/fetchSizeLimits.ts b/src/features/granda/fetchSizeLimits.ts new file mode 100644 index 0000000..28a8441 --- /dev/null +++ b/src/features/granda/fetchSizeLimits.ts @@ -0,0 +1,37 @@ +import type { ContractKit } from '@celo/contractkit' +import { createAsyncThunk } from '@reduxjs/toolkit' +import type { AppDispatch, AppState } from 'src/app/store' +import { SizeLimits } from 'src/features/granda/types' +import { getNativeTokenId } from 'src/features/swap/contracts' + +interface FetchSizeLimitsParams { + kit: ContractKit +} + +export const fetchSizeLimits = createAsyncThunk< + SizeLimits | null, + FetchSizeLimitsParams, + { dispatch: AppDispatch; state: AppState } +>('granda/fetchSizeLimits', async (params, thunkAPI) => { + const { kit } = params + const sizeLimits = thunkAPI.getState().granda.sizeLimits + if (!sizeLimits) { + return _fetchSizeLimits(kit) + } else { + return null + } +}) + +async function _fetchSizeLimits(kit: ContractKit): Promise { + const contract = await kit.contracts.getGrandaMento() + const configMap = await contract.getAllStableTokenLimits() + const sizeLimits: SizeLimits = {} + for (const [name, limits] of configMap.entries()) { + const tokenId = getNativeTokenId(name) + sizeLimits[tokenId] = { + min: limits.minExchangeAmount.toFixed(0), + max: limits.maxExchangeAmount.toFixed(0), + } + } + return sizeLimits +} diff --git a/src/features/granda/grandaSlice.ts b/src/features/granda/grandaSlice.ts index 4a0ed76..35a7d97 100644 --- a/src/features/granda/grandaSlice.ts +++ b/src/features/granda/grandaSlice.ts @@ -1,27 +1,40 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' import { fetchProposals } from 'src/features/granda/fetchProposals' -import { GrandaFormValues, GrandaProposal, GrandaSubpage } from 'src/features/granda/types' +import { fetchSizeLimits } from 'src/features/granda/fetchSizeLimits' +import { + GrandaFormValues, + GrandaProposal, + GrandaSubpage, + SizeLimits, +} from 'src/features/granda/types' export interface GrandaState { + isActive: boolean // tracks if granda page was loaded to lazily fetch granda info subpage: GrandaSubpage proposals: Record proposalsLastUpdated: number | null - viewProposalId: string | null + viewProposalId: string | null // id of proposal for details view subpage formValues: GrandaFormValues | null + sizeLimits: SizeLimits | null } const initialState: GrandaState = { + isActive: false, subpage: GrandaSubpage.List, proposals: {}, proposalsLastUpdated: null, viewProposalId: null, formValues: null, + sizeLimits: null, } export const grandaSlice = createSlice({ name: 'granda', initialState, reducers: { + activateGranda: (state) => { + state.isActive = true + }, setSubpage: (state, action: PayloadAction) => { state.subpage = action.payload }, @@ -36,6 +49,9 @@ export const grandaSlice = createSlice({ state.viewProposalId = null state.subpage = GrandaSubpage.List }, + setSizeLimits: (state, action: PayloadAction) => { + state.sizeLimits = action.payload + }, reset: () => initialState, }, extraReducers: (builder) => { @@ -45,8 +61,21 @@ export const grandaSlice = createSlice({ state.proposals = proposals state.proposalsLastUpdated = Date.now() }) + builder.addCase(fetchSizeLimits.fulfilled, (state, action) => { + const limits = action.payload + if (!limits) return + state.sizeLimits = limits + }) }, }) -export const { setFormValues, setSubpage, viewProposal, clearProposal, reset } = grandaSlice.actions +export const { + activateGranda, + setFormValues, + setSubpage, + viewProposal, + clearProposal, + setSizeLimits, + reset, +} = grandaSlice.actions export const grandaReducer = grandaSlice.reducer diff --git a/src/features/granda/types.ts b/src/features/granda/types.ts index a58ca6f..d05cfb2 100644 --- a/src/features/granda/types.ts +++ b/src/features/granda/types.ts @@ -31,3 +31,5 @@ export interface GrandaProposal { vetoPeriodSeconds: number approvalTimestamp: number } + +export type SizeLimits = Partial> diff --git a/src/features/polling/PollingWorker.tsx b/src/features/polling/PollingWorker.tsx index 1d4520e..5c3fb77 100644 --- a/src/features/polling/PollingWorker.tsx +++ b/src/features/polling/PollingWorker.tsx @@ -1,16 +1,18 @@ import { useContractKit } from '@celo-tools/use-contractkit' import { useEffect } from 'react' import { toast } from 'react-toastify' -import { useAppDispatch } from 'src/app/hooks' +import { useAppDispatch, useAppSelector } from 'src/app/hooks' import { STATUS_POLLER_DELAY } from 'src/config/consts' import { fetchBalances } from 'src/features/accounts/fetchBalances' import { fetchLatestBlock } from 'src/features/blocks/fetchLatestBlock' import { fetchProposals } from 'src/features/granda/fetchProposals' +import { fetchSizeLimits } from 'src/features/granda/fetchSizeLimits' import { fetchExchangeRates } from 'src/features/swap/fetchExchangeRates' import { logger } from 'src/utils/logger' import { useInterval } from 'src/utils/timeout' export function PollingWorker() { + const isGrandaActive = useAppSelector((s) => s.granda.isActive) const dispatch = useAppDispatch() const { address, kit, initialised } = useContractKit() @@ -24,10 +26,16 @@ export function PollingWorker() { toast.warn('Error retrieving latest block') logger.error('Failed to retrieve latest block', err) }) - dispatch(fetchProposals({ kit })).catch((err) => { - toast.warn('Error retrieving Granda proposals') - logger.error('Failed to granda proposals', err) - }) + if (isGrandaActive) { + dispatch(fetchProposals({ kit })).catch((err) => { + toast.warn('Error retrieving Granda proposals') + logger.error('Failed to retrieve granda proposals', err) + }) + dispatch(fetchSizeLimits({ kit })).catch((err) => { + toast.warn('Error retrieving Granda size limits') + logger.error('Failed to retrieve granda size limits', err) + }) + } if (address) { dispatch(fetchBalances({ address, kit })).catch((err) => { toast.error('Error retrieving account balances') @@ -36,7 +44,7 @@ export function PollingWorker() { } } - useEffect(onPoll, [address, kit, initialised, dispatch]) + useEffect(onPoll, [isGrandaActive, address, kit, initialised, dispatch]) useInterval(onPoll, STATUS_POLLER_DELAY) diff --git a/src/features/swap/contracts.ts b/src/features/swap/contracts.ts index fb6d5b2..54a5d71 100644 --- a/src/features/swap/contracts.ts +++ b/src/features/swap/contracts.ts @@ -1,5 +1,5 @@ import type { ContractKit } from '@celo/contractkit' -import { StableToken } from '@celo/contractkit' +import { CeloContract, StableToken } from '@celo/contractkit' import { NativeTokenId } from 'src/config/tokens' export async function getExchangeContract(kit: ContractKit, tokenId: NativeTokenId) { @@ -25,3 +25,10 @@ export async function getTokenContract(kit: ContractKit, tokenId: NativeTokenId) throw new Error(`Could not get contract for token ${tokenId}`) } } + +export function getNativeTokenId(name: CeloContract): NativeTokenId { + if (name === CeloContract.StableToken) return NativeTokenId.cUSD + if (name === CeloContract.StableTokenEUR) return NativeTokenId.cEUR + if (name === CeloContract.GoldToken) return NativeTokenId.CELO + throw new Error(`Unsupported token contract name {name}`) +} diff --git a/src/pages/granda.tsx b/src/pages/granda.tsx index d969010..1707e39 100644 --- a/src/pages/granda.tsx +++ b/src/pages/granda.tsx @@ -1,6 +1,6 @@ import { useEffect } from 'react' import { useAppDispatch, useAppSelector } from 'src/app/hooks' -import { setSubpage } from 'src/features/granda/grandaSlice' +import { activateGranda, setSubpage } from 'src/features/granda/grandaSlice' import { ProposalConfirm } from 'src/features/granda/ProposalConfirm' import { ProposalForm } from 'src/features/granda/ProposalForm' import { ProposalList } from 'src/features/granda/ProposalList' @@ -12,6 +12,8 @@ export default function GrandaPage() { const dispatch = useAppDispatch() useEffect(() => { + dispatch(activateGranda()) + // Restore subpage to list if users leaves granda return () => { dispatch(setSubpage(GrandaSubpage.List)) From b92488f1d272efa3f3a901e452423a70f2538a4f Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Sat, 6 Nov 2021 14:13:26 +0100 Subject: [PATCH 06/30] Use swap form in granda form page --- src/components/TxSuccessToast.tsx | 18 ++++ src/features/granda/ProposalConfirm.tsx | 5 +- src/features/granda/ProposalForm.tsx | 45 +++++++--- src/features/granda/grandaSlice.ts | 11 ++- src/features/swap/SettingsMenu.tsx | 37 ++++++++ src/features/swap/SwapConfirm.tsx | 16 +--- src/features/swap/SwapForm.tsx | 115 ++++++++++-------------- src/features/swap/useFormValidator.ts | 34 +++++++ src/pages/granda.tsx | 3 +- 9 files changed, 182 insertions(+), 102 deletions(-) create mode 100644 src/components/TxSuccessToast.tsx create mode 100644 src/features/swap/SettingsMenu.tsx create mode 100644 src/features/swap/useFormValidator.ts diff --git a/src/components/TxSuccessToast.tsx b/src/components/TxSuccessToast.tsx new file mode 100644 index 0000000..34434bd --- /dev/null +++ b/src/components/TxSuccessToast.tsx @@ -0,0 +1,18 @@ +import { toast } from 'react-toastify' +import { TextLink } from 'src/components/buttons/TextLink' +import { config } from 'src/config/config' + +export function toastToYourSuccess(msg: string, txHash: string) { + toast.success(, { autoClose: 15000 }) +} + +export function TxSuccessToast({ msg, txHash }: { msg: string; txHash: string }) { + return ( +
+ {msg + ' '} + + See Details + +
+ ) +} diff --git a/src/features/granda/ProposalConfirm.tsx b/src/features/granda/ProposalConfirm.tsx index 80f5c82..56432c3 100644 --- a/src/features/granda/ProposalConfirm.tsx +++ b/src/features/granda/ProposalConfirm.tsx @@ -1,14 +1,13 @@ import { useAppDispatch } from 'src/app/hooks' import { BackButton } from 'src/components/buttons/BackButton' -import { setSubpage } from 'src/features/granda/grandaSlice' -import { GrandaSubpage } from 'src/features/granda/types' +import { setFormValues } from 'src/features/granda/grandaSlice' import { FloatingBox } from 'src/layout/FloatingBox' export function ProposalConfirm() { const dispatch = useAppDispatch() const onClickBack = () => { - dispatch(setSubpage(GrandaSubpage.List)) + dispatch(setFormValues(null)) } return ( diff --git a/src/features/granda/ProposalForm.tsx b/src/features/granda/ProposalForm.tsx index ed2e9c3..aafc620 100644 --- a/src/features/granda/ProposalForm.tsx +++ b/src/features/granda/ProposalForm.tsx @@ -1,24 +1,43 @@ -import { useAppDispatch } from 'src/app/hooks' -import { BackButton } from 'src/components/buttons/BackButton' -import { setSubpage } from 'src/features/granda/grandaSlice' -import { GrandaSubpage } from 'src/features/granda/types' +import { useAppDispatch, useAppSelector } from 'src/app/hooks' +import { setFormValues } from 'src/features/granda/grandaSlice' +import { SwapFormInner } from 'src/features/swap/SwapForm' +import { SwapFormValues } from 'src/features/swap/types' +import { useFormValidator } from 'src/features/swap/useFormValidator' import { FloatingBox } from 'src/layout/FloatingBox' export function ProposalForm() { - const dispatch = useAppDispatch() + const balances = useAppSelector((s) => s.account.balances) + const { toCeloRates, showSlippage } = useAppSelector((s) => s.swap) + const sizeLimits = useAppSelector((s) => s.granda.sizeLimits) - const onClickBack = () => { - dispatch(setSubpage(GrandaSubpage.List)) + const dispatch = useAppDispatch() + const onSubmit = (values: SwapFormValues) => { + dispatch(setFormValues(values)) } + const validateForm = useFormValidator(balances, sizeLimits) + // const onClickBack = () => { + // dispatch(setSubpage(GrandaSubpage.List)) + // } + return ( - -
- -

Create Proposal

-
+ +
+ {/* */} +

Propose Granda Exchange

+ {/*
*/} +
+
+
TODO icon
+
TODO info
-
TODO show form
+
) } diff --git a/src/features/granda/grandaSlice.ts b/src/features/granda/grandaSlice.ts index 35a7d97..f250495 100644 --- a/src/features/granda/grandaSlice.ts +++ b/src/features/granda/grandaSlice.ts @@ -20,7 +20,7 @@ export interface GrandaState { const initialState: GrandaState = { isActive: false, - subpage: GrandaSubpage.List, + subpage: GrandaSubpage.Form, proposals: {}, proposalsLastUpdated: null, viewProposalId: null, @@ -39,7 +39,14 @@ export const grandaSlice = createSlice({ state.subpage = action.payload }, setFormValues: (state, action: PayloadAction) => { - state.formValues = action.payload + const values = action.payload + if (values) { + state.subpage = GrandaSubpage.Confirm + state.formValues = values + } else { + state.subpage = GrandaSubpage.Form + state.formValues = null + } }, viewProposal: (state, action: PayloadAction) => { state.viewProposalId = action.payload diff --git a/src/features/swap/SettingsMenu.tsx b/src/features/swap/SettingsMenu.tsx new file mode 100644 index 0000000..ede19c0 --- /dev/null +++ b/src/features/swap/SettingsMenu.tsx @@ -0,0 +1,37 @@ +import useDropdownMenu from 'react-accessible-dropdown-menu-hook' +import { useAppDispatch, useAppSelector } from 'src/app/hooks' +import { IconButton } from 'src/components/buttons/IconButton' +import { SwitchButton } from 'src/components/buttons/SwitchButton' +import { setShowSlippage } from 'src/features/swap/swapSlice' +import Sliders from 'src/images/icons/sliders.svg' + +export function SettingsMenu() { + const showSlippage = useAppSelector((s) => s.swap.showSlippage) + const dispatch = useAppDispatch() + const onToggleSlippage = (checked: boolean) => { + dispatch(setShowSlippage(checked)) + } + + const { buttonProps, itemProps, isOpen } = useDropdownMenu(1) + + return ( + + ) +} diff --git a/src/features/swap/SwapConfirm.tsx b/src/features/swap/SwapConfirm.tsx index b712ec7..3c131c6 100644 --- a/src/features/swap/SwapConfirm.tsx +++ b/src/features/swap/SwapConfirm.tsx @@ -7,8 +7,7 @@ import { useAppDispatch, useAppSelector } from 'src/app/hooks' import { BackButton } from 'src/components/buttons/BackButton' import { RefreshButton } from 'src/components/buttons/RefreshButton' import { SolidButton } from 'src/components/buttons/SolidButton' -import { TextLink } from 'src/components/buttons/TextLink' -import { config } from 'src/config/config' +import { toastToYourSuccess } from 'src/components/TxSuccessToast' import { MAX_EXCHANGE_RATE, MAX_EXCHANGE_TOKEN_SIZE, @@ -109,7 +108,7 @@ export function SwapConfirm(props: Props) { exchangeOpWithTimeout )) as string[] if (!txHashes || txHashes.length !== 2) throw new Error('Tx hashes not found') - toast.success(, { autoClose: 15000 }) + toastToYourSuccess('Swap Complete!', txHashes[1]) dispatch(setFormValues(null)) } catch (err: any) { if (err.message === PROMISE_TIMEOUT) { @@ -206,14 +205,3 @@ function RightCircleArrow() {
) } - -function SuccessToast({ txHash }: { txHash: string }) { - return ( -
- Swap Complete!{' '} - - See Details - -
- ) -} diff --git a/src/features/swap/SwapForm.tsx b/src/features/swap/SwapForm.tsx index 1cce25b..cd70088 100644 --- a/src/features/swap/SwapForm.tsx +++ b/src/features/swap/SwapForm.tsx @@ -1,24 +1,22 @@ import { Connector, useContractKit } from '@celo-tools/use-contractkit' import { Field, Form, Formik, FormikErrors, useFormikContext } from 'formik' import { useCallback } from 'react' -import useDropdownMenu from 'react-accessible-dropdown-menu-hook' import { toast } from 'react-toastify' import { useAppDispatch, useAppSelector } from 'src/app/hooks' import { IconButton } from 'src/components/buttons/IconButton' import { SolidButton } from 'src/components/buttons/SolidButton' -import { SwitchButton } from 'src/components/buttons/SwitchButton' import { RadioInput } from 'src/components/input/RadioInput' import TokenSelectField, { TokenOption } from 'src/components/input/TokenSelectField' -import { MIN_ROUNDED_VALUE } from 'src/config/consts' import { CELO, cEUR, cUSD, isStableToken, NativeTokenId } from 'src/config/tokens' import { AccountBalances } from 'src/features/accounts/fetchBalances' -import { setFormValues, setShowSlippage } from 'src/features/swap/swapSlice' +import { SettingsMenu } from 'src/features/swap/SettingsMenu' +import { setFormValues } from 'src/features/swap/swapSlice' import { SwapFormValues, ToCeloRates } from 'src/features/swap/types' +import { useFormValidator } from 'src/features/swap/useFormValidator' import { useExchangeValues } from 'src/features/swap/utils' import DownArrow from 'src/images/icons/arrow-down-short.svg' -import Sliders from 'src/images/icons/sliders.svg' import { FloatingBox } from 'src/layout/FloatingBox' -import { areAmountsNearlyEqual, fromWeiRounded, parseAmount, toWei } from 'src/utils/amount' +import { fromWeiRounded } from 'src/utils/amount' import { useTimeout } from 'src/utils/timeout' const initialValues: SwapFormValues = { @@ -35,8 +33,6 @@ const tokens = [ ] export function SwapForm() { - const { connect, address } = useContractKit() - const balances = useAppSelector((s) => s.account.balances) const { toCeloRates, showSlippage } = useAppSelector((s) => s.swap) @@ -44,21 +40,7 @@ export function SwapForm() { const onSubmit = (values: SwapFormValues) => { dispatch(setFormValues(values)) } - - const validateForm = (values?: SwapFormValues): FormikErrors => { - if (!values || !values.fromAmount) return { fromAmount: 'Amount Required' } - const parsedAmount = parseAmount(values.fromAmount) - if (!parsedAmount) return { fromAmount: 'Amount is Invalid' } - if (parsedAmount.lt(0)) return { fromAmount: 'Amount cannot be negative' } - if (parsedAmount.lt(MIN_ROUNDED_VALUE)) return { fromAmount: 'Amount too small' } - const tokenId = values.fromTokenId - const tokenBalance = balances[tokenId] - const weiAmount = toWei(parsedAmount) - if (weiAmount.gt(tokenBalance) && !areAmountsNearlyEqual(weiAmount, tokenBalance)) { - return { fromAmount: 'Amount exceeds balance' } - } - return {} - } + const validateForm = useFormValidator(balances) return ( @@ -66,25 +48,53 @@ export function SwapForm() {

Swap

- - initialValues={initialValues} + -
- - {showSlippage && } -
- -
- - + validateForm={validateForm} + /> ) } +interface SwapFormInnerProps { + balances: AccountBalances + toCeloRates: ToCeloRates + showSlippage: boolean + onSubmit: (values: SwapFormValues) => void + validateForm: (values?: SwapFormValues) => FormikErrors +} + +export function SwapFormInner({ + balances, + toCeloRates, + showSlippage, + onSubmit, + validateForm, +}: SwapFormInnerProps) { + const { connect, address } = useContractKit() + + return ( + + initialValues={initialValues} + onSubmit={onSubmit} + validate={validateForm} + validateOnChange={false} + validateOnBlur={false} + > +
+ + {showSlippage && } +
+ +
+ + + ) +} + interface FormInputProps { balances: AccountBalances toCeloRates: ToCeloRates @@ -245,34 +255,3 @@ function SubmitButton({ address, connect }: ButtonProps) { ) } - -function SettingsMenu() { - const showSlippage = useAppSelector((s) => s.swap.showSlippage) - const dispatch = useAppDispatch() - const onToggleSlippage = (checked: boolean) => { - dispatch(setShowSlippage(checked)) - } - - const { buttonProps, itemProps, isOpen } = useDropdownMenu(1) - - return ( - - ) -} diff --git a/src/features/swap/useFormValidator.ts b/src/features/swap/useFormValidator.ts new file mode 100644 index 0000000..280d0fb --- /dev/null +++ b/src/features/swap/useFormValidator.ts @@ -0,0 +1,34 @@ +import { FormikErrors } from 'formik' +import { useCallback } from 'react' +import { MIN_ROUNDED_VALUE } from 'src/config/consts' +import { AccountBalances } from 'src/features/accounts/fetchBalances' +import { SizeLimits } from 'src/features/granda/types' +import { SwapFormValues } from 'src/features/swap/types' +import { areAmountsNearlyEqual, parseAmount, toWei } from 'src/utils/amount' + +export function useFormValidator(balances: AccountBalances, sizeLimits?: SizeLimits | null) { + return useCallback( + (values?: SwapFormValues): FormikErrors => { + if (!values || !values.fromAmount) return { fromAmount: 'Amount Required' } + const parsedAmount = parseAmount(values.fromAmount) + if (!parsedAmount) return { fromAmount: 'Amount is Invalid' } + if (parsedAmount.lt(0)) return { fromAmount: 'Amount cannot be negative' } + if (parsedAmount.lt(MIN_ROUNDED_VALUE)) return { fromAmount: 'Amount too small' } + const tokenId = values.fromTokenId + const tokenBalance = balances[tokenId] + const weiAmount = toWei(parsedAmount) + if (weiAmount.gt(tokenBalance) && !areAmountsNearlyEqual(weiAmount, tokenBalance)) { + return { fromAmount: 'Amount exceeds balance' } + } + if (sizeLimits) { + const fromTokenId = values.fromTokenId + const limits = sizeLimits[fromTokenId] + if (limits?.min && weiAmount.lt(limits?.min)) return { fromAmount: 'Amount below minimum' } + if (limits?.max && weiAmount.gt(limits?.max)) + return { fromAmount: 'Amount exceeds maximum' } + } + return {} + }, + [balances, sizeLimits] + ) +} diff --git a/src/pages/granda.tsx b/src/pages/granda.tsx index 1707e39..92d3750 100644 --- a/src/pages/granda.tsx +++ b/src/pages/granda.tsx @@ -13,10 +13,9 @@ export default function GrandaPage() { useEffect(() => { dispatch(activateGranda()) - // Restore subpage to list if users leaves granda return () => { - dispatch(setSubpage(GrandaSubpage.List)) + dispatch(setSubpage(GrandaSubpage.Form)) } // eslint-disable-next-line react-hooks/exhaustive-deps }, []) From 6f45e533ad699a833e16e396fd4c7c9350e8ac90 Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Sat, 6 Nov 2021 14:30:21 +0100 Subject: [PATCH 07/30] Use adjusted amount in swap confirm --- src/features/swap/SwapConfirm.tsx | 19 +++++++++++-------- src/utils/amount.ts | 15 +++++++++++++++ 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/src/features/swap/SwapConfirm.tsx b/src/features/swap/SwapConfirm.tsx index 3c131c6..c0e0679 100644 --- a/src/features/swap/SwapConfirm.tsx +++ b/src/features/swap/SwapConfirm.tsx @@ -24,7 +24,7 @@ import { getMinBuyAmount, useExchangeValues } from 'src/features/swap/utils' import { TokenIcon } from 'src/images/tokens/TokenIcon' import { FloatingBox } from 'src/layout/FloatingBox' import { Color } from 'src/styles/Color' -import { fromWeiRounded } from 'src/utils/amount' +import { fromWeiRounded, getAdjustedAmount } from 'src/utils/amount' import { logger } from 'src/utils/logger' import { asyncTimeout, PROMISE_TIMEOUT } from 'src/utils/timeout' @@ -35,6 +35,7 @@ interface Props { export function SwapConfirm(props: Props) { const { fromAmount, fromTokenId, toTokenId, slippage } = props.formValues const toCeloRates = useAppSelector((s) => s.swap.toCeloRates) + const balances = useAppSelector((s) => s.account.balances) const dispatch = useAppDispatch() const { address, kit, initialised, performActions } = useContractKit() @@ -52,7 +53,11 @@ export function SwapConfirm(props: Props) { toTokenId, toCeloRates ) - const minBuyAmountWei = getMinBuyAmount(from.weiAmount, slippage, rate.value) + const tokenBalance = balances[fromTokenId] + // Check if amount is almost equal to balance max, in which case use max + // Helps handle problems from imprecision in non-wei amount display + const finalFromAmount = getAdjustedAmount(from.weiAmount, tokenBalance) + const minBuyAmountWei = getMinBuyAmount(finalFromAmount, slippage, rate.value) const minBuyAmount = fromWeiRounded(minBuyAmountWei, true) const fromToken = NativeTokens[fromTokenId] const toToken = NativeTokens[toTokenId] @@ -62,8 +67,8 @@ export function SwapConfirm(props: Props) { toast.error('Kit not connected') return } - if (new BigNumber(from.weiAmount).gt(MAX_EXCHANGE_TOKEN_SIZE)) { - toast.error('Amount too large') + if (new BigNumber(finalFromAmount).gt(MAX_EXCHANGE_TOKEN_SIZE)) { + toast.error('Amount seems too large') return } if (rate.value < MIN_EXCHANGE_RATE || rate.value > MAX_EXCHANGE_RATE) { @@ -71,15 +76,13 @@ export function SwapConfirm(props: Props) { return } - // TODO get adjusted amount? - const approvalOperation = async (k: ContractKit) => { const stableTokenId = fromTokenId === NativeTokenId.CELO ? toTokenId : fromTokenId const tokenContract = await getTokenContract(k, fromTokenId) const exchangeContract = await getExchangeContract(k, stableTokenId) const approveTx = await tokenContract.increaseAllowance( exchangeContract.address, - from.weiAmount + finalFromAmount ) // Gas price must be set manually because contractkit pre-populate it and // its helpers for getting gas price are only meant for stable token prices @@ -93,7 +96,7 @@ export function SwapConfirm(props: Props) { const exchangeOperation = async (k: ContractKit) => { const sellGold = fromTokenId === NativeTokenId.CELO const exchangeContract = await getExchangeContract(k, stableTokenId) - const exchangeTx = await exchangeContract.sell(from.weiAmount, minBuyAmountWei, sellGold) + const exchangeTx = await exchangeContract.sell(finalFromAmount, minBuyAmountWei, sellGold) const gasPrice = await k.web3.eth.getGasPrice() const exchangeReceipt = await exchangeTx.sendAndWaitForReceipt({ gasPrice }) logger.info(`Tx receipt received for swap: ${exchangeReceipt.transactionHash}`) diff --git a/src/utils/amount.ts b/src/utils/amount.ts index 7a8a1c1..425fd8a 100644 --- a/src/utils/amount.ts +++ b/src/utils/amount.ts @@ -71,3 +71,18 @@ export function areAmountsNearlyEqual(amountInWei1: BigNumber, amountInWei2: Num // Is difference btwn amount and balance less than min amount shown for token return amountInWei1.minus(amountInWei2).abs().lt(minValueWei) } + +// Get amount that is adjusted when user input is nearly the same as max value +export function getAdjustedAmount( + _amountInWei: BigNumber.Value, + _maxAmount: BigNumber.Value +): BigNumber { + const amountInWei = new BigNumber(_amountInWei) + const maxAmount = new BigNumber(_maxAmount) + if (areAmountsNearlyEqual(amountInWei, maxAmount)) { + return maxAmount + } else { + // Just the amount entered, no adjustment needed + return amountInWei + } +} From 9b3d8da69e2021ef2544a2ba6f7866a34ce265a4 Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Sat, 6 Nov 2021 19:37:20 +0100 Subject: [PATCH 08/30] Fetch entire granda config --- src/features/granda/ProposalForm.tsx | 35 +++++++++++--- src/features/granda/fetchConfig.ts | 66 ++++++++++++++++++++++++++ src/features/granda/fetchSizeLimits.ts | 37 --------------- src/features/granda/grandaSlice.ts | 22 ++++----- src/features/granda/types.ts | 8 ++++ src/features/polling/PollingWorker.tsx | 8 ++-- src/features/swap/SwapForm.tsx | 4 +- src/images/icons/info-circle.svg | 4 ++ 8 files changed, 124 insertions(+), 60 deletions(-) create mode 100644 src/features/granda/fetchConfig.ts delete mode 100644 src/features/granda/fetchSizeLimits.ts create mode 100644 src/images/icons/info-circle.svg diff --git a/src/features/granda/ProposalForm.tsx b/src/features/granda/ProposalForm.tsx index aafc620..305c0e2 100644 --- a/src/features/granda/ProposalForm.tsx +++ b/src/features/granda/ProposalForm.tsx @@ -1,14 +1,18 @@ +import Image from 'next/image' import { useAppDispatch, useAppSelector } from 'src/app/hooks' -import { setFormValues } from 'src/features/granda/grandaSlice' +import { TextLink } from 'src/components/buttons/TextLink' +import { setFormValues, setSubpage } from 'src/features/granda/grandaSlice' +import { GrandaSubpage } from 'src/features/granda/types' import { SwapFormInner } from 'src/features/swap/SwapForm' import { SwapFormValues } from 'src/features/swap/types' import { useFormValidator } from 'src/features/swap/useFormValidator' +import InfoCircle from 'src/images/icons/info-circle.svg' import { FloatingBox } from 'src/layout/FloatingBox' export function ProposalForm() { const balances = useAppSelector((s) => s.account.balances) const { toCeloRates, showSlippage } = useAppSelector((s) => s.swap) - const sizeLimits = useAppSelector((s) => s.granda.sizeLimits) + const sizeLimits = useAppSelector((s) => s.granda.config?.exchangeLimits) const dispatch = useAppDispatch() const onSubmit = (values: SwapFormValues) => { @@ -20,16 +24,35 @@ export function ProposalForm() { // dispatch(setSubpage(GrandaSubpage.List)) // } + const onClickSeeHistory = () => { + dispatch(setSubpage(GrandaSubpage.List)) + } + return ( - +
{/* */}

Propose Granda Exchange

{/*
*/}
-
-
TODO icon
-
TODO info
+
+
+ info +
+ Granda Mento is a{' '} + + special process + {' '} + for very large exchanges (cUSD 500,000+). See{' '} + + . +
+
('granda/fetchConfig', async (params, thunkAPI) => { + const { kit } = params + const config = thunkAPI.getState().granda.config + if (!config) { + return _fetchConfig(kit) + } else { + return null + } +}) + +async function _fetchConfig(kit: ContractKit): Promise { + const contract = await kit.contracts.getGrandaMento() + const rawConfig = await contract.getConfig() + + const approver = rawConfig.approver + if (!approver || !isValidAddress(approver)) throw new Error(`Invalid approver: ${approver}`) + + const spread = rawConfig.spread.toNumber() + if (spread > 0.05 || spread < 0) throw new Error(`Invalid spread: ${spread}`) + + const vetoPeriodSeconds = rawConfig.vetoPeriodSeconds.toNumber() + if (vetoPeriodSeconds > 2592000 || vetoPeriodSeconds < 86400) + throw new Error(`Invalid veto period: ${vetoPeriodSeconds}`) + + const maxApprovalExchangeRateChange = rawConfig.maxApprovalExchangeRateChange.toNumber() + if (maxApprovalExchangeRateChange > 0.9 || maxApprovalExchangeRateChange < 0.01) + throw new Error(`Invalid approval exchange rate: ${maxApprovalExchangeRateChange}`) + + const exchangeLimits: SizeLimits = {} + for (const [name, limits] of rawConfig.exchangeLimits.entries()) { + const tokenId = getNativeTokenId(name) + const min = limits.minExchangeAmount + const max = limits.maxExchangeAmount + if (min.gt(toWei(1_000_000)) || min.lt(toWei(50_000))) + throw new Error(`Invalid exchange min: ${min}`) + if (max.gt(toWei(100_000_000)) || max.lt(toWei(1_000_000))) + throw new Error(`Invalid exchange max: ${max}`) + exchangeLimits[tokenId] = { + min: min.toFixed(0), + max: max.toFixed(0), + } + } + return { + approver, + spread, + vetoPeriodSeconds, + maxApprovalExchangeRateChange, + exchangeLimits, + } +} diff --git a/src/features/granda/fetchSizeLimits.ts b/src/features/granda/fetchSizeLimits.ts deleted file mode 100644 index 28a8441..0000000 --- a/src/features/granda/fetchSizeLimits.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { ContractKit } from '@celo/contractkit' -import { createAsyncThunk } from '@reduxjs/toolkit' -import type { AppDispatch, AppState } from 'src/app/store' -import { SizeLimits } from 'src/features/granda/types' -import { getNativeTokenId } from 'src/features/swap/contracts' - -interface FetchSizeLimitsParams { - kit: ContractKit -} - -export const fetchSizeLimits = createAsyncThunk< - SizeLimits | null, - FetchSizeLimitsParams, - { dispatch: AppDispatch; state: AppState } ->('granda/fetchSizeLimits', async (params, thunkAPI) => { - const { kit } = params - const sizeLimits = thunkAPI.getState().granda.sizeLimits - if (!sizeLimits) { - return _fetchSizeLimits(kit) - } else { - return null - } -}) - -async function _fetchSizeLimits(kit: ContractKit): Promise { - const contract = await kit.contracts.getGrandaMento() - const configMap = await contract.getAllStableTokenLimits() - const sizeLimits: SizeLimits = {} - for (const [name, limits] of configMap.entries()) { - const tokenId = getNativeTokenId(name) - sizeLimits[tokenId] = { - min: limits.minExchangeAmount.toFixed(0), - max: limits.maxExchangeAmount.toFixed(0), - } - } - return sizeLimits -} diff --git a/src/features/granda/grandaSlice.ts b/src/features/granda/grandaSlice.ts index f250495..f3bd63c 100644 --- a/src/features/granda/grandaSlice.ts +++ b/src/features/granda/grandaSlice.ts @@ -1,11 +1,11 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' +import { fetchConfig } from 'src/features/granda/fetchConfig' import { fetchProposals } from 'src/features/granda/fetchProposals' -import { fetchSizeLimits } from 'src/features/granda/fetchSizeLimits' import { + GrandaConfig, GrandaFormValues, GrandaProposal, GrandaSubpage, - SizeLimits, } from 'src/features/granda/types' export interface GrandaState { @@ -15,7 +15,7 @@ export interface GrandaState { proposalsLastUpdated: number | null viewProposalId: string | null // id of proposal for details view subpage formValues: GrandaFormValues | null - sizeLimits: SizeLimits | null + config: GrandaConfig | null } const initialState: GrandaState = { @@ -25,7 +25,7 @@ const initialState: GrandaState = { proposalsLastUpdated: null, viewProposalId: null, formValues: null, - sizeLimits: null, + config: null, } export const grandaSlice = createSlice({ @@ -56,8 +56,8 @@ export const grandaSlice = createSlice({ state.viewProposalId = null state.subpage = GrandaSubpage.List }, - setSizeLimits: (state, action: PayloadAction) => { - state.sizeLimits = action.payload + setConfig: (state, action: PayloadAction) => { + state.config = action.payload }, reset: () => initialState, }, @@ -68,10 +68,10 @@ export const grandaSlice = createSlice({ state.proposals = proposals state.proposalsLastUpdated = Date.now() }) - builder.addCase(fetchSizeLimits.fulfilled, (state, action) => { - const limits = action.payload - if (!limits) return - state.sizeLimits = limits + builder.addCase(fetchConfig.fulfilled, (state, action) => { + const config = action.payload + if (!config) return + state.config = config }) }, }) @@ -82,7 +82,7 @@ export const { setSubpage, viewProposal, clearProposal, - setSizeLimits, + setConfig: setSizeLimits, reset, } = grandaSlice.actions export const grandaReducer = grandaSlice.reducer diff --git a/src/features/granda/types.ts b/src/features/granda/types.ts index d05cfb2..9836032 100644 --- a/src/features/granda/types.ts +++ b/src/features/granda/types.ts @@ -32,4 +32,12 @@ export interface GrandaProposal { approvalTimestamp: number } +export interface GrandaConfig { + approver: string + spread: number + vetoPeriodSeconds: number + maxApprovalExchangeRateChange: number + exchangeLimits: SizeLimits +} + export type SizeLimits = Partial> diff --git a/src/features/polling/PollingWorker.tsx b/src/features/polling/PollingWorker.tsx index 5c3fb77..ea537f9 100644 --- a/src/features/polling/PollingWorker.tsx +++ b/src/features/polling/PollingWorker.tsx @@ -5,8 +5,8 @@ import { useAppDispatch, useAppSelector } from 'src/app/hooks' import { STATUS_POLLER_DELAY } from 'src/config/consts' import { fetchBalances } from 'src/features/accounts/fetchBalances' import { fetchLatestBlock } from 'src/features/blocks/fetchLatestBlock' +import { fetchConfig } from 'src/features/granda/fetchConfig' import { fetchProposals } from 'src/features/granda/fetchProposals' -import { fetchSizeLimits } from 'src/features/granda/fetchSizeLimits' import { fetchExchangeRates } from 'src/features/swap/fetchExchangeRates' import { logger } from 'src/utils/logger' import { useInterval } from 'src/utils/timeout' @@ -31,9 +31,9 @@ export function PollingWorker() { toast.warn('Error retrieving Granda proposals') logger.error('Failed to retrieve granda proposals', err) }) - dispatch(fetchSizeLimits({ kit })).catch((err) => { - toast.warn('Error retrieving Granda size limits') - logger.error('Failed to retrieve granda size limits', err) + dispatch(fetchConfig({ kit })).catch((err) => { + toast.warn('Error retrieving Granda config') + logger.error('Failed to retrieve granda config', err) }) } if (address) { diff --git a/src/features/swap/SwapForm.tsx b/src/features/swap/SwapForm.tsx index cd70088..b79cbdc 100644 --- a/src/features/swap/SwapForm.tsx +++ b/src/features/swap/SwapForm.tsx @@ -44,7 +44,7 @@ export function SwapForm() { return ( -
+

Swap

@@ -138,7 +138,7 @@ function SwapFormInputs(props: FormInputProps) { return (
-
+
+ + + \ No newline at end of file From baa1a4786b587466276055b569b3e940117292d5 Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Sat, 6 Nov 2021 19:37:29 +0100 Subject: [PATCH 09/30] Bump version to 1.1.0 --- package.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index dcf5df4..1fbcee8 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,11 @@ { "name": "@celo-tools/mento-fi", - "version": "1.0.0", + "version": "1.1.0", "description": "A simple DApp for Celo Mento exchanges", "keywords": [ "Celo", "Mento", + "Granda", "Exchange" ], "author": "J M Rossy", @@ -12,7 +13,7 @@ "type": "git", "url": "https://github.com/celo-tools/mento-fi" }, - "homepage": "TODO", + "homepage": "https://mento.finance", "license": "Apache-2.0", "scripts": { "dev": "next", From 957364adf79f3d1cdfa338267059cf7806c18185 Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Sat, 6 Nov 2021 20:08:22 +0100 Subject: [PATCH 10/30] Make exchange value formatter injectable into form inputs --- src/features/granda/ProposalForm.tsx | 60 +++++++++++++++++----------- src/features/swap/SwapConfirm.tsx | 4 +- src/features/swap/SwapForm.tsx | 28 ++++++++----- src/features/swap/utils.ts | 16 +++++--- 4 files changed, 66 insertions(+), 42 deletions(-) diff --git a/src/features/granda/ProposalForm.tsx b/src/features/granda/ProposalForm.tsx index 305c0e2..b849de0 100644 --- a/src/features/granda/ProposalForm.tsx +++ b/src/features/granda/ProposalForm.tsx @@ -6,6 +6,7 @@ import { GrandaSubpage } from 'src/features/granda/types' import { SwapFormInner } from 'src/features/swap/SwapForm' import { SwapFormValues } from 'src/features/swap/types' import { useFormValidator } from 'src/features/swap/useFormValidator' +import { ExchangeValueFormatter, getExchangeValues } from 'src/features/swap/utils' import InfoCircle from 'src/images/icons/info-circle.svg' import { FloatingBox } from 'src/layout/FloatingBox' @@ -20,14 +21,15 @@ export function ProposalForm() { } const validateForm = useFormValidator(balances, sizeLimits) + + // TODO use diff getExchangeValues + const valueFormatter: ExchangeValueFormatter = (fromAmount, fromTokenId, toTokenId) => + getExchangeValues(fromAmount, fromTokenId, toTokenId, toCeloRates) + // const onClickBack = () => { // dispatch(setSubpage(GrandaSubpage.List)) // } - const onClickSeeHistory = () => { - dispatch(setSubpage(GrandaSubpage.List)) - } - return (
@@ -35,28 +37,10 @@ export function ProposalForm() {

Propose Granda Exchange

{/*
*/}
-
-
- info -
- Granda Mento is a{' '} - - special process - {' '} - for very large exchanges (cUSD 500,000+). See{' '} - - . -
-
-
+ ) } + +function InfoTip() { + const dispatch = useAppDispatch() + const onClickSeeHistory = () => { + dispatch(setSubpage(GrandaSubpage.List)) + } + return ( +
+
+ info +
+ Granda Mento is a{' '} + + special process + {' '} + for very large exchanges (cUSD 500,000+). See{' '} + + . +
+
+
+ ) +} diff --git a/src/features/swap/SwapConfirm.tsx b/src/features/swap/SwapConfirm.tsx index c0e0679..9a610ab 100644 --- a/src/features/swap/SwapConfirm.tsx +++ b/src/features/swap/SwapConfirm.tsx @@ -20,7 +20,7 @@ import { getExchangeContract, getTokenContract } from 'src/features/swap/contrac import { fetchExchangeRates } from 'src/features/swap/fetchExchangeRates' import { setFormValues } from 'src/features/swap/swapSlice' import { SwapFormValues } from 'src/features/swap/types' -import { getMinBuyAmount, useExchangeValues } from 'src/features/swap/utils' +import { getExchangeValues, getMinBuyAmount } from 'src/features/swap/utils' import { TokenIcon } from 'src/images/tokens/TokenIcon' import { FloatingBox } from 'src/layout/FloatingBox' import { Color } from 'src/styles/Color' @@ -47,7 +47,7 @@ export function SwapConfirm(props: Props) { } }, [isConfirmValid, dispatch]) - const { from, to, rate, stableTokenId } = useExchangeValues( + const { from, to, rate, stableTokenId } = getExchangeValues( fromAmount, fromTokenId, toTokenId, diff --git a/src/features/swap/SwapForm.tsx b/src/features/swap/SwapForm.tsx index b79cbdc..5547163 100644 --- a/src/features/swap/SwapForm.tsx +++ b/src/features/swap/SwapForm.tsx @@ -11,9 +11,9 @@ import { CELO, cEUR, cUSD, isStableToken, NativeTokenId } from 'src/config/token import { AccountBalances } from 'src/features/accounts/fetchBalances' import { SettingsMenu } from 'src/features/swap/SettingsMenu' import { setFormValues } from 'src/features/swap/swapSlice' -import { SwapFormValues, ToCeloRates } from 'src/features/swap/types' +import { SwapFormValues } from 'src/features/swap/types' import { useFormValidator } from 'src/features/swap/useFormValidator' -import { useExchangeValues } from 'src/features/swap/utils' +import { ExchangeValueFormatter, getExchangeValues } from 'src/features/swap/utils' import DownArrow from 'src/images/icons/arrow-down-short.svg' import { FloatingBox } from 'src/layout/FloatingBox' import { fromWeiRounded } from 'src/utils/amount' @@ -42,6 +42,9 @@ export function SwapForm() { } const validateForm = useFormValidator(balances) + const valueFormatter: ExchangeValueFormatter = (fromAmount, fromTokenId, toTokenId) => + getExchangeValues(fromAmount, fromTokenId, toTokenId, toCeloRates) + return (
@@ -50,7 +53,7 @@ export function SwapForm() {
void validateForm: (values?: SwapFormValues) => FormikErrors + valueFormatter: ExchangeValueFormatter } export function SwapFormInner({ balances, - toCeloRates, showSlippage, onSubmit, validateForm, + valueFormatter, }: SwapFormInnerProps) { const { connect, address } = useContractKit() @@ -85,7 +88,11 @@ export function SwapFormInner({ validateOnBlur={false} >
- + {showSlippage && }
@@ -97,19 +104,18 @@ export function SwapFormInner({ interface FormInputProps { balances: AccountBalances - toCeloRates: ToCeloRates isConnected: boolean + valueFormatter: ExchangeValueFormatter } function SwapFormInputs(props: FormInputProps) { - const { balances, toCeloRates, isConnected } = props + const { balances, isConnected, valueFormatter } = props const { values, setFieldValue } = useFormikContext() - const { to, rate, stableTokenId } = useExchangeValues( + const { to, rate, stableTokenId } = valueFormatter( values.fromAmount, values.fromTokenId, - values.toTokenId, - toCeloRates + values.toTokenId ) const roundedBalance = fromWeiRounded(balances[values.fromTokenId]) diff --git a/src/features/swap/utils.ts b/src/features/swap/utils.ts index d1baeb1..66d3dcb 100644 --- a/src/features/swap/utils.ts +++ b/src/features/swap/utils.ts @@ -15,7 +15,13 @@ import { } from 'src/utils/amount' import { logger } from 'src/utils/logger' -interface ExchangeValues { +export type ExchangeValueFormatter = ( + fromAmount: NumberT | null | undefined, + fromTokenId: NativeTokenId | null | undefined, + toTokenId: NativeTokenId | null | undefined +) => ExchangeValues + +export interface ExchangeValues { from: { amount: string weiAmount: string @@ -38,12 +44,12 @@ interface ExchangeValues { stableTokenId: NativeTokenId } -export function useExchangeValues( +// Takes raw input and rates info and computes/formats to convenient form +export function getExchangeValues( fromAmount: NumberT | null | undefined, fromTokenId: NativeTokenId | null | undefined, toTokenId: NativeTokenId | null | undefined, - toCeloRates: ToCeloRates, - isFromAmountWei = false + toCeloRates: ToCeloRates ): ExchangeValues { // Return some defaults when values are missing if (!fromTokenId || !toTokenId || !toCeloRates) return getDefaultExchangeValues() @@ -56,7 +62,7 @@ export function useExchangeValues( const { stableBucket, celoBucket, spread } = toCeloRate const [buyBucket, sellBucket] = sellCelo ? [stableBucket, celoBucket] : [celoBucket, stableBucket] - const fromAmountWei = parseInputExchangeAmount(fromAmount, isFromAmountWei) + const fromAmountWei = parseInputExchangeAmount(fromAmount, false) const { exchangeRateNum, exchangeRateWei, fromCeloRateWei, toAmountWei } = calcSimpleExchangeRate( fromAmountWei, buyBucket, From f14472cc77b4db4e65a8db217d4b2f12ddd44887 Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Sun, 7 Nov 2021 17:08:49 +0100 Subject: [PATCH 11/30] Implement oracle rate fetching and exchange estimation for granda --- src/config/consts.ts | 2 +- src/features/granda/ProposalForm.tsx | 15 ++--- src/features/granda/fetchOracleRates.ts | 50 ++++++++++++++++ src/features/granda/fetchProposals.ts | 2 +- src/features/granda/grandaSlice.ts | 31 +++++----- src/features/granda/types.ts | 3 + src/features/granda/utils.ts | 77 +++++++++++++++++++++++++ src/features/polling/PollingWorker.tsx | 5 ++ src/features/swap/contracts.ts | 17 +++++- src/features/swap/fetchExchangeRates.ts | 20 +++---- src/features/swap/types.ts | 2 +- src/features/swap/utils.ts | 10 +--- src/utils/time.ts | 10 ++++ 13 files changed, 196 insertions(+), 48 deletions(-) create mode 100644 src/features/granda/fetchOracleRates.ts create mode 100644 src/features/granda/utils.ts diff --git a/src/config/consts.ts b/src/config/consts.ts index 21bd49a..2e777ce 100644 --- a/src/config/consts.ts +++ b/src/config/consts.ts @@ -5,7 +5,7 @@ export const STALE_BLOCK_TIME = 25000 // 25 seconds export const EXCHANGE_RATE_STALE_TIME = 5000 // 5 second export const GRANDA_PROPOSAL_STALE_TIME = 30000 // 30 second export const BALANCE_STALE_TIME = 5000 // 5 seconds -export const STATUS_POLLER_DELAY = 10000 // 10 seconds +export const STATUS_POLLER_DELAY = 5000 // 5 seconds export const SIGN_OPERATION_TIMEOUT = 90000 // 90 seconds export const STALE_TOKEN_PRICE_TIME = 900000 // 15 minutes diff --git a/src/features/granda/ProposalForm.tsx b/src/features/granda/ProposalForm.tsx index b849de0..415c314 100644 --- a/src/features/granda/ProposalForm.tsx +++ b/src/features/granda/ProposalForm.tsx @@ -3,28 +3,29 @@ import { useAppDispatch, useAppSelector } from 'src/app/hooks' import { TextLink } from 'src/components/buttons/TextLink' import { setFormValues, setSubpage } from 'src/features/granda/grandaSlice' import { GrandaSubpage } from 'src/features/granda/types' +import { getExchangeValues } from 'src/features/granda/utils' import { SwapFormInner } from 'src/features/swap/SwapForm' import { SwapFormValues } from 'src/features/swap/types' import { useFormValidator } from 'src/features/swap/useFormValidator' -import { ExchangeValueFormatter, getExchangeValues } from 'src/features/swap/utils' +import { ExchangeValueFormatter } from 'src/features/swap/utils' import InfoCircle from 'src/images/icons/info-circle.svg' import { FloatingBox } from 'src/layout/FloatingBox' export function ProposalForm() { const balances = useAppSelector((s) => s.account.balances) - const { toCeloRates, showSlippage } = useAppSelector((s) => s.swap) - const sizeLimits = useAppSelector((s) => s.granda.config?.exchangeLimits) + const { config, oracleRates } = useAppSelector((s) => s.granda) + const exchangeLimits = config?.exchangeLimits + const spread = config?.spread const dispatch = useAppDispatch() const onSubmit = (values: SwapFormValues) => { dispatch(setFormValues(values)) } - const validateForm = useFormValidator(balances, sizeLimits) + const validateForm = useFormValidator(balances, exchangeLimits) - // TODO use diff getExchangeValues const valueFormatter: ExchangeValueFormatter = (fromAmount, fromTokenId, toTokenId) => - getExchangeValues(fromAmount, fromTokenId, toTokenId, toCeloRates) + getExchangeValues(fromAmount, fromTokenId, toTokenId, spread, oracleRates) // const onClickBack = () => { // dispatch(setSubpage(GrandaSubpage.List)) @@ -41,7 +42,7 @@ export function ProposalForm() { diff --git a/src/features/granda/fetchOracleRates.ts b/src/features/granda/fetchOracleRates.ts new file mode 100644 index 0000000..559de24 --- /dev/null +++ b/src/features/granda/fetchOracleRates.ts @@ -0,0 +1,50 @@ +import type { ContractKit } from '@celo/contractkit' +import { createAsyncThunk } from '@reduxjs/toolkit' +import type { AppDispatch, AppState } from 'src/app/store' +import { MAX_EXCHANGE_RATE, MIN_EXCHANGE_RATE } from 'src/config/consts' +import { NativeTokenId, StableTokenIds } from 'src/config/tokens' +import { OracleRates } from 'src/features/granda/types' +import { getContractKitToken } from 'src/features/swap/contracts' +import { SimpleExchangeRate } from 'src/features/swap/types' +import { logger } from 'src/utils/logger' +import { areRatesStale } from 'src/utils/time' + +interface FetchOracleRatesParams { + kit: ContractKit +} + +export const fetchOracleRates = createAsyncThunk< + OracleRates | null, + FetchOracleRatesParams, + { dispatch: AppDispatch; state: AppState } +>('granda/fetchOracleRates', async (params, thunkAPI) => { + const { kit } = params + const oracleRates = thunkAPI.getState().granda.oracleRates + if (areRatesStale(oracleRates)) { + const newRates: OracleRates = {} + for (const tokenId of StableTokenIds) { + const rate = await _fetchOracleRates(kit, tokenId) + newRates[tokenId] = rate + } + return newRates + } else { + return null + } +}) + +async function _fetchOracleRates( + kit: ContractKit, + tokenId: NativeTokenId +): Promise { + logger.debug('Fetching oracle rate for:', tokenId) + const token = getContractKitToken(tokenId) + const stableTokenAddress = await kit.celoTokens.getAddress(token) + const sortedOracles = await kit.contracts.getSortedOracles() + const { rate } = await sortedOracles.medianRate(stableTokenAddress) + if (!rate || rate.lt(MIN_EXCHANGE_RATE) || rate.gt(MAX_EXCHANGE_RATE)) + throw new Error(`Invalid oracle rate ${rate}`) + return { + rate: rate.toNumber(), + lastUpdated: Date.now(), + } +} diff --git a/src/features/granda/fetchProposals.ts b/src/features/granda/fetchProposals.ts index 3b19bc0..b5202cd 100644 --- a/src/features/granda/fetchProposals.ts +++ b/src/features/granda/fetchProposals.ts @@ -16,7 +16,7 @@ export const fetchProposals = createAsyncThunk< { dispatch: AppDispatch; state: AppState } >('granda/fetchProposals', async (params, thunkAPI) => { const { kit } = params - const { proposalsLastUpdated } = thunkAPI.getState().granda + const proposalsLastUpdated = thunkAPI.getState().granda.proposalsLastUpdated if (isStale(proposalsLastUpdated, GRANDA_PROPOSAL_STALE_TIME)) { return _fetchProposals(kit) } else { diff --git a/src/features/granda/grandaSlice.ts b/src/features/granda/grandaSlice.ts index f3bd63c..bf7af9a 100644 --- a/src/features/granda/grandaSlice.ts +++ b/src/features/granda/grandaSlice.ts @@ -1,30 +1,34 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' import { fetchConfig } from 'src/features/granda/fetchConfig' +import { fetchOracleRates } from 'src/features/granda/fetchOracleRates' import { fetchProposals } from 'src/features/granda/fetchProposals' import { GrandaConfig, GrandaFormValues, GrandaProposal, GrandaSubpage, + OracleRates, } from 'src/features/granda/types' export interface GrandaState { isActive: boolean // tracks if granda page was loaded to lazily fetch granda info subpage: GrandaSubpage - proposals: Record - proposalsLastUpdated: number | null viewProposalId: string | null // id of proposal for details view subpage formValues: GrandaFormValues | null + proposals: Record + proposalsLastUpdated: number | null + oracleRates: OracleRates config: GrandaConfig | null } const initialState: GrandaState = { isActive: false, subpage: GrandaSubpage.Form, - proposals: {}, - proposalsLastUpdated: null, viewProposalId: null, formValues: null, + proposals: {}, + proposalsLastUpdated: null, + oracleRates: {}, config: null, } @@ -56,9 +60,6 @@ export const grandaSlice = createSlice({ state.viewProposalId = null state.subpage = GrandaSubpage.List }, - setConfig: (state, action: PayloadAction) => { - state.config = action.payload - }, reset: () => initialState, }, extraReducers: (builder) => { @@ -68,6 +69,11 @@ export const grandaSlice = createSlice({ state.proposals = proposals state.proposalsLastUpdated = Date.now() }) + builder.addCase(fetchOracleRates.fulfilled, (state, action) => { + const rates = action.payload + if (!rates) return + state.oracleRates = rates + }) builder.addCase(fetchConfig.fulfilled, (state, action) => { const config = action.payload if (!config) return @@ -76,13 +82,6 @@ export const grandaSlice = createSlice({ }, }) -export const { - activateGranda, - setFormValues, - setSubpage, - viewProposal, - clearProposal, - setConfig: setSizeLimits, - reset, -} = grandaSlice.actions +export const { activateGranda, setFormValues, setSubpage, viewProposal, clearProposal, reset } = + grandaSlice.actions export const grandaReducer = grandaSlice.reducer diff --git a/src/features/granda/types.ts b/src/features/granda/types.ts index 9836032..d434177 100644 --- a/src/features/granda/types.ts +++ b/src/features/granda/types.ts @@ -1,4 +1,5 @@ import { NativeTokenId } from 'src/config/tokens' +import { SimpleExchangeRate } from 'src/features/swap/types' export enum GrandaSubpage { List = 'list', @@ -32,6 +33,8 @@ export interface GrandaProposal { approvalTimestamp: number } +export type OracleRates = Partial> + export interface GrandaConfig { approver: string spread: number diff --git a/src/features/granda/utils.ts b/src/features/granda/utils.ts new file mode 100644 index 0000000..1164096 --- /dev/null +++ b/src/features/granda/utils.ts @@ -0,0 +1,77 @@ +import BigNumber from 'bignumber.js' +import { WEI_PER_UNIT } from 'src/config/consts' +import { NativeTokenId } from 'src/config/tokens' +import { OracleRates } from 'src/features/granda/types' +import { + ExchangeValues, + getDefaultExchangeValues, + parseInputExchangeAmount, +} from 'src/features/swap/utils' +import { fromWeiRounded, NumberT, toWei } from 'src/utils/amount' +import { logger } from 'src/utils/logger' + +// Takes raw input and rates info and computes/formats to convenient form +export function getExchangeValues( + fromAmount: NumberT | null | undefined, + fromTokenId: NativeTokenId | null | undefined, + toTokenId: NativeTokenId | null | undefined, + spread: BigNumber.Value | null | undefined, + oracleRates: OracleRates +): ExchangeValues { + try { + // Return some defaults when values are missing + if (!fromTokenId || !toTokenId || !oracleRates || spread === null || spread === undefined) + return getDefaultExchangeValues() + + const sellCelo = fromTokenId === NativeTokenId.CELO + const stableTokenId = sellCelo ? toTokenId : fromTokenId + const fromCeloRate = oracleRates[stableTokenId] + if (!fromCeloRate) return getDefaultExchangeValues(fromTokenId, toTokenId) + + const { rate: fromCeloRateNum } = fromCeloRate + const fromCeloRateWei = toWei(fromCeloRateNum) + const toCeloRateNum = 1 / fromCeloRateNum + const toCeloRateWei = toWei(toCeloRateNum) + const exchangeRateNum = sellCelo ? fromCeloRateNum : toCeloRateNum + const exchangeRateWei = sellCelo ? fromCeloRateWei : toCeloRateWei + + const fromAmountWei = parseInputExchangeAmount(fromAmount, false) + const toAmountWei = getBuyAmount(fromAmountWei, exchangeRateNum, spread) + + return { + from: { + amount: fromWeiRounded(fromAmountWei, true), + weiAmount: fromAmountWei.toString(), + token: fromTokenId, + }, + to: { + amount: fromWeiRounded(toAmountWei, true), + weiAmount: toAmountWei.toString(), + token: toTokenId, + }, + rate: { + value: exchangeRateNum, + weiValue: exchangeRateWei.toString(), + fromCeloValue: fromWeiRounded(fromCeloRateWei, true), + fromCeloWeiValue: fromCeloRateWei.toString(), + weiBasis: WEI_PER_UNIT, + lastUpdated: fromCeloRate.lastUpdated, + isReady: true, + }, + stableTokenId, + } + } catch (error) { + logger.warn('Error computing exchange values', error) + return getDefaultExchangeValues() + } +} + +// Should match GetBuyAmount in https://github.com/celo-org/celo-monorepo/blob/master/packages/protocol/contracts/stability/GrandaMento.sol +function getBuyAmount( + amountInWei: BigNumber.Value, + exchangeRate: BigNumber.Value, + spread: BigNumber.Value +): BigNumber { + const adjustedAmount = new BigNumber(amountInWei).times(new BigNumber(1).minus(spread)) + return adjustedAmount.times(exchangeRate) +} diff --git a/src/features/polling/PollingWorker.tsx b/src/features/polling/PollingWorker.tsx index ea537f9..decbe49 100644 --- a/src/features/polling/PollingWorker.tsx +++ b/src/features/polling/PollingWorker.tsx @@ -6,6 +6,7 @@ import { STATUS_POLLER_DELAY } from 'src/config/consts' import { fetchBalances } from 'src/features/accounts/fetchBalances' import { fetchLatestBlock } from 'src/features/blocks/fetchLatestBlock' import { fetchConfig } from 'src/features/granda/fetchConfig' +import { fetchOracleRates } from 'src/features/granda/fetchOracleRates' import { fetchProposals } from 'src/features/granda/fetchProposals' import { fetchExchangeRates } from 'src/features/swap/fetchExchangeRates' import { logger } from 'src/utils/logger' @@ -35,6 +36,10 @@ export function PollingWorker() { toast.warn('Error retrieving Granda config') logger.error('Failed to retrieve granda config', err) }) + dispatch(fetchOracleRates({ kit })).catch((err) => { + toast.warn('Error retrieving oracle rates') + logger.error('Failed to retrieve oracle rates', err) + }) } if (address) { dispatch(fetchBalances({ address, kit })).catch((err) => { diff --git a/src/features/swap/contracts.ts b/src/features/swap/contracts.ts index 54a5d71..9417206 100644 --- a/src/features/swap/contracts.ts +++ b/src/features/swap/contracts.ts @@ -1,5 +1,5 @@ import type { ContractKit } from '@celo/contractkit' -import { CeloContract, StableToken } from '@celo/contractkit' +import { CeloContract, CeloTokenType, StableToken, Token } from '@celo/contractkit' import { NativeTokenId } from 'src/config/tokens' export async function getExchangeContract(kit: ContractKit, tokenId: NativeTokenId) { @@ -30,5 +30,18 @@ export function getNativeTokenId(name: CeloContract): NativeTokenId { if (name === CeloContract.StableToken) return NativeTokenId.cUSD if (name === CeloContract.StableTokenEUR) return NativeTokenId.cEUR if (name === CeloContract.GoldToken) return NativeTokenId.CELO - throw new Error(`Unsupported token contract name {name}`) + throw new Error(`Unsupported token contract name ${name}`) +} + +export function getContractKitToken(tokenId: NativeTokenId): CeloTokenType { + switch (tokenId) { + case NativeTokenId.cUSD: + return StableToken.cUSD + case NativeTokenId.cEUR: + return StableToken.cEUR + case NativeTokenId.CELO: + return Token.CELO + default: + throw new Error(`Unsupported token id ${tokenId}`) + } } diff --git a/src/features/swap/fetchExchangeRates.ts b/src/features/swap/fetchExchangeRates.ts index 3563eb6..17cb589 100644 --- a/src/features/swap/fetchExchangeRates.ts +++ b/src/features/swap/fetchExchangeRates.ts @@ -1,11 +1,12 @@ import type { ContractKit } from '@celo/contractkit' import { createAsyncThunk } from '@reduxjs/toolkit' import type { AppDispatch, AppState } from 'src/app/store' -import { EXCHANGE_RATE_STALE_TIME, MAX_EXCHANGE_SPREAD } from 'src/config/consts' +import { MAX_EXCHANGE_SPREAD } from 'src/config/consts' import { NativeTokenId, StableTokenIds } from 'src/config/tokens' import { getExchangeContract } from 'src/features/swap/contracts' import { ExchangeRate, ToCeloRates } from 'src/features/swap/types' -import { isStale } from 'src/utils/time' +import { logger } from 'src/utils/logger' +import { areRatesStale } from 'src/utils/time' interface FetchExchangeRatesParams { kit: ContractKit @@ -21,12 +22,12 @@ export const fetchExchangeRates = createAsyncThunk< const { kit } = params const toCeloRates = thunkAPI.getState().swap.toCeloRates if (areRatesStale(toCeloRates)) { - const newToCeloRates: ToCeloRates = {} + const newRates: ToCeloRates = {} for (const tokenId of StableTokenIds) { const rate = await _fetchExchangeRates(kit, tokenId) - newToCeloRates[tokenId] = rate + newRates[tokenId] = rate } - return newToCeloRates + return newRates } else { return null } @@ -36,6 +37,7 @@ async function _fetchExchangeRates( kit: ContractKit, tokenId: NativeTokenId ): Promise { + logger.debug('Fetching exchange rate for:', tokenId) const contract = await getExchangeContract(kit, tokenId) const spread = await contract.spread() if (spread.lt(0) || spread.gt(MAX_EXCHANGE_SPREAD)) @@ -53,11 +55,3 @@ async function _fetchExchangeRates( lastUpdated: Date.now(), } } - -function areRatesStale(rates: ToCeloRates) { - return ( - !rates || - !Object.keys(rates).length || - Object.values(rates).some((r) => isStale(r.lastUpdated, EXCHANGE_RATE_STALE_TIME)) - ) -} diff --git a/src/features/swap/types.ts b/src/features/swap/types.ts index 69c674f..ae50342 100644 --- a/src/features/swap/types.ts +++ b/src/features/swap/types.ts @@ -7,7 +7,7 @@ export interface SwapFormValues { slippage: string } -export type ToCeloRates = Record // token id to token<->CELO rate +export type ToCeloRates = Partial> // Raw Mento chain data from an Exchange contract export interface ExchangeRate { diff --git a/src/features/swap/utils.ts b/src/features/swap/utils.ts index 66d3dcb..ded2ed2 100644 --- a/src/features/swap/utils.ts +++ b/src/features/swap/utils.ts @@ -1,7 +1,3 @@ -// import { WEI_PER_UNIT } from 'src/config/consts' -// import { toWei } from 'src/utils/amount' -// import { logger } from 'src/utils/logger' - import BigNumber from 'bignumber.js' import { WEI_PER_UNIT } from 'src/config/consts' import { NativeTokenId } from 'src/config/tokens' @@ -95,7 +91,7 @@ export function getExchangeValues( } } -function parseInputExchangeAmount(amount: NumberT | null | undefined, isWei: boolean) { +export function parseInputExchangeAmount(amount: NumberT | null | undefined, isWei: boolean) { const parsed = parseAmountWithDefault(amount, 0) const parsedWei = isWei ? parsed : toWei(parsed) return BigNumber.max(parsedWei, 0) @@ -132,12 +128,12 @@ export function calcSimpleExchangeRate( return { exchangeRateNum, exchangeRateWei, fromCeloRateWei, toAmountWei } } catch (error) { - logger.warn('Error computing exchange values') + logger.warn('Error computing exchange values', error) return { exchangeRateNum: 0, exchangeRateWei: '0', fromCeloRateWei: '0', toAmountWei: '0' } } } -function getDefaultExchangeValues( +export function getDefaultExchangeValues( _fromToken?: NativeTokenId | null, _toToken?: NativeTokenId | null ): ExchangeValues { diff --git a/src/utils/time.ts b/src/utils/time.ts index 6ba2176..20cdcdf 100644 --- a/src/utils/time.ts +++ b/src/utils/time.ts @@ -1,7 +1,17 @@ +import { EXCHANGE_RATE_STALE_TIME } from 'src/config/consts' + export function isStale(lastUpdated: number | null, staleTime: number) { return !lastUpdated || Date.now() - lastUpdated > staleTime } +export function areRatesStale(rates: Record) { + return ( + !rates || + !Object.keys(rates).length || + Object.values(rates).some((r) => isStale(r.lastUpdated, EXCHANGE_RATE_STALE_TIME)) + ) +} + export function areDatesSameDay(d1: Date, d2: Date) { return ( d1.getDate() === d2.getDate() && From 003fd0fcda413949f1e528322c50ab5f538d7acc Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Sun, 7 Nov 2021 21:24:58 +0100 Subject: [PATCH 12/30] Implement granda confirm functionality (untested) --- src/features/granda/ProposalConfirm.tsx | 123 +++++++++++++++++++++++- src/features/granda/fetchOracleRates.ts | 4 +- src/features/swap/SwapConfirm.tsx | 78 +++++++++------ src/features/swap/contracts.ts | 2 +- src/layout/AppLayout.tsx | 13 ++- 5 files changed, 181 insertions(+), 39 deletions(-) diff --git a/src/features/granda/ProposalConfirm.tsx b/src/features/granda/ProposalConfirm.tsx index 56432c3..0131f5b 100644 --- a/src/features/granda/ProposalConfirm.tsx +++ b/src/features/granda/ProposalConfirm.tsx @@ -1,23 +1,140 @@ -import { useAppDispatch } from 'src/app/hooks' +import { useContractKit } from '@celo-tools/use-contractkit' +import { ContractKit, StableToken } from '@celo/contractkit' +import { useEffect } from 'react' +import { toast } from 'react-toastify' +import { useAppDispatch, useAppSelector } from 'src/app/hooks' import { BackButton } from 'src/components/buttons/BackButton' +import { RefreshButton } from 'src/components/buttons/RefreshButton' +import { SolidButton } from 'src/components/buttons/SolidButton' +import { toastToYourSuccess } from 'src/components/TxSuccessToast' +import { MAX_EXCHANGE_RATE, MIN_EXCHANGE_RATE, SIGN_OPERATION_TIMEOUT } from 'src/config/consts' +import { NativeTokenId } from 'src/config/tokens' +import { fetchBalances } from 'src/features/accounts/fetchBalances' +import { fetchOracleRates } from 'src/features/granda/fetchOracleRates' import { setFormValues } from 'src/features/granda/grandaSlice' +import { getExchangeValues } from 'src/features/granda/utils' +import { getKitToken, getTokenContract } from 'src/features/swap/contracts' +import { SwapConfirmSummary } from 'src/features/swap/SwapConfirm' import { FloatingBox } from 'src/layout/FloatingBox' +import { getAdjustedAmount } from 'src/utils/amount' +import { logger } from 'src/utils/logger' +import { asyncTimeout, PROMISE_TIMEOUT } from 'src/utils/timeout' export function ProposalConfirm() { const dispatch = useAppDispatch() + const balances = useAppSelector((s) => s.account.balances) + const { config, oracleRates, formValues } = useAppSelector((s) => s.granda) + const { fromAmount, fromTokenId, toTokenId } = formValues || {} + const { address, kit, initialised, performActions } = useContractKit() + + // Ensure invariants are met, otherwise return to form + const isConfirmValid = + fromAmount && fromTokenId && toTokenId && address && kit && config && oracleRates + useEffect(() => { + if (!isConfirmValid) { + dispatch(setFormValues(null)) + } + }, [isConfirmValid, dispatch]) + if (!isConfirmValid) return null + + const { from, to, rate, stableTokenId } = getExchangeValues( + fromAmount, + fromTokenId, + toTokenId, + config?.spread, + oracleRates + ) + const tokenBalance = balances[fromTokenId] + // Check if amount is almost equal to balance max, in which case use max + // Helps handle problems from imprecision in non-wei amount display + const finalFromAmount = getAdjustedAmount(from.weiAmount, tokenBalance) + + const onSubmit = async () => { + if (!address || !kit) { + toast.error('Kit not connected') + return + } + if (rate.value < MIN_EXCHANGE_RATE || rate.value > MAX_EXCHANGE_RATE) { + toast.error('Rate seems incorrect') + return + } + + const approvalOperation = async (k: ContractKit) => { + const tokenContract = await getTokenContract(k, fromTokenId) + const grandaContract = await k.contracts.getGrandaMento() + const approveTx = await tokenContract.increaseAllowance( + grandaContract.address, + finalFromAmount + ) + // Gas price must be set manually because contractkit pre-populate it and + // its helpers for getting gas price are only meant for stable token prices + const gasPrice = await k.web3.eth.getGasPrice() + const approveReceipt = await approveTx.sendAndWaitForReceipt({ gasPrice }) + logger.info(`Tx receipt received for approval: ${approveReceipt.transactionHash}`) + return approveReceipt.transactionHash + } + const approvalOpWithTimeout = asyncTimeout(approvalOperation, SIGN_OPERATION_TIMEOUT) + + const proposeOperation = async (k: ContractKit) => { + const sellCelo = fromTokenId === NativeTokenId.CELO + const grandaContract = await k.contracts.getGrandaMento() + const contractId = k.celoTokens.getContract(getKitToken(stableTokenId) as StableToken) + const proposeTx = await grandaContract.createExchangeProposal( + contractId, + finalFromAmount, + sellCelo + ) + const gasPrice = await k.web3.eth.getGasPrice() + const proposeReceipt = await proposeTx.sendAndWaitForReceipt({ gasPrice }) + logger.info(`Tx receipt received for swap: ${proposeReceipt.transactionHash}`) + await dispatch(fetchBalances({ address, kit: k })) + return proposeReceipt.transactionHash + } + const proposeOpWithTimeout = asyncTimeout(proposeOperation, SIGN_OPERATION_TIMEOUT) + + try { + const txHashes = (await performActions( + approvalOpWithTimeout, + proposeOpWithTimeout + )) as string[] + if (!txHashes || txHashes.length !== 2) throw new Error('Tx hashes not found') + toastToYourSuccess('Proposal Created!', txHashes[1]) + dispatch(setFormValues(null)) + } catch (err: any) { + if (err.message === PROMISE_TIMEOUT) { + toast.error('Action timed out') + } else { + toast.error('Unable to complete swap') + } + logger.error('Failed to execute swap', err) + } + } const onClickBack = () => { dispatch(setFormValues(null)) } + const onClickRefresh = () => { + if (!kit || !initialised) return + dispatch(fetchOracleRates({ kit })).catch((err) => { + toast.error('Error retrieving exchange rates') + logger.error('Failed to retrieve exchange rates', err) + }) + } + return (

Confirm Proposal

-
+ +
+ +
+ + Swap +
-
TODO show confirmation
) } diff --git a/src/features/granda/fetchOracleRates.ts b/src/features/granda/fetchOracleRates.ts index 559de24..696715d 100644 --- a/src/features/granda/fetchOracleRates.ts +++ b/src/features/granda/fetchOracleRates.ts @@ -4,7 +4,7 @@ import type { AppDispatch, AppState } from 'src/app/store' import { MAX_EXCHANGE_RATE, MIN_EXCHANGE_RATE } from 'src/config/consts' import { NativeTokenId, StableTokenIds } from 'src/config/tokens' import { OracleRates } from 'src/features/granda/types' -import { getContractKitToken } from 'src/features/swap/contracts' +import { getKitToken } from 'src/features/swap/contracts' import { SimpleExchangeRate } from 'src/features/swap/types' import { logger } from 'src/utils/logger' import { areRatesStale } from 'src/utils/time' @@ -37,7 +37,7 @@ async function _fetchOracleRates( tokenId: NativeTokenId ): Promise { logger.debug('Fetching oracle rate for:', tokenId) - const token = getContractKitToken(tokenId) + const token = getKitToken(tokenId) const stableTokenAddress = await kit.celoTokens.getAddress(token) const sortedOracles = await kit.contracts.getSortedOracles() const { rate } = await sortedOracles.medianRate(stableTokenAddress) diff --git a/src/features/swap/SwapConfirm.tsx b/src/features/swap/SwapConfirm.tsx index 9a610ab..3df8fb2 100644 --- a/src/features/swap/SwapConfirm.tsx +++ b/src/features/swap/SwapConfirm.tsx @@ -20,7 +20,7 @@ import { getExchangeContract, getTokenContract } from 'src/features/swap/contrac import { fetchExchangeRates } from 'src/features/swap/fetchExchangeRates' import { setFormValues } from 'src/features/swap/swapSlice' import { SwapFormValues } from 'src/features/swap/types' -import { getExchangeValues, getMinBuyAmount } from 'src/features/swap/utils' +import { ExchangeValues, getExchangeValues, getMinBuyAmount } from 'src/features/swap/utils' import { TokenIcon } from 'src/images/tokens/TokenIcon' import { FloatingBox } from 'src/layout/FloatingBox' import { Color } from 'src/styles/Color' @@ -59,8 +59,6 @@ export function SwapConfirm(props: Props) { const finalFromAmount = getAdjustedAmount(from.weiAmount, tokenBalance) const minBuyAmountWei = getMinBuyAmount(finalFromAmount, slippage, rate.value) const minBuyAmount = fromWeiRounded(minBuyAmountWei, true) - const fromToken = NativeTokens[fromTokenId] - const toToken = NativeTokens[toTokenId] const onSubmit = async () => { if (!address || !kit) { @@ -142,35 +140,7 @@ export function SwapConfirm(props: Props) {

Confirm Swap

-
-
-
- -
-
{fromToken.symbol}
-
{from.amount}
-
-
-
-
-
{toToken.symbol}
-
{to.amount}
-
- -
-
- -
-
-
-
- {rate.isReady ? `${rate.fromCeloValue} ${stableTokenId} : 1 CELO` : 'Loading...'} -
-
-
+
Max Slippage:
@@ -190,6 +160,50 @@ export function SwapConfirm(props: Props) { ) } +interface SwapConfirmSummaryProps { + from: ExchangeValues['from'] + to: ExchangeValues['to'] + rate: ExchangeValues['rate'] + stableTokenId: NativeTokenId +} + +export function SwapConfirmSummary({ from, to, rate, stableTokenId }: SwapConfirmSummaryProps) { + const fromToken = NativeTokens[from.token] + const toToken = NativeTokens[to.token] + + return ( +
+
+
+ +
+
{fromToken.symbol}
+
{from.amount}
+
+
+
+
+
{toToken.symbol}
+
{to.amount}
+
+ +
+
+ +
+
+
+
+ {rate.isReady ? `${rate.fromCeloValue} ${stableTokenId} : 1 CELO` : 'Loading...'} +
+
+
+ ) +} + function RightCircleArrow() { return (
diff --git a/src/features/swap/contracts.ts b/src/features/swap/contracts.ts index 9417206..2e547b9 100644 --- a/src/features/swap/contracts.ts +++ b/src/features/swap/contracts.ts @@ -33,7 +33,7 @@ export function getNativeTokenId(name: CeloContract): NativeTokenId { throw new Error(`Unsupported token contract name ${name}`) } -export function getContractKitToken(tokenId: NativeTokenId): CeloTokenType { +export function getKitToken(tokenId: NativeTokenId): CeloTokenType { switch (tokenId) { case NativeTokenId.cUSD: return StableToken.cUSD diff --git a/src/layout/AppLayout.tsx b/src/layout/AppLayout.tsx index 8c6e554..c4edbd1 100644 --- a/src/layout/AppLayout.tsx +++ b/src/layout/AppLayout.tsx @@ -1,7 +1,9 @@ +import { useContractKit } from '@celo-tools/use-contractkit' import { PropsWithChildren, useEffect } from 'react' import Modal from 'react-modal' import { Footer } from 'src/components/nav/Footer' import { Header } from 'src/components/nav/Header' +import { NULL_ADDRESS } from 'src/config/consts' import { PollingWorker } from 'src/features/polling/PollingWorker' import { HeadMeta } from 'src/layout/HeadMeta' @@ -10,12 +12,21 @@ interface Props { } export function AppLayout({ pathName, children }: PropsWithChildren) { - // Required to prevent react-modal from showing aria related error + // Prevent react-modal from showing aria related error // Note react-modal not used directly, it's part of use-contractkit useEffect(() => { Modal.setAppElement('#__next') }, []) + // Prevent web3 from spamming errors due to missing ENS on Celo + // Error: https://github.com/ChainSafe/web3.js/blob/1.x/packages/web3-eth-ens/src/ENS.js#L526 + // Related: https://github.com/ChainSafe/web3.js/issues/3787 + // Related: https://github.com/ChainSafe/web3.js/issues/3010 + const { kit } = useContractKit() + useEffect(() => { + kit.web3.eth.ens.registryAddress = NULL_ADDRESS + }, [kit]) + return ( <> From 64e405df5a49a0d861d21ac961db6f43cdc30f22 Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Sat, 13 Nov 2021 12:43:12 +0100 Subject: [PATCH 13/30] Create NetworkModal with network switching --- src/components/nav/Footer.tsx | 28 ++++++--- src/components/nav/NetworkModal.tsx | 89 +++++++++++++++++++++++++++++ src/layout/HrDivider.tsx | 2 +- 3 files changed, 109 insertions(+), 10 deletions(-) create mode 100644 src/components/nav/NetworkModal.tsx diff --git a/src/components/nav/Footer.tsx b/src/components/nav/Footer.tsx index 36f4917..7517008 100644 --- a/src/components/nav/Footer.tsx +++ b/src/components/nav/Footer.tsx @@ -1,6 +1,8 @@ import BigNumber from 'bignumber.js' import Image from 'next/image' +import { useState } from 'react' import { useAppSelector } from 'src/app/hooks' +import { NetworkModal } from 'src/components/nav/NetworkModal' import { config } from 'src/config/config' import { STALE_BLOCK_TIME } from 'src/config/consts' import Discord from 'src/images/logos/discord.svg' @@ -64,16 +66,24 @@ function BlockIndicator() { classColor = 'red-600' } + const [showNetworkModal, setShowNetworkModal] = useState(false) + return ( -
-
{summary}
-
-
-
+ <> + + setShowNetworkModal(false)} /> + ) } diff --git a/src/components/nav/NetworkModal.tsx b/src/components/nav/NetworkModal.tsx new file mode 100644 index 0000000..6eec52b --- /dev/null +++ b/src/components/nav/NetworkModal.tsx @@ -0,0 +1,89 @@ +import { Alfajores, Baklava, Mainnet, useContractKit } from '@celo-tools/use-contractkit' +import ReactModal from 'react-modal' +import { useAppSelector } from 'src/app/hooks' +import { IconButton } from 'src/components/buttons/IconButton' +import XCircle from 'src/images/icons/x-circle.svg' +import { HrDivider } from 'src/layout/HrDivider' +import { logger } from 'src/utils/logger' + +interface Props { + isOpen: boolean + close: () => void +} + +export function NetworkModal({ isOpen, close }: Props) { + const latestBlock = useAppSelector((s) => s.block.latestBlock) + const { network, updateNetwork } = useContractKit() + const allNetworks = [Mainnet, Alfajores, Baklava] + + return ( + +
+
+
+ +
+

Network Details

+
+
Connected to:
+
{network.name}
+
+
+
Block Number:
+
{latestBlock?.number ?? 'Unknown'}
+
+
+
Node Rpc Url:
+
{shortenUrl(network.rpcUrl) ?? 'Unknown'}
+
+ +

Switch Network

+
+ {allNetworks.map((n) => ( + + ))} +
+
+
+
+ ) +} + +function shortenUrl(url: string) { + try { + if (!url) return null + return new URL(url).hostname + } catch (error) { + logger.error('Error parsing url', error) + return null + } +} + +export const defaultModalStyles = { + content: { + top: '50%', + left: '50%', + right: 'auto', + bottom: 'auto', + transform: 'translate(-50%, -50%)', + border: 'unset', + background: 'unset', + padding: 'unset', + borderRadius: 10, + boxShadow: '0px 3px 4px 0px rgba(0, 0, 0, 0.15)', + }, +} diff --git a/src/layout/HrDivider.tsx b/src/layout/HrDivider.tsx index fc4aeaf..0873ceb 100644 --- a/src/layout/HrDivider.tsx +++ b/src/layout/HrDivider.tsx @@ -4,5 +4,5 @@ interface Props { export function HrDivider(props: Props) { const { classes } = props - return
+ return
} From c76edc383f2ec7b280517bb4a7a22ecf90065d5b Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Sat, 13 Nov 2021 12:51:57 +0100 Subject: [PATCH 14/30] Fix thunk error handling --- src/features/granda/ProposalConfirm.tsx | 10 +++-- src/features/granda/fetchConfig.ts | 2 + src/features/granda/fetchOracleRates.ts | 2 +- src/features/granda/fetchProposals.ts | 2 + src/features/polling/PollingWorker.tsx | 58 +++++++++++++++---------- src/features/swap/SwapConfirm.tsx | 10 +++-- src/features/swap/fetchExchangeRates.ts | 2 +- 7 files changed, 53 insertions(+), 33 deletions(-) diff --git a/src/features/granda/ProposalConfirm.tsx b/src/features/granda/ProposalConfirm.tsx index 0131f5b..fea7dea 100644 --- a/src/features/granda/ProposalConfirm.tsx +++ b/src/features/granda/ProposalConfirm.tsx @@ -116,10 +116,12 @@ export function ProposalConfirm() { const onClickRefresh = () => { if (!kit || !initialised) return - dispatch(fetchOracleRates({ kit })).catch((err) => { - toast.error('Error retrieving exchange rates') - logger.error('Failed to retrieve exchange rates', err) - }) + dispatch(fetchOracleRates({ kit })) + .unwrap() + .catch((err) => { + toast.error('Error retrieving exchange rates') + logger.error('Failed to retrieve exchange rates', err) + }) } return ( diff --git a/src/features/granda/fetchConfig.ts b/src/features/granda/fetchConfig.ts index f1bb367..b6dc4b8 100644 --- a/src/features/granda/fetchConfig.ts +++ b/src/features/granda/fetchConfig.ts @@ -5,6 +5,7 @@ import { GrandaConfig, SizeLimits } from 'src/features/granda/types' import { getNativeTokenId } from 'src/features/swap/contracts' import { isValidAddress } from 'src/utils/addresses' import { toWei } from 'src/utils/amount' +import { logger } from 'src/utils/logger' interface FetchConfigParams { kit: ContractKit @@ -18,6 +19,7 @@ export const fetchConfig = createAsyncThunk< const { kit } = params const config = thunkAPI.getState().granda.config if (!config) { + logger.debug('Fetching granda config') return _fetchConfig(kit) } else { return null diff --git a/src/features/granda/fetchOracleRates.ts b/src/features/granda/fetchOracleRates.ts index 696715d..84208df 100644 --- a/src/features/granda/fetchOracleRates.ts +++ b/src/features/granda/fetchOracleRates.ts @@ -21,6 +21,7 @@ export const fetchOracleRates = createAsyncThunk< const { kit } = params const oracleRates = thunkAPI.getState().granda.oracleRates if (areRatesStale(oracleRates)) { + logger.debug('Fetching oracle rates') const newRates: OracleRates = {} for (const tokenId of StableTokenIds) { const rate = await _fetchOracleRates(kit, tokenId) @@ -36,7 +37,6 @@ async function _fetchOracleRates( kit: ContractKit, tokenId: NativeTokenId ): Promise { - logger.debug('Fetching oracle rate for:', tokenId) const token = getKitToken(tokenId) const stableTokenAddress = await kit.celoTokens.getAddress(token) const sortedOracles = await kit.contracts.getSortedOracles() diff --git a/src/features/granda/fetchProposals.ts b/src/features/granda/fetchProposals.ts index b5202cd..0c7851c 100644 --- a/src/features/granda/fetchProposals.ts +++ b/src/features/granda/fetchProposals.ts @@ -4,6 +4,7 @@ import { createAsyncThunk } from '@reduxjs/toolkit' import type { AppDispatch, AppState } from 'src/app/store' import { GRANDA_PROPOSAL_STALE_TIME } from 'src/config/consts' import { GrandaProposal, GrandaProposalState } from 'src/features/granda/types' +import { logger } from 'src/utils/logger' import { isStale } from 'src/utils/time' interface FetchProposalsParams { @@ -18,6 +19,7 @@ export const fetchProposals = createAsyncThunk< const { kit } = params const proposalsLastUpdated = thunkAPI.getState().granda.proposalsLastUpdated if (isStale(proposalsLastUpdated, GRANDA_PROPOSAL_STALE_TIME)) { + logger.debug('Fetching granda proposals') return _fetchProposals(kit) } else { return null diff --git a/src/features/polling/PollingWorker.tsx b/src/features/polling/PollingWorker.tsx index decbe49..9c07a28 100644 --- a/src/features/polling/PollingWorker.tsx +++ b/src/features/polling/PollingWorker.tsx @@ -19,33 +19,45 @@ export function PollingWorker() { const onPoll = () => { if (!kit || !initialised) return - dispatch(fetchExchangeRates({ kit })).catch((err) => { - toast.error('Error retrieving exchange rates') - logger.error('Failed to retrieve exchange rates', err) - }) - dispatch(fetchLatestBlock({ kit })).catch((err) => { - toast.warn('Error retrieving latest block') - logger.error('Failed to retrieve latest block', err) - }) - if (isGrandaActive) { - dispatch(fetchProposals({ kit })).catch((err) => { - toast.warn('Error retrieving Granda proposals') - logger.error('Failed to retrieve granda proposals', err) - }) - dispatch(fetchConfig({ kit })).catch((err) => { - toast.warn('Error retrieving Granda config') - logger.error('Failed to retrieve granda config', err) + dispatch(fetchExchangeRates({ kit })) + .unwrap() + .catch((err) => { + toast.error('Error retrieving exchange rates') + logger.error('Failed to retrieve exchange rates', err) }) - dispatch(fetchOracleRates({ kit })).catch((err) => { - toast.warn('Error retrieving oracle rates') - logger.error('Failed to retrieve oracle rates', err) + dispatch(fetchLatestBlock({ kit })) + .unwrap() + .catch((err) => { + toast.warn('Error retrieving latest block') + logger.error('Failed to retrieve latest block', err) }) + if (isGrandaActive) { + dispatch(fetchProposals({ kit })) + .unwrap() + .catch((err) => { + toast.warn('Error retrieving Granda proposals') + logger.error('Failed to retrieve granda proposals', err) + }) + dispatch(fetchConfig({ kit })) + .unwrap() + .catch((err) => { + toast.warn('Error retrieving Granda config') + logger.error('Failed to retrieve granda config', err) + }) + dispatch(fetchOracleRates({ kit })) + .unwrap() + .catch((err) => { + toast.warn('Error retrieving oracle rates') + logger.error('Failed to retrieve oracle rates', err) + }) } if (address) { - dispatch(fetchBalances({ address, kit })).catch((err) => { - toast.error('Error retrieving account balances') - logger.error('Failed to retrieve balances', err) - }) + dispatch(fetchBalances({ address, kit })) + .unwrap() + .catch((err) => { + toast.error('Error retrieving account balances') + logger.error('Failed to retrieve balances', err) + }) } } diff --git a/src/features/swap/SwapConfirm.tsx b/src/features/swap/SwapConfirm.tsx index 3df8fb2..8409bb9 100644 --- a/src/features/swap/SwapConfirm.tsx +++ b/src/features/swap/SwapConfirm.tsx @@ -127,10 +127,12 @@ export function SwapConfirm(props: Props) { const onClickRefresh = () => { if (!kit || !initialised) return - dispatch(fetchExchangeRates({ kit })).catch((err) => { - toast.error('Error retrieving exchange rates') - logger.error('Failed to retrieve exchange rates', err) - }) + dispatch(fetchExchangeRates({ kit })) + .unwrap() + .catch((err) => { + toast.error('Error retrieving exchange rates') + logger.error('Failed to retrieve exchange rates', err) + }) } return ( diff --git a/src/features/swap/fetchExchangeRates.ts b/src/features/swap/fetchExchangeRates.ts index 17cb589..df07a66 100644 --- a/src/features/swap/fetchExchangeRates.ts +++ b/src/features/swap/fetchExchangeRates.ts @@ -22,6 +22,7 @@ export const fetchExchangeRates = createAsyncThunk< const { kit } = params const toCeloRates = thunkAPI.getState().swap.toCeloRates if (areRatesStale(toCeloRates)) { + logger.debug('Fetching exchange rates') const newRates: ToCeloRates = {} for (const tokenId of StableTokenIds) { const rate = await _fetchExchangeRates(kit, tokenId) @@ -37,7 +38,6 @@ async function _fetchExchangeRates( kit: ContractKit, tokenId: NativeTokenId ): Promise { - logger.debug('Fetching exchange rate for:', tokenId) const contract = await getExchangeContract(kit, tokenId) const spread = await contract.spread() if (spread.lt(0) || spread.gt(MAX_EXCHANGE_SPREAD)) From 9f63a3f5608086c37f02626f4803540a6b1aecc0 Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Sat, 13 Nov 2021 13:11:10 +0100 Subject: [PATCH 15/30] Fix proposal fetching and add validation --- src/features/granda/fetchConfig.ts | 2 +- src/features/granda/fetchProposals.ts | 38 ++++++++++++++++++++------- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/src/features/granda/fetchConfig.ts b/src/features/granda/fetchConfig.ts index b6dc4b8..2a1b42e 100644 --- a/src/features/granda/fetchConfig.ts +++ b/src/features/granda/fetchConfig.ts @@ -37,7 +37,7 @@ async function _fetchConfig(kit: ContractKit): Promise { if (spread > 0.05 || spread < 0) throw new Error(`Invalid spread: ${spread}`) const vetoPeriodSeconds = rawConfig.vetoPeriodSeconds.toNumber() - if (vetoPeriodSeconds > 2592000 || vetoPeriodSeconds < 86400) + if (vetoPeriodSeconds > 2592000 || vetoPeriodSeconds < 1000) throw new Error(`Invalid veto period: ${vetoPeriodSeconds}`) const maxApprovalExchangeRateChange = rawConfig.maxApprovalExchangeRateChange.toNumber() diff --git a/src/features/granda/fetchProposals.ts b/src/features/granda/fetchProposals.ts index 0c7851c..911cc95 100644 --- a/src/features/granda/fetchProposals.ts +++ b/src/features/granda/fetchProposals.ts @@ -4,6 +4,7 @@ import { createAsyncThunk } from '@reduxjs/toolkit' import type { AppDispatch, AppState } from 'src/app/store' import { GRANDA_PROPOSAL_STALE_TIME } from 'src/config/consts' import { GrandaProposal, GrandaProposalState } from 'src/features/granda/types' +import { isValidAddress } from 'src/utils/addresses' import { logger } from 'src/utils/logger' import { isStale } from 'src/utils/time' @@ -31,20 +32,37 @@ async function _fetchProposals(kit: ContractKit): Promise = {} - for (let i = 0; i < propCount; i++) { + for (let i = 1; i <= propCount; i++) { const proposal = await contract.getExchangeProposal(i) const id = proposal.id.toString() - // TODO validate proposal + const { + state, + exchanger, + stableToken, + sellAmount, + buyAmount, + sellCelo, + vetoPeriodSeconds, + approvalTimestamp, + } = proposal + if (!isValidAddress(exchanger)) throw new Error(`Invalid proposal exchanger ${exchanger}`) + if (!isValidAddress(stableToken)) throw new Error(`Invalid proposal stableToken ${stableToken}`) + if (!sellAmount || sellAmount.lte(0)) throw new Error(`Invalid sell amount ${sellAmount}`) + if (!buyAmount || buyAmount.lte(0)) throw new Error(`Invalid buy amount ${buyAmount}`) + if (!vetoPeriodSeconds || vetoPeriodSeconds.lte(0)) + throw new Error(`Invalid veto period ${vetoPeriodSeconds}`) + if (!approvalTimestamp || approvalTimestamp.lte(0)) + throw new Error(`Invalid approval time ${approvalTimestamp}`) proposals[id] = { id, - state: toGrandaProposalState(proposal.state), - exchanger: proposal.exchanger, - stableToken: proposal.stableToken, - sellAmount: proposal.sellAmount.toString(), - buyAmount: proposal.buyAmount.toString(), - sellCelo: proposal.sellCelo, - vetoPeriodSeconds: proposal.vetoPeriodSeconds.toNumber(), - approvalTimestamp: proposal.approvalTimestamp.toNumber(), + state: toGrandaProposalState(state), + exchanger, + stableToken, + sellAmount: sellAmount.toFixed(0), + buyAmount: buyAmount.toFixed(0), + sellCelo, + vetoPeriodSeconds: vetoPeriodSeconds.toNumber(), + approvalTimestamp: approvalTimestamp.toNumber(), } } return proposals From b90d293f8ef10884dfe1a08259aad55bc293a199 Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Sun, 14 Nov 2021 14:11:36 +0100 Subject: [PATCH 16/30] Implement ProposalList UI Reset state on network switch --- src/components/animation/Spinner.module.css | 1 + src/components/nav/NetworkModal.tsx | 22 ++- .../contracts.ts => config/tokenMapping.ts} | 22 ++- src/features/granda/ProposalConfirm.tsx | 6 +- src/features/granda/ProposalList.tsx | 149 ++++++++++++------ src/features/granda/ProposalView.tsx | 4 +- src/features/granda/fetchConfig.ts | 4 +- src/features/granda/fetchOracleRates.ts | 4 +- src/features/granda/fetchProposals.ts | 39 ++++- src/features/granda/grandaSlice.ts | 2 +- src/features/granda/types.ts | 2 +- src/features/polling/PollingWorker.tsx | 6 +- src/features/swap/SwapConfirm.tsx | 2 +- src/features/swap/fetchExchangeRates.ts | 2 +- src/images/icons/arrow-right-short.svg | 3 + src/images/tokens/TokenIcon.tsx | 6 +- 16 files changed, 203 insertions(+), 71 deletions(-) rename src/{features/swap/contracts.ts => config/tokenMapping.ts} (70%) create mode 100644 src/images/icons/arrow-right-short.svg diff --git a/src/components/animation/Spinner.module.css b/src/components/animation/Spinner.module.css index 864bbc8..83ca1ab 100644 --- a/src/components/animation/Spinner.module.css +++ b/src/components/animation/Spinner.module.css @@ -3,6 +3,7 @@ position: relative; width: 80px; height: 80px; + opacity: 0.8; } .spinner div { diff --git a/src/components/nav/NetworkModal.tsx b/src/components/nav/NetworkModal.tsx index 6eec52b..5ad36af 100644 --- a/src/components/nav/NetworkModal.tsx +++ b/src/components/nav/NetworkModal.tsx @@ -1,7 +1,12 @@ -import { Alfajores, Baklava, Mainnet, useContractKit } from '@celo-tools/use-contractkit' +import { Alfajores, Baklava, Mainnet, Network, useContractKit } from '@celo-tools/use-contractkit' import ReactModal from 'react-modal' -import { useAppSelector } from 'src/app/hooks' +import { useAppDispatch, useAppSelector } from 'src/app/hooks' import { IconButton } from 'src/components/buttons/IconButton' +import { reset as accountReset } from 'src/features/accounts/accountSlice' +import { reset as blockReset } from 'src/features/blocks/blockSlice' +import { resetTokenPrices } from 'src/features/chart/tokenPriceSlice' +import { reset as grandaReset } from 'src/features/granda/grandaSlice' +import { reset as swapReset } from 'src/features/swap/swapSlice' import XCircle from 'src/images/icons/x-circle.svg' import { HrDivider } from 'src/layout/HrDivider' import { logger } from 'src/utils/logger' @@ -16,6 +21,17 @@ export function NetworkModal({ isOpen, close }: Props) { const { network, updateNetwork } = useContractKit() const allNetworks = [Mainnet, Alfajores, Baklava] + const dispatch = useAppDispatch() + const switchToNetwork = (n: Network) => { + logger.debug('Resetting and switching to network', n.name) + updateNetwork(n) + dispatch(blockReset()) + dispatch(accountReset()) + dispatch(grandaReset()) + dispatch(swapReset()) + dispatch(resetTokenPrices()) + } + return ( {allNetworks.map((n) => ( + ) + })} +
+ ) +} +function NewButton({ onClickCreate }: { onClickCreate: () => void }) { return (
) } + +function proposalStateToColor(state: GrandaProposalState) { + switch (state) { + case GrandaProposalState.Proposed: + return 'border-yellow-500 text-yellow-500' + case GrandaProposalState.Approved: + case GrandaProposalState.Executed: + return 'border-green-darkest text-green-darkest' + case GrandaProposalState.Cancelled: + return 'border-red-600 text-red-600' + default: + return 'border-gray-500 text-grey-500' + } +} diff --git a/src/features/granda/ProposalView.tsx b/src/features/granda/ProposalView.tsx index 2e8cb5c..b304e8d 100644 --- a/src/features/granda/ProposalView.tsx +++ b/src/features/granda/ProposalView.tsx @@ -26,10 +26,10 @@ export function ProposalView() { } return ( - +
-

{`Proposal ${proposalId}`}

+

{`Granda Proposal ${proposalId}`}

TODO show proposal
diff --git a/src/features/granda/fetchConfig.ts b/src/features/granda/fetchConfig.ts index 2a1b42e..2191e8b 100644 --- a/src/features/granda/fetchConfig.ts +++ b/src/features/granda/fetchConfig.ts @@ -1,8 +1,8 @@ import type { ContractKit } from '@celo/contractkit' import { createAsyncThunk } from '@reduxjs/toolkit' import type { AppDispatch, AppState } from 'src/app/store' +import { kitContractToNativeToken } from 'src/config/tokenMapping' import { GrandaConfig, SizeLimits } from 'src/features/granda/types' -import { getNativeTokenId } from 'src/features/swap/contracts' import { isValidAddress } from 'src/utils/addresses' import { toWei } from 'src/utils/amount' import { logger } from 'src/utils/logger' @@ -46,7 +46,7 @@ async function _fetchConfig(kit: ContractKit): Promise { const exchangeLimits: SizeLimits = {} for (const [name, limits] of rawConfig.exchangeLimits.entries()) { - const tokenId = getNativeTokenId(name) + const tokenId = kitContractToNativeToken(name) const min = limits.minExchangeAmount const max = limits.maxExchangeAmount if (min.gt(toWei(1_000_000)) || min.lt(toWei(50_000))) diff --git a/src/features/granda/fetchOracleRates.ts b/src/features/granda/fetchOracleRates.ts index 84208df..97bd66b 100644 --- a/src/features/granda/fetchOracleRates.ts +++ b/src/features/granda/fetchOracleRates.ts @@ -2,9 +2,9 @@ import type { ContractKit } from '@celo/contractkit' import { createAsyncThunk } from '@reduxjs/toolkit' import type { AppDispatch, AppState } from 'src/app/store' import { MAX_EXCHANGE_RATE, MIN_EXCHANGE_RATE } from 'src/config/consts' +import { nativeTokenToKitToken } from 'src/config/tokenMapping' import { NativeTokenId, StableTokenIds } from 'src/config/tokens' import { OracleRates } from 'src/features/granda/types' -import { getKitToken } from 'src/features/swap/contracts' import { SimpleExchangeRate } from 'src/features/swap/types' import { logger } from 'src/utils/logger' import { areRatesStale } from 'src/utils/time' @@ -37,7 +37,7 @@ async function _fetchOracleRates( kit: ContractKit, tokenId: NativeTokenId ): Promise { - const token = getKitToken(tokenId) + const token = nativeTokenToKitToken(tokenId) const stableTokenAddress = await kit.celoTokens.getAddress(token) const sortedOracles = await kit.contracts.getSortedOracles() const { rate } = await sortedOracles.medianRate(stableTokenAddress) diff --git a/src/features/granda/fetchProposals.ts b/src/features/granda/fetchProposals.ts index 911cc95..b06e34f 100644 --- a/src/features/granda/fetchProposals.ts +++ b/src/features/granda/fetchProposals.ts @@ -1,10 +1,12 @@ -import type { ContractKit } from '@celo/contractkit' +import type { CeloTokenType, ContractKit } from '@celo/contractkit' import { ExchangeProposalState } from '@celo/contractkit/lib/wrappers/GrandaMento' import { createAsyncThunk } from '@reduxjs/toolkit' import type { AppDispatch, AppState } from 'src/app/store' import { GRANDA_PROPOSAL_STALE_TIME } from 'src/config/consts' +import { kitTokenToNativeToken } from 'src/config/tokenMapping' +import { isStableToken, NativeTokenId } from 'src/config/tokens' import { GrandaProposal, GrandaProposalState } from 'src/features/granda/types' -import { isValidAddress } from 'src/utils/addresses' +import { areAddressesEqual, isValidAddress } from 'src/utils/addresses' import { logger } from 'src/utils/logger' import { isStale } from 'src/utils/time' @@ -32,13 +34,19 @@ async function _fetchProposals(kit: ContractKit): Promise = {} + if (propCount <= 0) return proposals + + // Get token addresses to map addr in proposal to token id + const tokenToAddr = await kit.celoTokens.getAddresses() + + // Validate and transform the proposal data to local models for (let i = 1; i <= propCount; i++) { const proposal = await contract.getExchangeProposal(i) const id = proposal.id.toString() const { state, exchanger, - stableToken, + stableToken: stableTokenAddr, sellAmount, buyAmount, sellCelo, @@ -46,18 +54,23 @@ async function _fetchProposals(kit: ContractKit): Promise initialState, + reset: (state) => ({ ...initialState, isActive: state.isActive }), }, extraReducers: (builder) => { builder.addCase(fetchProposals.fulfilled, (state, action) => { diff --git a/src/features/granda/types.ts b/src/features/granda/types.ts index d434177..aaeb4ee 100644 --- a/src/features/granda/types.ts +++ b/src/features/granda/types.ts @@ -25,7 +25,7 @@ export interface GrandaProposal { id: string state: GrandaProposalState exchanger: string - stableToken: string + stableTokenId: NativeTokenId sellAmount: string buyAmount: string sellCelo: boolean diff --git a/src/features/polling/PollingWorker.tsx b/src/features/polling/PollingWorker.tsx index 9c07a28..31ac758 100644 --- a/src/features/polling/PollingWorker.tsx +++ b/src/features/polling/PollingWorker.tsx @@ -17,12 +17,14 @@ export function PollingWorker() { const dispatch = useAppDispatch() const { address, kit, initialised } = useContractKit() + // TODO debounce toast errors + const onPoll = () => { if (!kit || !initialised) return dispatch(fetchExchangeRates({ kit })) .unwrap() .catch((err) => { - toast.error('Error retrieving exchange rates') + toast.warn('Error retrieving exchange rates') logger.error('Failed to retrieve exchange rates', err) }) dispatch(fetchLatestBlock({ kit })) @@ -55,7 +57,7 @@ export function PollingWorker() { dispatch(fetchBalances({ address, kit })) .unwrap() .catch((err) => { - toast.error('Error retrieving account balances') + toast.warn('Error retrieving account balances') logger.error('Failed to retrieve balances', err) }) } diff --git a/src/features/swap/SwapConfirm.tsx b/src/features/swap/SwapConfirm.tsx index 8409bb9..f33f1da 100644 --- a/src/features/swap/SwapConfirm.tsx +++ b/src/features/swap/SwapConfirm.tsx @@ -14,9 +14,9 @@ import { MIN_EXCHANGE_RATE, SIGN_OPERATION_TIMEOUT, } from 'src/config/consts' +import { getExchangeContract, getTokenContract } from 'src/config/tokenMapping' import { NativeTokenId, NativeTokens } from 'src/config/tokens' import { fetchBalances } from 'src/features/accounts/fetchBalances' -import { getExchangeContract, getTokenContract } from 'src/features/swap/contracts' import { fetchExchangeRates } from 'src/features/swap/fetchExchangeRates' import { setFormValues } from 'src/features/swap/swapSlice' import { SwapFormValues } from 'src/features/swap/types' diff --git a/src/features/swap/fetchExchangeRates.ts b/src/features/swap/fetchExchangeRates.ts index df07a66..b68e82b 100644 --- a/src/features/swap/fetchExchangeRates.ts +++ b/src/features/swap/fetchExchangeRates.ts @@ -2,8 +2,8 @@ import type { ContractKit } from '@celo/contractkit' import { createAsyncThunk } from '@reduxjs/toolkit' import type { AppDispatch, AppState } from 'src/app/store' import { MAX_EXCHANGE_SPREAD } from 'src/config/consts' +import { getExchangeContract } from 'src/config/tokenMapping' import { NativeTokenId, StableTokenIds } from 'src/config/tokens' -import { getExchangeContract } from 'src/features/swap/contracts' import { ExchangeRate, ToCeloRates } from 'src/features/swap/types' import { logger } from 'src/utils/logger' import { areRatesStale } from 'src/utils/time' diff --git a/src/images/icons/arrow-right-short.svg b/src/images/icons/arrow-right-short.svg new file mode 100644 index 0000000..e26cfd4 --- /dev/null +++ b/src/images/icons/arrow-right-short.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/images/tokens/TokenIcon.tsx b/src/images/tokens/TokenIcon.tsx index 2e2a9f5..a26a136 100644 --- a/src/images/tokens/TokenIcon.tsx +++ b/src/images/tokens/TokenIcon.tsx @@ -7,7 +7,7 @@ import cUSDIcon from 'src/images/tokens/cUSD.svg' interface Props { token?: Token | null - size?: 's' | 'm' | 'l' + size?: 'xs' | 's' | 'm' | 'l' } function _TokenIcon({ token, size = 'm' }: Props) { @@ -56,6 +56,10 @@ function _TokenIcon({ token, size = 'm' }: Props) { } const sizeValues = { + xs: { + actualSize: '22px', + fontSize: '13px', + }, s: { actualSize: '30px', fontSize: '15px', From 8c617a9ed61cc5c56fac53550f78728dfc0a1d8b Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Sun, 14 Nov 2021 16:56:17 +0100 Subject: [PATCH 17/30] Implement proposal view screen Fix explorer url for non-mainnet networks --- src/components/TxSuccessToast.tsx | 19 ++- src/components/buttons/SolidButton.tsx | 37 ++++- src/components/nav/ConnectButton.tsx | 2 + src/components/nav/NetworkModal.tsx | 16 +- src/config/config.ts | 2 - src/features/granda/ProposalConfirm.tsx | 6 +- src/features/granda/ProposalList.tsx | 2 +- src/features/granda/ProposalView.tsx | 195 +++++++++++++++++++++++- src/features/granda/fetchProposals.ts | 5 +- src/features/swap/SwapConfirm.tsx | 8 +- src/features/swap/SwapForm.tsx | 2 +- src/styles/modals.ts | 14 ++ 12 files changed, 260 insertions(+), 48 deletions(-) create mode 100644 src/styles/modals.ts diff --git a/src/components/TxSuccessToast.tsx b/src/components/TxSuccessToast.tsx index 34434bd..1a3d327 100644 --- a/src/components/TxSuccessToast.tsx +++ b/src/components/TxSuccessToast.tsx @@ -1,16 +1,25 @@ import { toast } from 'react-toastify' import { TextLink } from 'src/components/buttons/TextLink' -import { config } from 'src/config/config' -export function toastToYourSuccess(msg: string, txHash: string) { - toast.success(, { autoClose: 15000 }) +export function toastToYourSuccess(msg: string, txHash: string, blockscoutUrl: string) { + toast.success(, { + autoClose: 15000, + }) } -export function TxSuccessToast({ msg, txHash }: { msg: string; txHash: string }) { +export function TxSuccessToast({ + msg, + txHash, + blockscoutUrl, +}: { + msg: string + txHash: string + blockscoutUrl: string +}) { return (
{msg + ' '} - + See Details
diff --git a/src/components/buttons/SolidButton.tsx b/src/components/buttons/SolidButton.tsx index 42395c9..d3e8845 100644 --- a/src/components/buttons/SolidButton.tsx +++ b/src/components/buttons/SolidButton.tsx @@ -4,7 +4,7 @@ interface ButtonProps { size?: 'xs' | 's' | 'm' | 'l' | 'xl' type?: 'submit' | 'reset' | 'button' onClick?: () => void - dark?: boolean // defaults to false + color?: 'white' | 'green' | 'red' // defaults to green classes?: string bold?: boolean disabled?: boolean @@ -14,16 +14,39 @@ interface ButtonProps { } export function SolidButton(props: PropsWithChildren) { - const { size, type, onClick, dark, classes, bold, icon, disabled, title, passThruProps } = props + const { + size, + type, + onClick, + color: _color, + classes, + bold, + icon, + disabled, + title, + passThruProps, + } = props + const color = _color ?? 'green' const base = 'flex items-center justify-center rounded-full transition-all duration-300' const sizing = sizeToClasses(size) - const colors = dark ? 'bg-green text-white' : 'bg-white text-black' - const onHover = dark ? 'hover:bg-green-dark' : 'hover:bg-gray-50' - const onDisabled = 'disabled:bg-gray-300 disabled:text-gray-300' - const onActive = dark ? 'active:bg-green-darkest' : 'active:bg-gray-100' + let baseColors, onHover, onActive + if (color === 'green') { + baseColors = 'bg-green text-white' + onHover = 'hover:bg-green-dark' + onActive = 'active:bg-green-darkest' + } else if (color === 'red') { + baseColors = 'bg-red-600 text-white' + onHover = 'hover:bg-red-500' + onActive = 'active:bg-red-400' + } else if (color === 'white') { + baseColors = 'bg-white text-black' + onHover = 'hover:bg-gray-50' + onActive = 'active:bg-gray-100' + } + const onDisabled = 'disabled:bg-gray-300 disabled:text-gray-500' const weight = bold ? 'font-semibold' : '' - const allClasses = `${base} ${sizing} ${colors} ${onHover} ${onDisabled} ${onActive} ${weight} ${classes}` + const allClasses = `${base} ${sizing} ${baseColors} ${onHover} ${onDisabled} ${onActive} ${weight} ${classes}` return (
- + Swap
diff --git a/src/features/granda/ProposalList.tsx b/src/features/granda/ProposalList.tsx index e8d3637..9cf9bd9 100644 --- a/src/features/granda/ProposalList.tsx +++ b/src/features/granda/ProposalList.tsx @@ -48,7 +48,7 @@ function EmptyList({ onClickCreate }: { onClickCreate: () => void }) {

There are no Granda Mento proposals on this network yet.

- + Create New Proposal
diff --git a/src/features/granda/ProposalView.tsx b/src/features/granda/ProposalView.tsx index b304e8d..2648ee5 100644 --- a/src/features/granda/ProposalView.tsx +++ b/src/features/granda/ProposalView.tsx @@ -1,28 +1,99 @@ import { useContractKit } from '@celo-tools/use-contractkit' -import { useEffect } from 'react' +import type { ContractKit } from '@celo/contractkit' +import BigNumber from 'bignumber.js' +import { useEffect, useState } from 'react' +import ReactModal from 'react-modal' +import { toast } from 'react-toastify' import { useAppDispatch, useAppSelector } from 'src/app/hooks' import { BackButton } from 'src/components/buttons/BackButton' +import { IconButton } from 'src/components/buttons/IconButton' import { RefreshButton } from 'src/components/buttons/RefreshButton' +import { SolidButton } from 'src/components/buttons/SolidButton' +import { TextLink } from 'src/components/buttons/TextLink' +import { toastToYourSuccess } from 'src/components/TxSuccessToast' +import { SIGN_OPERATION_TIMEOUT, WEI_PER_UNIT } from 'src/config/consts' +import { NativeTokenId } from 'src/config/tokens' +import { fetchProposals } from 'src/features/granda/fetchProposals' import { clearProposal } from 'src/features/granda/grandaSlice' +import { GrandaProposal, GrandaProposalState } from 'src/features/granda/types' +import { SwapConfirmSummary } from 'src/features/swap/SwapConfirm' +import XCircle from 'src/images/icons/x-circle.svg' import { FloatingBox } from 'src/layout/FloatingBox' +import { defaultModalStyles } from 'src/styles/modals' +import { areAddressesEqual, shortenAddress } from 'src/utils/addresses' +import { fromWei } from 'src/utils/amount' +import { logger } from 'src/utils/logger' +import { asyncTimeout, PROMISE_TIMEOUT } from 'src/utils/timeout' export function ProposalView() { - const proposalId = useAppSelector((s) => s.granda.viewProposalId) const dispatch = useAppDispatch() + const { viewProposalId: proposalId, proposals } = useAppSelector((s) => s.granda) + const proposal = proposalId ? proposals[proposalId] : null useEffect(() => { - // TODO check if proposal id is in data - if (!proposalId) dispatch(clearProposal()) - }, [proposalId, dispatch]) + if (!proposal) dispatch(clearProposal()) + }, [proposal, dispatch]) const onClickBack = () => { dispatch(clearProposal()) } - const { kit, initialised } = useContractKit() + const { address, kit, initialised, network, performActions } = useContractKit() + const onClickRefresh = () => { if (!kit || !initialised) return - console.log('TODO') + dispatch(fetchProposals({ kit, force: true })) + .unwrap() + .catch((err) => { + toast.warn('Error retrieving Granda proposals') + logger.error('Failed to retrieve granda proposals', err) + }) + } + + const [showConfModal, setShowConfModal] = useState(false) + + if (!proposal) return null + + const isCancellable = + kit && + initialised && + address && + areAddressesEqual(address, proposal.exchanger) && + proposal.state === GrandaProposalState.Proposed + + const onClickCancel = () => { + setShowConfModal(true) + } + + const onSubmitCancel = async () => { + setShowConfModal(false) + if (!address || !kit) { + toast.error('Kit not connected') + return + } + const cancelOperation = async (k: ContractKit) => { + const grandaContract = await k.contracts.getGrandaMento() + const cancelTx = await grandaContract.cancelExchangeProposal(proposal.id) + // Gas price must be set manually because contractkit pre-populate it and + // its helpers for getting gas price are only meant for stable token prices + const gasPrice = await k.web3.eth.getGasPrice() + const cancelReceipt = await cancelTx.sendAndWaitForReceipt({ gasPrice }) + logger.info(`Tx receipt received for approval: ${cancelReceipt.transactionHash}`) + return cancelReceipt.transactionHash + } + const cancelOpWithTimeout = asyncTimeout(cancelOperation, SIGN_OPERATION_TIMEOUT) + try { + const txHashes = (await performActions(cancelOpWithTimeout)) as string[] + if (!txHashes || txHashes.length !== 1) throw new Error('Tx hashes not found') + toastToYourSuccess('Proposal cancelled', txHashes[1], network.explorer) + } catch (err: any) { + if (err.message === PROMISE_TIMEOUT) { + toast.error('Action timed out') + } else { + toast.error('Unable to cancel proposal') + } + logger.error('Failed to cancel proposal', err) + } } return ( @@ -32,7 +103,115 @@ export function ProposalView() {

{`Granda Proposal ${proposalId}`}

-
TODO show proposal
+ +
+ + Cancel + +
+ setShowConfModal(false)} + submit={onSubmitCancel} + /> ) } + +function SwapDetails({ + proposal: p, + blockscoutUrl, +}: { + proposal: GrandaProposal + blockscoutUrl: string +}) { + const fromToken = p.sellCelo ? NativeTokenId.CELO : p.stableTokenId + const toToken = p.sellCelo ? p.stableTokenId : NativeTokenId.CELO + const fromAmount = new BigNumber(fromWei(p.sellAmount)).integerValue().toFixed(0) + const toAmount = new BigNumber(fromWei(p.buyAmount)).integerValue().toFixed(0) + const effectiveRate = new BigNumber(p.buyAmount).div(p.sellAmount).toNumber() + const fromCeloRate = p.sellCelo ? effectiveRate : 1 / effectiveRate + + const from = { + amount: fromAmount, + weiAmount: p.sellAmount, + token: fromToken, + } + const to = { + amount: toAmount, + weiAmount: p.buyAmount, + token: toToken, + } + const rate = { + value: effectiveRate, + weiValue: '', // Not needed here + fromCeloValue: fromCeloRate.toFixed(2), + fromCeloWeiValue: '', // Not needed here + weiBasis: WEI_PER_UNIT, + lastUpdated: Date.now(), + isReady: true, + } + + return ( +
+ +
+
+
Proposer:
+
+ + {shortenAddress(p.exchanger, true)} + +
+
+
+
Approval time:
+
{new Date(p.approvalTimestamp * 1000).toLocaleString()}
+
+
+
Proposal Status:
+
{p.state.toUpperCase()}
+
+
+
+ ) +} + +interface Props { + isOpen: boolean + close: () => void + submit: () => void +} + +function CancelConfirmationModal({ isOpen, close, submit }: Props) { + return ( + +
+
+
+ +
+

Confirm Cancellation

+

+ Are you sure you want to cancel this proposal? +

+

This will refund your exchange amount.

+
+ + Cancel + +
+
+
+
+ ) +} diff --git a/src/features/granda/fetchProposals.ts b/src/features/granda/fetchProposals.ts index b06e34f..e4e79c3 100644 --- a/src/features/granda/fetchProposals.ts +++ b/src/features/granda/fetchProposals.ts @@ -12,6 +12,7 @@ import { isStale } from 'src/utils/time' interface FetchProposalsParams { kit: ContractKit + force?: boolean } export const fetchProposals = createAsyncThunk< @@ -19,9 +20,9 @@ export const fetchProposals = createAsyncThunk< FetchProposalsParams, { dispatch: AppDispatch; state: AppState } >('granda/fetchProposals', async (params, thunkAPI) => { - const { kit } = params + const { kit, force } = params const proposalsLastUpdated = thunkAPI.getState().granda.proposalsLastUpdated - if (isStale(proposalsLastUpdated, GRANDA_PROPOSAL_STALE_TIME)) { + if (isStale(proposalsLastUpdated, GRANDA_PROPOSAL_STALE_TIME) || force) { logger.debug('Fetching granda proposals') return _fetchProposals(kit) } else { diff --git a/src/features/swap/SwapConfirm.tsx b/src/features/swap/SwapConfirm.tsx index f33f1da..dea96b3 100644 --- a/src/features/swap/SwapConfirm.tsx +++ b/src/features/swap/SwapConfirm.tsx @@ -1,5 +1,5 @@ import { useContractKit } from '@celo-tools/use-contractkit' -import { ContractKit } from '@celo/contractkit' +import type { ContractKit } from '@celo/contractkit' import BigNumber from 'bignumber.js' import { useEffect } from 'react' import { toast } from 'react-toastify' @@ -37,7 +37,7 @@ export function SwapConfirm(props: Props) { const toCeloRates = useAppSelector((s) => s.swap.toCeloRates) const balances = useAppSelector((s) => s.account.balances) const dispatch = useAppDispatch() - const { address, kit, initialised, performActions } = useContractKit() + const { address, kit, initialised, network, performActions } = useContractKit() // Ensure invariants are met, otherwise return to swap form const isConfirmValid = fromAmount && fromTokenId && toTokenId && address && kit @@ -109,7 +109,7 @@ export function SwapConfirm(props: Props) { exchangeOpWithTimeout )) as string[] if (!txHashes || txHashes.length !== 2) throw new Error('Tx hashes not found') - toastToYourSuccess('Swap Complete!', txHashes[1]) + toastToYourSuccess('Swap Complete!', txHashes[1], network.explorer) dispatch(setFormValues(null)) } catch (err: any) { if (err.message === PROMISE_TIMEOUT) { @@ -154,7 +154,7 @@ export function SwapConfirm(props: Props) {
- + Swap
diff --git a/src/features/swap/SwapForm.tsx b/src/features/swap/SwapForm.tsx index 5547163..b14b7e3 100644 --- a/src/features/swap/SwapForm.tsx +++ b/src/features/swap/SwapForm.tsx @@ -256,7 +256,7 @@ function SubmitButton({ address, connect }: ButtonProps) { useTimeout(clearErrors, 3000) return ( - + {text} ) diff --git a/src/styles/modals.ts b/src/styles/modals.ts new file mode 100644 index 0000000..2a2aa77 --- /dev/null +++ b/src/styles/modals.ts @@ -0,0 +1,14 @@ +export const defaultModalStyles = { + content: { + top: '50%', + left: '50%', + right: 'auto', + bottom: 'auto', + transform: 'translate(-50%, -50%)', + border: 'unset', + background: 'unset', + padding: 'unset', + borderRadius: 10, + boxShadow: '0px 3px 4px 0px rgba(0, 0, 0, 0.15)', + }, +} From 6765b9589243a7630b0334839cc343e2df3a0ec1 Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Sun, 14 Nov 2021 17:29:30 +0100 Subject: [PATCH 18/30] Attempt to fix twitter image preview Attempt token icon prefetch --- src/features/granda/ProposalView.tsx | 5 +++++ src/images/tokens/TokenIcon.tsx | 10 +++++++++- src/pages/_document.tsx | 6 +++--- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/features/granda/ProposalView.tsx b/src/features/granda/ProposalView.tsx index 2648ee5..dfb6ca1 100644 --- a/src/features/granda/ProposalView.tsx +++ b/src/features/granda/ProposalView.tsx @@ -86,6 +86,11 @@ export function ProposalView() { const txHashes = (await performActions(cancelOpWithTimeout)) as string[] if (!txHashes || txHashes.length !== 1) throw new Error('Tx hashes not found') toastToYourSuccess('Proposal cancelled', txHashes[1], network.explorer) + dispatch(fetchProposals({ kit, force: true })) + .unwrap() + .catch((err) => { + logger.error('Failed to retrieve proposals after cancel', err) + }) } catch (err: any) { if (err.message === PROMISE_TIMEOUT) { toast.error('Action timed out') diff --git a/src/images/tokens/TokenIcon.tsx b/src/images/tokens/TokenIcon.tsx index a26a136..6da52b7 100644 --- a/src/images/tokens/TokenIcon.tsx +++ b/src/images/tokens/TokenIcon.tsx @@ -19,7 +19,15 @@ function _TokenIcon({ token, size = 'm' }: Props) { const { actualSize, fontSize } = sizeValues[size] if (token && imgSrc) { - return {token.symbol} + return ( + {token.symbol} + ) } if (token) { diff --git a/src/pages/_document.tsx b/src/pages/_document.tsx index 6e03a94..a843716 100644 --- a/src/pages/_document.tsx +++ b/src/pages/_document.tsx @@ -28,12 +28,12 @@ class MyDocument extends Document { - + + - - +
From aefc8f8835dbcd6b5390729b750d186824fd78ba Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Sun, 14 Nov 2021 17:44:06 +0100 Subject: [PATCH 19/30] Upgrade some deps to latest, including Next12 --- next.config.js | 2 +- package.json | 30 +- tsconfig.json | 17 +- yarn.lock | 1213 ++++++++++++++++++++++-------------------------- 4 files changed, 576 insertions(+), 686 deletions(-) diff --git a/next.config.js b/next.config.js index 75d08e3..aad7cb9 100644 --- a/next.config.js +++ b/next.config.js @@ -9,7 +9,7 @@ module.exports = { child_process: false, readline: false, } - config.plugins.push(new webpack.IgnorePlugin(/^electron$/)) + config.plugins.push(new webpack.IgnorePlugin({ resourceRegExp: /^electron$/ })) return config }, diff --git a/package.json b/package.json index 1fbcee8..d43191a 100644 --- a/package.json +++ b/package.json @@ -32,38 +32,38 @@ "bignumber.js": "^9.0.1", "formik": "^2.2.9", "frappe-charts": "^1.6.2", - "next": "11.1.2", + "next": "12.0.3", "next-persist": "^1.2.4", "react": "^17.0.2", "react-accessible-dropdown-menu-hook": "^3.1.0", "react-dom": "^17.0.2", - "react-redux": "^7.2.5", + "react-redux": "^7.2.6", "react-select": "4.3.1", - "react-toastify": "^8.0.3" + "react-toastify": "^8.1.0" }, "devDependencies": { - "@testing-library/jest-dom": "^5.0.0", + "@testing-library/jest-dom": "^5.15.0", "@testing-library/react": "^12.1.2", - "@testing-library/user-event": "^13.4.1", + "@testing-library/user-event": "^13.5.0", "@types/jest": "^27.0.2", - "@types/node": "16.11.1", - "@types/react": "17.0.30", - "@types/react-dom": "17.0.9", - "@types/react-redux": "7.1.19", + "@types/node": "16.11.7", + "@types/react": "17.0.34", + "@types/react-dom": "17.0.11", + "@types/react-redux": "7.1.20", "@types/react-select": "4.0.18", "@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/parser": "^5.0.0", - "autoprefixer": "^10.3.7", + "autoprefixer": "^10.4.0", "eslint": "7.32.0", - "eslint-config-next": "^11.1.2", + "eslint-config-next": "^12.0.3", "eslint-config-prettier": "^8.3.0", - "jest": "^27.2.5", + "jest": "^27.3.1", "jest-css-modules-transform": "^4.3.0", - "postcss": "^8.3.9", + "postcss": "^8.3.11", "prettier": "^2.4.1", - "tailwindcss": "^2.2.17", + "tailwindcss": "^2.2.19", "ts-jest": "^27.0.7", - "ts-node": "^10.3.0", + "ts-node": "^10.4.0", "typescript": "4.4.4" }, "resolutions": { diff --git a/tsconfig.json b/tsconfig.json index b63f99a..0849c28 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,8 +5,13 @@ "baseUrl": ".", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, + "incremental": true, "isolatedModules": true, - "lib": ["dom", "dom.iterable", "esnext"], + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], "module": "esnext", "moduleResolution": "node", "noEmit": true, @@ -18,6 +23,12 @@ "strict": true, "target": "es2020" }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], - "exclude": ["node_modules"] + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx" + ], + "exclude": [ + "node_modules" + ] } diff --git a/yarn.lock b/yarn.lock index a349bae..ebe4216 100644 --- a/yarn.lock +++ b/yarn.lock @@ -292,20 +292,27 @@ core-js-pure "^3.15.0" regenerator-runtime "^0.13.4" -"@babel/runtime@7.15.3": - version "7.15.3" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.15.3.tgz#2e1c2880ca118e5b2f9988322bd8a7656a32502b" - integrity sha512-OvwMLqNXkCXSz1kSm58sEsNuhqOx/fKpnUnKnFB5v8uDda5bLNEHNgKPvhDN6IU0LDcnHQ90LlJ0Q6jnyBSIBA== +"@babel/runtime@7.15.4": + version "7.15.4" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.15.4.tgz#fd17d16bfdf878e6dd02d19753a39fa8a8d9c84a" + integrity sha512-99catp6bHCaxr4sJ/DbTGgHS4+Rs2RVd2g7iOap6SLGPDknRK9ztKNsE/Fg6QhSeh1FGE5f6gHGQmvvn3I3xhw== dependencies: regenerator-runtime "^0.13.4" -"@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": version "7.14.6" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.6.tgz#535203bc0892efc7dec60bdc27b2ecf6e409062d" integrity sha512-/PCB2uJ7oM44tz8YhC4Z/6PeOKXp4K588f+5M3clr1M4zbqztlo0XEfJ2LEzj/FgwfgGcIdl8n7YYjTCI0BYwg== dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.15.4": + version "7.16.3" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.3.tgz#b86f0db02a04187a3c17caa77de69840165d42d5" + integrity sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/template@^7.14.5", "@babel/template@^7.3.3": version "7.14.5" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.14.5.tgz#a9bc9d8b33354ff6e55a9c60d1109200a68974f4" @@ -958,27 +965,27 @@ resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== -"@jest/console@^27.2.5": - version "27.2.5" - resolved "https://registry.yarnpkg.com/@jest/console/-/console-27.2.5.tgz#bddbf8d41c191f17b52bf0c9e6c0d18605e35d6e" - integrity sha512-smtlRF9vNKorRMCUtJ+yllIoiY8oFmfFG7xlzsAE76nKEwXNhjPOJIsc7Dv+AUitVt76t+KjIpUP9m98Crn2LQ== +"@jest/console@^27.3.1": + version "27.3.1" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-27.3.1.tgz#e8ea3a475d3f8162f23d69efbfaa9cbe486bee93" + integrity sha512-RkFNWmv0iui+qsOr/29q9dyfKTTT5DCuP31kUwg7rmOKPT/ozLeGLKJKVIiOfbiKyleUZKIrHwhmiZWVe8IMdw== dependencies: "@jest/types" "^27.2.5" "@types/node" "*" chalk "^4.0.0" - jest-message-util "^27.2.5" - jest-util "^27.2.5" + jest-message-util "^27.3.1" + jest-util "^27.3.1" slash "^3.0.0" -"@jest/core@^27.2.5": - version "27.2.5" - resolved "https://registry.yarnpkg.com/@jest/core/-/core-27.2.5.tgz#854c314708cee0d892ac4f531b9129f00a21ee69" - integrity sha512-VR7mQ+jykHN4WO3OvusRJMk4xCa2MFLipMS+43fpcRGaYrN1KwMATfVEXif7ccgFKYGy5D1TVXTNE4mGq/KMMA== +"@jest/core@^27.3.1": + version "27.3.1" + resolved "https://registry.yarnpkg.com/@jest/core/-/core-27.3.1.tgz#04992ef1b58b17c459afb87ab56d81e63d386925" + integrity sha512-DMNE90RR5QKx0EA+wqe3/TNEwiRpOkhshKNxtLxd4rt3IZpCt+RSL+FoJsGeblRZmqdK4upHA/mKKGPPRAifhg== dependencies: - "@jest/console" "^27.2.5" - "@jest/reporters" "^27.2.5" - "@jest/test-result" "^27.2.5" - "@jest/transform" "^27.2.5" + "@jest/console" "^27.3.1" + "@jest/reporters" "^27.3.1" + "@jest/test-result" "^27.3.1" + "@jest/transform" "^27.3.1" "@jest/types" "^27.2.5" "@types/node" "*" ansi-escapes "^4.2.1" @@ -986,64 +993,64 @@ emittery "^0.8.1" exit "^0.1.2" graceful-fs "^4.2.4" - jest-changed-files "^27.2.5" - jest-config "^27.2.5" - jest-haste-map "^27.2.5" - jest-message-util "^27.2.5" + jest-changed-files "^27.3.0" + jest-config "^27.3.1" + jest-haste-map "^27.3.1" + jest-message-util "^27.3.1" jest-regex-util "^27.0.6" - jest-resolve "^27.2.5" - jest-resolve-dependencies "^27.2.5" - jest-runner "^27.2.5" - jest-runtime "^27.2.5" - jest-snapshot "^27.2.5" - jest-util "^27.2.5" - jest-validate "^27.2.5" - jest-watcher "^27.2.5" + jest-resolve "^27.3.1" + jest-resolve-dependencies "^27.3.1" + jest-runner "^27.3.1" + jest-runtime "^27.3.1" + jest-snapshot "^27.3.1" + jest-util "^27.3.1" + jest-validate "^27.3.1" + jest-watcher "^27.3.1" micromatch "^4.0.4" rimraf "^3.0.0" slash "^3.0.0" strip-ansi "^6.0.0" -"@jest/environment@^27.2.5": - version "27.2.5" - resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-27.2.5.tgz#b85517ccfcec55690c82c56f5a01a3b30c5e3c84" - integrity sha512-XvUW3q6OUF+54SYFCgbbfCd/BKTwm5b2MGLoc2jINXQLKQDTCS2P2IrpPOtQ08WWZDGzbhAzVhOYta3J2arubg== +"@jest/environment@^27.3.1": + version "27.3.1" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-27.3.1.tgz#2182defbce8d385fd51c5e7c7050f510bd4c86b1" + integrity sha512-BCKCj4mOVLme6Tanoyc9k0ultp3pnmuyHw73UHRPeeZxirsU/7E3HC4le/VDb/SMzE1JcPnto+XBKFOcoiJzVw== dependencies: - "@jest/fake-timers" "^27.2.5" + "@jest/fake-timers" "^27.3.1" "@jest/types" "^27.2.5" "@types/node" "*" - jest-mock "^27.2.5" + jest-mock "^27.3.0" -"@jest/fake-timers@^27.2.5": - version "27.2.5" - resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-27.2.5.tgz#0c7e5762d7bfe6e269e7b49279b097a52a42f0a0" - integrity sha512-ZGUb6jg7BgwY+nmO0TW10bc7z7Hl2G/UTAvmxEyZ/GgNFoa31tY9/cgXmqcxnnZ7o5Xs7RAOz3G1SKIj8IVDlg== +"@jest/fake-timers@^27.3.1": + version "27.3.1" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-27.3.1.tgz#1fad860ee9b13034762cdb94266e95609dfce641" + integrity sha512-M3ZFgwwlqJtWZ+QkBG5NmC23A9w+A6ZxNsO5nJxJsKYt4yguBd3i8TpjQz5NfCX91nEve1KqD9RA2Q+Q1uWqoA== dependencies: "@jest/types" "^27.2.5" "@sinonjs/fake-timers" "^8.0.1" "@types/node" "*" - jest-message-util "^27.2.5" - jest-mock "^27.2.5" - jest-util "^27.2.5" + jest-message-util "^27.3.1" + jest-mock "^27.3.0" + jest-util "^27.3.1" -"@jest/globals@^27.2.5": - version "27.2.5" - resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-27.2.5.tgz#4115538f98ed6cee4051a90fdbd0854062902099" - integrity sha512-naRI537GM+enFVJQs6DcwGYPn/0vgJNb06zGVbzXfDfe/epDPV73hP1vqO37PqSKDeOXM2KInr6ymYbL1HTP7g== +"@jest/globals@^27.3.1": + version "27.3.1" + resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-27.3.1.tgz#ce1dfb03d379237a9da6c1b99ecfaca1922a5f9e" + integrity sha512-Q651FWiWQAIFiN+zS51xqhdZ8g9b88nGCobC87argAxA7nMfNQq0Q0i9zTfQYgLa6qFXk2cGANEqfK051CZ8Pg== dependencies: - "@jest/environment" "^27.2.5" + "@jest/environment" "^27.3.1" "@jest/types" "^27.2.5" - expect "^27.2.5" + expect "^27.3.1" -"@jest/reporters@^27.2.5": - version "27.2.5" - resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-27.2.5.tgz#65198ed1f3f4449e3f656129764dc6c5bb27ebe3" - integrity sha512-zYuR9fap3Q3mxQ454VWF8I6jYHErh368NwcKHWO2uy2fwByqBzRHkf9j2ekMDM7PaSTWcLBSZyd7NNxR1iHxzQ== +"@jest/reporters@^27.3.1": + version "27.3.1" + resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-27.3.1.tgz#28b5c1f5789481e23788048fa822ed15486430b9" + integrity sha512-m2YxPmL9Qn1emFVgZGEiMwDntDxRRQ2D58tiDQlwYTg5GvbFOKseYCcHtn0WsI8CG4vzPglo3nqbOiT8ySBT/w== dependencies: "@bcoe/v8-coverage" "^0.2.3" - "@jest/console" "^27.2.5" - "@jest/test-result" "^27.2.5" - "@jest/transform" "^27.2.5" + "@jest/console" "^27.3.1" + "@jest/test-result" "^27.3.1" + "@jest/transform" "^27.3.1" "@jest/types" "^27.2.5" "@types/node" "*" chalk "^4.0.0" @@ -1056,10 +1063,10 @@ istanbul-lib-report "^3.0.0" istanbul-lib-source-maps "^4.0.0" istanbul-reports "^3.0.2" - jest-haste-map "^27.2.5" - jest-resolve "^27.2.5" - jest-util "^27.2.5" - jest-worker "^27.2.5" + jest-haste-map "^27.3.1" + jest-resolve "^27.3.1" + jest-util "^27.3.1" + jest-worker "^27.3.1" slash "^3.0.0" source-map "^0.6.0" string-length "^4.0.1" @@ -1075,30 +1082,30 @@ graceful-fs "^4.2.4" source-map "^0.6.0" -"@jest/test-result@^27.2.5": - version "27.2.5" - resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-27.2.5.tgz#e9f73cf6cd5e2cc6eb3105339248dea211f9320e" - integrity sha512-ub7j3BrddxZ0BdSnM5JCF6cRZJ/7j3wgdX0+Dtwhw2Po+HKsELCiXUTvh+mgS4/89mpnU1CPhZxe2mTvuLPJJg== +"@jest/test-result@^27.3.1": + version "27.3.1" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-27.3.1.tgz#89adee8b771877c69b3b8d59f52f29dccc300194" + integrity sha512-mLn6Thm+w2yl0opM8J/QnPTqrfS4FoXsXF2WIWJb2O/GBSyResL71BRuMYbYRsGt7ELwS5JGcEcGb52BNrumgg== dependencies: - "@jest/console" "^27.2.5" + "@jest/console" "^27.3.1" "@jest/types" "^27.2.5" "@types/istanbul-lib-coverage" "^2.0.0" collect-v8-coverage "^1.0.0" -"@jest/test-sequencer@^27.2.5": - version "27.2.5" - resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-27.2.5.tgz#ed5ae91c00e623fb719111d58e380395e16cefbb" - integrity sha512-8j8fHZRfnjbbdMitMAGFKaBZ6YqvFRFJlMJzcy3v75edTOqc7RY65S9JpMY6wT260zAcL2sTQRga/P4PglCu3Q== +"@jest/test-sequencer@^27.3.1": + version "27.3.1" + resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-27.3.1.tgz#4b3bde2dbb05ee74afdae608cf0768e3354683b1" + integrity sha512-siySLo07IMEdSjA4fqEnxfIX8lB/lWYsBPwNFtkOvsFQvmBrL3yj3k3uFNZv/JDyApTakRpxbKLJ3CT8UGVCrA== dependencies: - "@jest/test-result" "^27.2.5" + "@jest/test-result" "^27.3.1" graceful-fs "^4.2.4" - jest-haste-map "^27.2.5" - jest-runtime "^27.2.5" + jest-haste-map "^27.3.1" + jest-runtime "^27.3.1" -"@jest/transform@^27.2.5": - version "27.2.5" - resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-27.2.5.tgz#02b08862a56dbedddf0ba3c2eae41e049a250e29" - integrity sha512-29lRtAHHYGALbZOx343v0zKmdOg4Sb0rsA1uSv0818bvwRhs3TyElOmTVXlrw0v1ZTqXJCAH/cmoDXimBhQOJQ== +"@jest/transform@^27.3.1": + version "27.3.1" + resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-27.3.1.tgz#ff80eafbeabe811e9025e4b6f452126718455220" + integrity sha512-3fSvQ02kuvjOI1C1ssqMVBKJpZf6nwoCiSu00zAKh5nrp3SptNtZy/8s5deayHnqxhjD9CWDJ+yqQwuQ0ZafXQ== dependencies: "@babel/core" "^7.1.0" "@jest/types" "^27.2.5" @@ -1107,9 +1114,9 @@ convert-source-map "^1.4.0" fast-json-stable-stringify "^2.0.0" graceful-fs "^4.2.4" - jest-haste-map "^27.2.5" + jest-haste-map "^27.3.1" jest-regex-util "^27.0.6" - jest-util "^27.2.5" + jest-util "^27.3.1" micromatch "^4.0.4" pirates "^4.0.1" slash "^3.0.0" @@ -1222,32 +1229,32 @@ dependencies: mersenne-twister "^1.1.0" -"@napi-rs/triples@^1.0.3": +"@napi-rs/triples@1.0.3": version "1.0.3" resolved "https://registry.yarnpkg.com/@napi-rs/triples/-/triples-1.0.3.tgz#76d6d0c3f4d16013c61e45dfca5ff1e6c31ae53c" integrity sha512-jDJTpta+P4p1NZTFVLHJ/TLFVYVcOqv6l8xwOeBKNPMgY/zDYH/YH7SJbvrr/h1RcS9GzbPcLKGzpuK9cV56UA== -"@next/env@11.1.2": - version "11.1.2" - resolved "https://registry.yarnpkg.com/@next/env/-/env-11.1.2.tgz#27996efbbc54c5f949f5e8c0a156e3aa48369b99" - integrity sha512-+fteyVdQ7C/OoulfcF6vd1Yk0FEli4453gr8kSFbU8sKseNSizYq6df5MKz/AjwLptsxrUeIkgBdAzbziyJ3mA== +"@next/env@12.0.3": + version "12.0.3" + resolved "https://registry.yarnpkg.com/@next/env/-/env-12.0.3.tgz#e676b4d1454d8b6be433a348e99f2b8276ab6cd7" + integrity sha512-QcdlpcwIH9dYcVlNAU+gXaqHA/omskbRlb+R3vN7LlB2EgLt+9WQwbokcHOsNyt4pI7kDM67W4tr9l7dWnlGdQ== -"@next/eslint-plugin-next@11.1.2": - version "11.1.2" - resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-11.1.2.tgz#f26cf90bcb6cd2e4645e2ba253bbc9aaaa43a170" - integrity sha512-cN+ojHRsufr9Yz0rtvjv8WI5En0RPZRJnt0y16Ha7DD+0n473evz8i1ETEJHmOLeR7iPJR0zxRrxeTN/bJMOjg== +"@next/eslint-plugin-next@12.0.3": + version "12.0.3" + resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-12.0.3.tgz#3945c251d551bacc3712d4a18d6ca56d2938f175" + integrity sha512-P7i+bMypneQcoRN+CX79xssvvIJCaw7Fndzbe7/lB0+LyRbVvGVyMUsFmLLbSxtZq4hvFMJ1p8wML/gsulMZWQ== dependencies: glob "7.1.7" -"@next/polyfill-module@11.1.2": - version "11.1.2" - resolved "https://registry.yarnpkg.com/@next/polyfill-module/-/polyfill-module-11.1.2.tgz#1fe92c364fdc81add775a16c678f5057c6aace98" - integrity sha512-xZmixqADM3xxtqBV0TpAwSFzWJP0MOQzRfzItHXf1LdQHWb0yofHHC+7eOrPFic8+ZGz5y7BdPkkgR1S25OymA== +"@next/polyfill-module@12.0.3": + version "12.0.3" + resolved "https://registry.yarnpkg.com/@next/polyfill-module/-/polyfill-module-12.0.3.tgz#4217e5284762124bf9fe2505622c4de89998f7a2" + integrity sha512-fgjVjdCk0Jq627d/N33oQIJjWrcKtzw6Dfa2PfypoIJ35/xFIKgs6mPyvq8cg3Ao5b7dEn9+Rw45PGjlY5e7JA== -"@next/react-dev-overlay@11.1.2": - version "11.1.2" - resolved "https://registry.yarnpkg.com/@next/react-dev-overlay/-/react-dev-overlay-11.1.2.tgz#73795dc5454b7af168bac93df7099965ebb603be" - integrity sha512-rDF/mGY2NC69mMg2vDqzVpCOlWqnwPUXB2zkARhvknUHyS6QJphPYv9ozoPJuoT/QBs49JJd9KWaAzVBvq920A== +"@next/react-dev-overlay@12.0.3": + version "12.0.3" + resolved "https://registry.yarnpkg.com/@next/react-dev-overlay/-/react-dev-overlay-12.0.3.tgz#d85a609bf7d75eb190940d0fc64eff94c0e4a478" + integrity sha512-gHfDEVHFeTUpQMcyytzvkuOu+5DQXjXbCbQHuavFftYrlHqXfzYFsa+wERff+g4/0IzEvcYVp3F4gdmynWfUog== dependencies: "@babel/code-frame" "7.12.11" anser "1.4.9" @@ -1256,42 +1263,70 @@ css.escape "1.5.1" data-uri-to-buffer "3.0.1" platform "1.3.6" - shell-quote "1.7.2" + shell-quote "1.7.3" source-map "0.8.0-beta.0" stacktrace-parser "0.1.10" - strip-ansi "6.0.0" - -"@next/react-refresh-utils@11.1.2": - version "11.1.2" - resolved "https://registry.yarnpkg.com/@next/react-refresh-utils/-/react-refresh-utils-11.1.2.tgz#44ea40d8e773e4b77bad85e24f6ac041d5e4b4a5" - integrity sha512-hsoJmPfhVqjZ8w4IFzoo8SyECVnN+8WMnImTbTKrRUHOVJcYMmKLL7xf7T0ft00tWwAl/3f3Q3poWIN2Ueql/Q== - -"@next/swc-darwin-arm64@11.1.2": - version "11.1.2" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-11.1.2.tgz#93226c38db488c4b62b30a53b530e87c969b8251" - integrity sha512-hZuwOlGOwBZADA8EyDYyjx3+4JGIGjSHDHWrmpI7g5rFmQNltjlbaefAbiU5Kk7j3BUSDwt30quJRFv3nyJQ0w== - -"@next/swc-darwin-x64@11.1.2": - version "11.1.2" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-11.1.2.tgz#792003989f560c00677b5daeff360b35b510db83" - integrity sha512-PGOp0E1GisU+EJJlsmJVGE+aPYD0Uh7zqgsrpD3F/Y3766Ptfbe1lEPPWnRDl+OzSSrSrX1lkyM/Jlmh5OwNvA== - -"@next/swc-linux-x64-gnu@11.1.2": - version "11.1.2" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-11.1.2.tgz#8216b2ae1f21f0112958735c39dd861088108f37" - integrity sha512-YcDHTJjn/8RqvyJVB6pvEKXihDcdrOwga3GfMv/QtVeLphTouY4BIcEUfrG5+26Nf37MP1ywN3RRl1TxpurAsQ== - -"@next/swc-win32-x64-msvc@11.1.2": - version "11.1.2" - resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-11.1.2.tgz#e15824405df137129918205e43cb5e9339589745" - integrity sha512-e/pIKVdB+tGQYa1cW3sAeHm8gzEri/HYLZHT4WZojrUxgWXqx8pk7S7Xs47uBcFTqBDRvK3EcQpPLf3XdVsDdg== - -"@node-rs/helper@1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@node-rs/helper/-/helper-1.2.1.tgz#e079b05f21ff4329d82c4e1f71c0290e4ecdc70c" - integrity sha512-R5wEmm8nbuQU0YGGmYVjEc0OHtYsuXdpRG+Ut/3wZ9XAvQWyThN08bTh2cBJgoZxHQUPtvRfeQuxcAgLuiBISg== - dependencies: - "@napi-rs/triples" "^1.0.3" + strip-ansi "6.0.1" + +"@next/react-refresh-utils@12.0.3": + version "12.0.3" + resolved "https://registry.yarnpkg.com/@next/react-refresh-utils/-/react-refresh-utils-12.0.3.tgz#1389b0370e258634432d6dd78f889c09a8328e10" + integrity sha512-YPtlfvkYh/4MvNNm5w3uwo+1KPMg67snzr5CuexbRewsu2ITaF7f0bh0Jcayi20wztk8SgWjNz1bmF8j9qbWIw== + +"@next/swc-android-arm64@12.0.3": + version "12.0.3" + resolved "https://registry.yarnpkg.com/@next/swc-android-arm64/-/swc-android-arm64-12.0.3.tgz#8b99b3e7f13dda1f4c3c6dc83af73d8f40afecd5" + integrity sha512-40sOl9/50aamX0dEMrecqJQcUrRK47D7S9F66ulrZmz+5Ujp0lnP1rBOXngo0PZMecfU1tr7zbNubiAMDxfCxw== + +"@next/swc-darwin-arm64@12.0.3": + version "12.0.3" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.0.3.tgz#a385e610fb4a20c47355520b82a79d08e0f6441e" + integrity sha512-iKSe2hCMB51Ft41cNAxZk6St1rBlqSRtBSl4oO0zJlGu7bCxXCGCJ058/OLvYxcNWgz7ODOApObm3Yjv8XEvxg== + +"@next/swc-darwin-x64@12.0.3": + version "12.0.3" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-12.0.3.tgz#0405a3838a652b7bb44c5cd5d920c11240194385" + integrity sha512-/BcnfLyhIj4rgU3yVDfD8uXK2TcNYIdflYHKkjFxd3/J1GWOtBN31m0dB8fL0h5LdW11kzaXvVvab3f5ilkEww== + +"@next/swc-linux-arm-gnueabihf@12.0.3": + version "12.0.3" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.0.3.tgz#f5d43be7314044526fd9f1eef34337bb95f02e01" + integrity sha512-2HNPhBJuN9L6JzqqqdYB4TKfFFmaKkpF0X3C1s83Xp61mR2sx8gOthHQtZqWDs4ZLnKZU0j2flGU1uuqpHPCpg== + +"@next/swc-linux-arm64-gnu@12.0.3": + version "12.0.3" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.0.3.tgz#6f1cda1dadabcc4d4f13bd6f5ce23b9879bc6d73" + integrity sha512-NXTON1XK7zi2i+A+bY1PVLi1g5b8cSwgzbnuVR0vAgOtU+3at7FqAKOWfuFIXY7eBEK65uu0Fu5gADhMj0uanQ== + +"@next/swc-linux-arm64-musl@12.0.3": + version "12.0.3" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.0.3.tgz#1eedc1f1fcafc9862ef7e83205ada96bf320a694" + integrity sha512-8D0q22VavhcIl2ZQErEffgh5q6mChaG84uTluAoFfjwrgYtPDZX0M5StqkTZL6T5gA5RLHboNVoscIKGZWMojQ== + +"@next/swc-linux-x64-gnu@12.0.3": + version "12.0.3" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.0.3.tgz#eca85107b01a7571957ae25104d11042e9835a49" + integrity sha512-4mkimH9nMzbuQfLmZ152NYSHdrII9AeqrkrHszexL1Lup2TLMPuxlXj55eVnyyeKFXRLlnqbCu7aOIND68RbOA== + +"@next/swc-linux-x64-musl@12.0.3": + version "12.0.3" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.0.3.tgz#758656b8e36a520c03763d154c366bec889c56b3" + integrity sha512-MXvx+IDYoSsSM7KcwbQAVo9r+ZeklHeDQiUEmyRRzQE1Q4JvkWwMdPu/NfFdyxur+RfKjRoUoWFdPi5MBKTpkw== + +"@next/swc-win32-arm64-msvc@12.0.3": + version "12.0.3" + resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.0.3.tgz#3f8ab8fa3367d729e49b3072fb24f9d0f8af7c21" + integrity sha512-8GusumFZLp/mtVix+3JZVTGqzqntTsrTIFZ+GpcLMwyVjB3KkBwHiwJaa38WGleUinJSpJvgmhTWgppsiSKW3A== + +"@next/swc-win32-ia32-msvc@12.0.3": + version "12.0.3" + resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.0.3.tgz#e3df153a4e0c896a5871f1d26c7e176fa1ceec72" + integrity sha512-mF7bkxSZ++QiB+E0HFqay/etvPF+ZFcCuG27lSwFIM00J+TE0IRqMyMx66vJ8g1h6khpwXPI0o2hrwIip/r8cQ== + +"@next/swc-win32-x64-msvc@12.0.3": + version "12.0.3" + resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.0.3.tgz#c3e4af29cd74190b89461ccc26b932ae4c27f99d" + integrity sha512-eXFwyf46UFFggMQ3k2tJsOmB3SuKjWaSiZJH0tTDUsLw74lyqyzJqMCVA4yY0gWSlEnSjmX5nrCBknVZd3joaA== "@nodelib/fs.scandir@2.1.5": version "2.1.5" @@ -1455,10 +1490,10 @@ lz-string "^1.4.4" pretty-format "^27.0.2" -"@testing-library/jest-dom@^5.0.0": - version "5.14.1" - resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-5.14.1.tgz#8501e16f1e55a55d675fe73eecee32cdaddb9766" - integrity sha512-dfB7HVIgTNCxH22M1+KU6viG5of2ldoA5ly8Ar8xkezKHKXjRvznCdbMbqjYGgO2xjRbwnR+rR8MLUIqF3kKbQ== +"@testing-library/jest-dom@^5.15.0": + version "5.15.0" + resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-5.15.0.tgz#4f5295dbc476a14aec3b07176434b3d51aae5da7" + integrity sha512-lOMuQidnL1tWHLEWIhL6UvSZC1Qt3OkNe1khvi2h6xFiqpe5O8arYs46OU0qyUGq0cSTbroQyMktYNXu3a7sAA== dependencies: "@babel/runtime" "^7.9.2" "@types/testing-library__jest-dom" "^5.9.1" @@ -1478,10 +1513,10 @@ "@babel/runtime" "^7.12.5" "@testing-library/dom" "^8.0.0" -"@testing-library/user-event@^13.4.1": - version "13.4.1" - resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-13.4.1.tgz#8d9e73bcc7be09560b4c0ffbb6842ac43bc80ed2" - integrity sha512-WcnVwi96MmFsHLMNvBz03aPMVDU3UOgucXcn85fNXKKdtd7CHi2NAgE3hASt157yTB9krym0ikFVKbqYghKRCg== +"@testing-library/user-event@^13.5.0": + version "13.5.0" + resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-13.5.0.tgz#69d77007f1e124d55314a2b73fd204b333b13295" + integrity sha512-5Kwtbo3Y/NowpkbRuSepbyMFkZmHgD+vPzYB/RJ4oxt5Gj/avFFBYjhw27cqSVPVw/3a67NK1PbiIr9k4Gwmdg== dependencies: "@babel/runtime" "^7.12.5" @@ -1689,10 +1724,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-11.11.6.tgz#df929d1bb2eee5afdda598a41930fe50b43eaa6a" integrity sha512-Exw4yUWMBXM3X+8oqzJNRqZSwUAaS4+7NdvHqQuFi/d+synz++xmX3QIf+BFqneW8N31R8Ky+sikfZUXq07ggQ== -"@types/node@16.11.1": - version "16.11.1" - resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.1.tgz#2e50a649a50fc403433a14f829eface1a3443e97" - integrity sha512-PYGcJHL9mwl1Ek3PLiYgyEKtwTMmkMw4vbiyz/ps3pfdRYLVv+SN7qHVAImrjdAXxgluDEw6Ph4lyv+m9UpRmA== +"@types/node@16.11.7": + version "16.11.7" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.7.tgz#36820945061326978c42a01e56b61cd223dfdc42" + integrity sha512-QB5D2sqfSjCmTuWcBWyJ+/44bcjO7VbjSbOE0ucoVbAsSNQc4Lt6QkgkVXkTDwkL4z/beecZNDvVX15D4P8Jbw== "@types/node@^10.12.18": version "10.17.60" @@ -1747,10 +1782,10 @@ dependencies: "@types/react" "*" -"@types/react-dom@17.0.9": - version "17.0.9" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.9.tgz#441a981da9d7be117042e1a6fd3dac4b30f55add" - integrity sha512-wIvGxLfgpVDSAMH5utdL9Ngm5Owu0VsGmldro3ORLXV8CShrL8awVj06NuEXFQ5xyaYfdca7Sgbk/50Ri1GdPg== +"@types/react-dom@17.0.11": + version "17.0.11" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.11.tgz#e1eadc3c5e86bdb5f7684e00274ae228e7bcc466" + integrity sha512-f96K3k+24RaLGVu/Y2Ng3e1EbZ8/cVJvypZWd7cy0ofCBaf2lcM46xNhycMZ2xGwbBjRql7hOlZ+e2WlJ5MH3Q== dependencies: "@types/react" "*" @@ -1761,20 +1796,10 @@ dependencies: "@types/react" "*" -"@types/react-redux@7.1.19": - version "7.1.19" - resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.19.tgz#477bd0a9b01bae6d6bf809418cdfa7d3c16d4c62" - integrity sha512-L37dSCT0aoJnCgpR8Iuginlbxoh7qhWOXiaDqEsxVMrER1CmVhFD+63NxgJeT4pkmEM28oX0NH4S4f+sXHTZjA== - dependencies: - "@types/hoist-non-react-statics" "^3.3.0" - "@types/react" "*" - hoist-non-react-statics "^3.3.0" - redux "^4.0.0" - -"@types/react-redux@^7.1.16": - version "7.1.18" - resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.18.tgz#2bf8fd56ebaae679a90ebffe48ff73717c438e04" - integrity sha512-9iwAsPyJ9DLTRH+OFeIrm9cAbIj1i2ANL3sKQFATqnPWRbg+jEFXyZOKHiQK/N86pNRXbb4HRxAxo0SIX1XwzQ== +"@types/react-redux@7.1.20", "@types/react-redux@^7.1.20": + version "7.1.20" + resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.20.tgz#42f0e61ababb621e12c66c96dda94c58423bd7df" + integrity sha512-q42es4c8iIeTgcnB+yJgRTTzftv3eYYvCZOh1Ckn2eX/3o5TdsQYKUWpLoLuGlcY/p+VAhV9IOEZJcWk/vfkXw== dependencies: "@types/hoist-non-react-statics" "^3.3.0" "@types/react" "*" @@ -1807,10 +1832,10 @@ "@types/scheduler" "*" csstype "^3.0.2" -"@types/react@17.0.30": - version "17.0.30" - resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.30.tgz#2f8e6f5ab6415c091cc5e571942ee9064b17609e" - integrity sha512-3Dt/A8gd3TCXi2aRe84y7cK1K8G+N9CZRDG8kDGguOKa0kf/ZkSwTmVIDPsm/KbQOVMaDJXwhBtuOXxqwdpWVg== +"@types/react@17.0.34": + version "17.0.34" + resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.34.tgz#797b66d359b692e3f19991b6b07e4b0c706c0102" + integrity sha512-46FEGrMjc2+8XhHXILr+3+/sTe3OfzSPU9YGKILLrUYbQ1CLQC9Daqo1KzENGXAWwrFwiY0l4ZbF20gRvgpWTg== dependencies: "@types/prop-types" "*" "@types/scheduler" "*" @@ -2172,6 +2197,11 @@ acorn-walk@^8.1.1: resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== +acorn@8.5.0, acorn@^8.4.1: + version "8.5.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.5.0.tgz#4512ccb99b3698c752591e9bb4472e38ad43cee2" + integrity sha512-yXbYeFy+jUuYd3/CDcg2NkIYE991XYX/bje7LmjJigUciaeO1JR4XxXgCIV1/Zc/dRuFEyw1L0pbA+qynJkW5Q== + acorn@^7.0.0, acorn@^7.1.1, acorn@^7.4.0: version "7.4.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" @@ -2182,11 +2212,6 @@ acorn@^8.2.4: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.4.1.tgz#56c36251fc7cabc7096adc18f05afe814321a28c" integrity sha512-asabaBSkEKosYKMITunzX177CXxQ4Q8BSSzMTKD+FefUhipQC70gfW5SiUDhYQ3vk8G+81HqQk7Fv9OXwwn9KA== -acorn@^8.4.1: - version "8.5.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.5.0.tgz#4512ccb99b3698c752591e9bb4472e38ad43cee2" - integrity sha512-yXbYeFy+jUuYd3/CDcg2NkIYE991XYX/bje7LmjJigUciaeO1JR4XxXgCIV1/Zc/dRuFEyw1L0pbA+qynJkW5Q== - aes-js@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-3.1.2.tgz#db9aabde85d5caabbfc0d4f2a4446960f627146a" @@ -2413,14 +2438,6 @@ assert@2.0.0: object-is "^1.0.1" util "^0.12.0" -assert@^1.1.1: - version "1.5.0" - resolved "https://registry.yarnpkg.com/assert/-/assert-1.5.0.tgz#55c109aaf6e0aefdb3dc4b71240c70bf574b18eb" - integrity sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA== - dependencies: - object-assign "^4.1.1" - util "0.10.3" - assertion-error@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" @@ -2431,11 +2448,6 @@ ast-types-flow@^0.0.7: resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.7.tgz#f70b735c6bca1a5c9c22d982c3e39e7feba3bdad" integrity sha1-9wtzXGvKGlycItmCw+Oef+ujva0= -ast-types@0.13.2: - version "0.13.2" - resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.13.2.tgz#df39b677a911a83f3a049644fb74fdded23cea48" - integrity sha512-uWMHxJxtfj/1oZClOxDEV1sQ1HCDkA4MG8Gr69KKeBjEVH0R84WlejZ0y2DcwyBlpAEMltmVYkVgqfLFb2oyiA== - astral-regex@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" @@ -2473,16 +2485,16 @@ autoprefixer@^10.2.6: normalize-range "^0.1.2" postcss-value-parser "^4.1.0" -autoprefixer@^10.3.7: - version "10.3.7" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.3.7.tgz#cef2562058406bd378c94aacda36bb46a97b3186" - integrity sha512-EmGpu0nnQVmMhX8ROoJ7Mx8mKYPlcUHuxkwrRYEYMz85lu7H09v8w6R1P0JPdn/hKU32GjpLBFEOuIlDWCRWvg== +autoprefixer@^10.4.0: + version "10.4.0" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.0.tgz#c3577eb32a1079a440ec253e404eaf1eb21388c8" + integrity sha512-7FdJ1ONtwzV1G43GDD0kpVMn/qbiNqyOPMFTX5nRffI+7vgWoFEc6DcXOxHJxrWNDXrZh18eDsZjvZGUljSRGA== dependencies: - browserslist "^4.17.3" - caniuse-lite "^1.0.30001264" + browserslist "^4.17.5" + caniuse-lite "^1.0.30001272" fraction.js "^4.1.1" normalize-range "^0.1.2" - picocolors "^0.2.1" + picocolors "^1.0.0" postcss-value-parser "^4.1.0" available-typed-arrays@^1.0.2: @@ -2510,12 +2522,12 @@ axobject-query@^2.2.0: resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be" integrity sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA== -babel-jest@^27.2.5: - version "27.2.5" - resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-27.2.5.tgz#6bbbc1bb4200fe0bfd1b1fbcbe02fc62ebed16aa" - integrity sha512-GC9pWCcitBhSuF7H3zl0mftoKizlswaF0E3qi+rPL417wKkCB0d+Sjjb0OfXvxj7gWiBf497ldgRMii68Xz+2g== +babel-jest@^27.3.1: + version "27.3.1" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-27.3.1.tgz#0636a3404c68e07001e434ac4956d82da8a80022" + integrity sha512-SjIF8hh/ir0peae2D6S6ZKRhUy7q/DnpH7k/V6fT4Bgs/LXXUztOpX4G2tCgq8mLo5HA9mN6NmlFMeYtKmIsTQ== dependencies: - "@jest/transform" "^27.2.5" + "@jest/transform" "^27.3.1" "@jest/types" "^27.2.5" "@types/babel__core" "^7.1.14" babel-plugin-istanbul "^6.0.0" @@ -2814,7 +2826,7 @@ browserify-sign@^4.0.0: readable-stream "^3.6.0" safe-buffer "^5.2.0" -browserify-zlib@0.2.0, browserify-zlib@^0.2.0: +browserify-zlib@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f" integrity sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA== @@ -2843,15 +2855,15 @@ browserslist@^4.17.1: nanocolors "^0.1.5" node-releases "^1.1.76" -browserslist@^4.17.3: - version "4.17.4" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.17.4.tgz#72e2508af2a403aec0a49847ef31bd823c57ead4" - integrity sha512-Zg7RpbZpIJRW3am9Lyckue7PLytvVxxhJj1CaJVlCWENsGEAOlnlt8X0ZxGRPp7Bt9o8tIRM5SEXy4BCPMJjLQ== +browserslist@^4.17.5: + version "4.18.0" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.18.0.tgz#849944d9bbbbe5ff6f418a8b558e3effca433cae" + integrity sha512-ER2M0g5iAR84fS/zjBDqEgU6iO5fS9JI2EkHr5zxDxYEFk3LjhU9Vpp/INb6RMQphxko7PDV1FH38H/qVP5yCA== dependencies: - caniuse-lite "^1.0.30001265" - electron-to-chromium "^1.3.867" + caniuse-lite "^1.0.30001280" + electron-to-chromium "^1.3.896" escalade "^3.1.1" - node-releases "^2.0.0" + node-releases "^2.0.1" picocolors "^1.0.0" bs-logger@0.x: @@ -2912,15 +2924,6 @@ buffer@5.6.0: base64-js "^1.0.2" ieee754 "^1.1.4" -buffer@^4.3.0: - version "4.9.2" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8" - integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg== - dependencies: - base64-js "^1.0.2" - ieee754 "^1.1.4" - isarray "^1.0.0" - buffer@^5.0.5, buffer@^5.5.0, buffer@^5.6.0: version "5.7.1" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" @@ -3009,10 +3012,10 @@ caniuse-lite@^1.0.30001260: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001263.tgz#7ce7a6fb482a137585cbc908aaf38e90c53a16a4" integrity sha512-doiV5dft6yzWO1WwU19kt8Qz8R0/8DgEziz6/9n2FxUasteZNwNNYSmJO3GLBH8lCVE73AB1RPDPAeYbcO5Cvw== -caniuse-lite@^1.0.30001264, caniuse-lite@^1.0.30001265: - version "1.0.30001269" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001269.tgz#3a71bee03df627364418f9fd31adfc7aa1cc2d56" - integrity sha512-UOy8okEVs48MyHYgV+RdW1Oiudl1H6KolybD6ZquD0VcrPSgj25omXO1S7rDydjpqaISCwA8Pyx+jUQKZwWO5w== +caniuse-lite@^1.0.30001272, caniuse-lite@^1.0.30001280: + version "1.0.30001280" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001280.tgz#066a506046ba4be34cde5f74a08db7a396718fb7" + integrity sha512-kFXwYvHe5rix25uwueBxC569o53J6TpnGu0BEEn+6Lhl2vsnAumRFWEBhDft1fwyo6m1r4i+RqA4+163FpeFcA== caseless@~0.12.0: version "0.12.0" @@ -3122,7 +3125,7 @@ chownr@^2.0.0: resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== -ci-info@^3.1.1: +ci-info@^3.1.1, ci-info@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.2.0.tgz#2876cb948a498797b5236f0095bc057d0dca38b6" integrity sha512-dVqRX7fLUm8J6FgHJ418XuIgDLZDkYcDFTeL6TA2gt5WlIZUQrrH6EZrNClwT/H0FateUsZkGIOPRrLbP+PR9A== @@ -3273,17 +3276,12 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= -console-browserify@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336" - integrity sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA== - console-control-strings@^1.0.0, console-control-strings@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= -constants-browserify@1.0.0, constants-browserify@^1.0.0: +constants-browserify@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" integrity sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U= @@ -3432,7 +3430,7 @@ cross-spawn@^7.0.2, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" -crypto-browserify@3.12.0, crypto-browserify@^3.11.0: +crypto-browserify@3.12.0: version "3.12.0" resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" integrity sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg== @@ -3799,11 +3797,6 @@ domain-browser@4.19.0: resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-4.19.0.tgz#1093e17c0a17dbd521182fe90d49ac1370054af1" integrity sha512-fRA+BaAWOR/yr/t7T9E9GJztHPeFjj8U35ajyAjCDtAAnTn1Rc1f6W6VGPJrO1tkQv9zWu+JRof7z6oQtiYVFQ== -domain-browser@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" - integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA== - domexception@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/domexception/-/domexception-2.0.1.tgz#fb44aefba793e1574b0af6aed2801d057529f304" @@ -3839,10 +3832,10 @@ electron-to-chromium@^1.3.846: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.850.tgz#c56c72abfeab051b4b328beb894461c5912d0456" integrity sha512-ZzkDcdzePeF4dhoGZQT77V2CyJOpwfTZEOg4h0x6R/jQhGt/rIRpbRyVreWLtD7B/WsVxo91URm2WxMKR9JQZA== -electron-to-chromium@^1.3.867: - version "1.3.871" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.871.tgz#6e87365fd72037a6c898fb46050ad4be3ac9ef62" - integrity sha512-qcLvDUPf8DSIMWarHT2ptgcqrYg62n3vPA7vhrOF24d8UNzbUBaHu2CySiENR3nEDzYgaN60071t0F6KLYMQ7Q== +electron-to-chromium@^1.3.896: + version "1.3.896" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.896.tgz#4a94efe4870b1687eafd5c378198a49da06e8a1b" + integrity sha512-NcGkBVXePiuUrPLV8IxP43n1EOtdg+dudVjrfVEUd/bOqpQUFZ2diL5PPYzbgEhZFEltdXV3AcyKwGnEQ5lhMA== elliptic@6.5.4, elliptic@^6.4.0, elliptic@^6.5.2, elliptic@^6.5.3, elliptic@^6.5.4: version "6.5.4" @@ -4014,12 +4007,12 @@ escodegen@^2.0.0: optionalDependencies: source-map "~0.6.1" -eslint-config-next@^11.1.2: - version "11.1.2" - resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-11.1.2.tgz#73c918f2fa6120d5f65080bf3fcf6b154905707e" - integrity sha512-dFutecxX2Z5/QVlLwdtKt+gIfmNMP8Qx6/qZh3LM/DFVdGJEAnUKrr4VwGmACB2kx/PQ5bx3R+QxnEg4fDPiTg== +eslint-config-next@^12.0.3: + version "12.0.3" + resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-12.0.3.tgz#a85ad423997f098b41b61c279472e0642e200a9e" + integrity sha512-q+mX6jhk3HrCo39G18MLhiC6f8zJnTA00f30RSuVUWsv45SQUm6r62oXVqrbAgMEybe0yx/GDRvfA6LvSolw6Q== dependencies: - "@next/eslint-plugin-next" "11.1.2" + "@next/eslint-plugin-next" "12.0.3" "@rushstack/eslint-patch" "^1.0.6" "@typescript-eslint/parser" "^4.20.0" eslint-import-resolver-node "^0.3.4" @@ -4382,7 +4375,7 @@ eventemitter3@4.0.4: resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.4.tgz#b5463ace635a083d018bdc7c917b4c5f10a85384" integrity sha512-rlaVLnVxtxvoyLsQQFBx53YmXHDxRIzzTLbdfxqi4yocpSjAxXwkU0cScM5JgSKMqEhrZpnvQ2D9gjylR0AimQ== -events@^3.0.0, events@^3.1.0, events@^3.3.0: +events@3.3.0, events@^3.1.0, events@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== @@ -4425,16 +4418,16 @@ expand-template@^2.0.3: resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== -expect@^27.2.5: - version "27.2.5" - resolved "https://registry.yarnpkg.com/expect/-/expect-27.2.5.tgz#16154aaa60b4d9a5b0adacfea3e4d6178f4b93fd" - integrity sha512-ZrO0w7bo8BgGoP/bLz+HDCI+0Hfei9jUSZs5yI/Wyn9VkG9w8oJ7rHRgYj+MA7yqqFa0IwHA3flJzZtYugShJA== +expect@^27.3.1: + version "27.3.1" + resolved "https://registry.yarnpkg.com/expect/-/expect-27.3.1.tgz#d0f170b1f5c8a2009bab0beffd4bb94f043e38e7" + integrity sha512-MrNXV2sL9iDRebWPGOGFdPQRl2eDQNu/uhxIMShjjx74T6kC6jFIkmQ6OqXDtevjGUkyB2IT56RzDBqXf/QPCg== dependencies: "@jest/types" "^27.2.5" ansi-styles "^5.0.0" - jest-get-type "^27.0.6" - jest-matcher-utils "^27.2.5" - jest-message-util "^27.2.5" + jest-get-type "^27.3.1" + jest-matcher-utils "^27.3.1" + jest-message-util "^27.3.1" jest-regex-util "^27.0.6" express@^4.14.0: @@ -5161,7 +5154,7 @@ http-signature@~1.2.0: jsprim "^1.2.2" sshpk "^1.7.0" -https-browserify@1.0.0, https-browserify@^1.0.0: +https-browserify@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= @@ -5275,16 +5268,11 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3, inherits@~2.0.4: +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3, inherits@~2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== -inherits@2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" - integrity sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE= - inherits@2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" @@ -5529,7 +5517,7 @@ is-typedarray@1.0.0, is-typedarray@^1.0.0, is-typedarray@~1.0.0: resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= -isarray@^1.0.0, isarray@~1.0.0: +isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= @@ -5593,84 +5581,84 @@ isurl@^1.0.0-alpha5: has-to-string-tag-x "^1.2.0" is-object "^1.0.1" -jest-changed-files@^27.2.5: - version "27.2.5" - resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-27.2.5.tgz#9dfd550d158260bcb6fa80aff491f5647f7daeca" - integrity sha512-jfnNJzF89csUKRPKJ4MwZ1SH27wTmX2xiAIHUHrsb/OYd9Jbo4/SXxJ17/nnx6RIifpthk3Y+LEeOk+/dDeGdw== +jest-changed-files@^27.3.0: + version "27.3.0" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-27.3.0.tgz#22a02cc2b34583fc66e443171dc271c0529d263c" + integrity sha512-9DJs9garMHv4RhylUMZgbdCJ3+jHSkpL9aaVKp13xtXAD80qLTLrqcDZL1PHA9dYA0bCI86Nv2BhkLpLhrBcPg== dependencies: "@jest/types" "^27.2.5" execa "^5.0.0" throat "^6.0.1" -jest-circus@^27.2.5: - version "27.2.5" - resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-27.2.5.tgz#573256a6fb6e447ac2fc7e0ade9375013309037f" - integrity sha512-eyL9IcrAxm3Saq3rmajFCwpaxaRMGJ1KJs+7hlTDinXpJmeR3P02bheM3CYohE7UfwOBmrFMJHjgo/WPcLTM+Q== +jest-circus@^27.3.1: + version "27.3.1" + resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-27.3.1.tgz#1679e74387cbbf0c6a8b42de963250a6469e0797" + integrity sha512-v1dsM9II6gvXokgqq6Yh2jHCpfg7ZqV4jWY66u7npz24JnhP3NHxI0sKT7+ZMQ7IrOWHYAaeEllOySbDbWsiXw== dependencies: - "@jest/environment" "^27.2.5" - "@jest/test-result" "^27.2.5" + "@jest/environment" "^27.3.1" + "@jest/test-result" "^27.3.1" "@jest/types" "^27.2.5" "@types/node" "*" chalk "^4.0.0" co "^4.6.0" dedent "^0.7.0" - expect "^27.2.5" + expect "^27.3.1" is-generator-fn "^2.0.0" - jest-each "^27.2.5" - jest-matcher-utils "^27.2.5" - jest-message-util "^27.2.5" - jest-runtime "^27.2.5" - jest-snapshot "^27.2.5" - jest-util "^27.2.5" - pretty-format "^27.2.5" + jest-each "^27.3.1" + jest-matcher-utils "^27.3.1" + jest-message-util "^27.3.1" + jest-runtime "^27.3.1" + jest-snapshot "^27.3.1" + jest-util "^27.3.1" + pretty-format "^27.3.1" slash "^3.0.0" stack-utils "^2.0.3" throat "^6.0.1" -jest-cli@^27.2.5: - version "27.2.5" - resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-27.2.5.tgz#88718c8f05f1c0f209152952ecd61afe4c3311bb" - integrity sha512-XzfcOXi5WQrXqFYsDxq5RDOKY4FNIgBgvgf3ZBz4e/j5/aWep5KnsAYH5OFPMdX/TP/LFsYQMRH7kzJUMh6JKg== +jest-cli@^27.3.1: + version "27.3.1" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-27.3.1.tgz#b576f9d146ba6643ce0a162d782b40152b6b1d16" + integrity sha512-WHnCqpfK+6EvT62me6WVs8NhtbjAS4/6vZJnk7/2+oOr50cwAzG4Wxt6RXX0hu6m1169ZGMlhYYUNeKBXCph/Q== dependencies: - "@jest/core" "^27.2.5" - "@jest/test-result" "^27.2.5" + "@jest/core" "^27.3.1" + "@jest/test-result" "^27.3.1" "@jest/types" "^27.2.5" chalk "^4.0.0" exit "^0.1.2" graceful-fs "^4.2.4" import-local "^3.0.2" - jest-config "^27.2.5" - jest-util "^27.2.5" - jest-validate "^27.2.5" + jest-config "^27.3.1" + jest-util "^27.3.1" + jest-validate "^27.3.1" prompts "^2.0.1" yargs "^16.2.0" -jest-config@^27.2.5: - version "27.2.5" - resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-27.2.5.tgz#c2e4ec6ea2bf4ffd2cae3d927999fe6159cba207" - integrity sha512-QdENtn9b5rIIYGlbDNEcgY9LDL5kcokJnXrp7x8AGjHob/XFqw1Z6p+gjfna2sUulQsQ3ce2Fvntnv+7fKYDhQ== +jest-config@^27.3.1: + version "27.3.1" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-27.3.1.tgz#cb3b7f6aaa8c0a7daad4f2b9573899ca7e09bbad" + integrity sha512-KY8xOIbIACZ/vdYCKSopL44I0xboxC751IX+DXL2+Wx6DKNycyEfV3rryC3BPm5Uq/BBqDoMrKuqLEUNJmMKKg== dependencies: "@babel/core" "^7.1.0" - "@jest/test-sequencer" "^27.2.5" + "@jest/test-sequencer" "^27.3.1" "@jest/types" "^27.2.5" - babel-jest "^27.2.5" + babel-jest "^27.3.1" chalk "^4.0.0" + ci-info "^3.2.0" deepmerge "^4.2.2" glob "^7.1.1" graceful-fs "^4.2.4" - is-ci "^3.0.0" - jest-circus "^27.2.5" - jest-environment-jsdom "^27.2.5" - jest-environment-node "^27.2.5" - jest-get-type "^27.0.6" - jest-jasmine2 "^27.2.5" + jest-circus "^27.3.1" + jest-environment-jsdom "^27.3.1" + jest-environment-node "^27.3.1" + jest-get-type "^27.3.1" + jest-jasmine2 "^27.3.1" jest-regex-util "^27.0.6" - jest-resolve "^27.2.5" - jest-runner "^27.2.5" - jest-util "^27.2.5" - jest-validate "^27.2.5" + jest-resolve "^27.3.1" + jest-runner "^27.3.1" + jest-util "^27.3.1" + jest-validate "^27.3.1" micromatch "^4.0.4" - pretty-format "^27.2.5" + pretty-format "^27.3.1" jest-css-modules-transform@^4.3.0: version "4.3.0" @@ -5701,15 +5689,15 @@ jest-diff@^27.0.0: jest-get-type "^27.0.6" pretty-format "^27.2.0" -jest-diff@^27.2.5: - version "27.2.5" - resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-27.2.5.tgz#908f7a6aca5653824516ad30e0a9fd9767e53623" - integrity sha512-7gfwwyYkeslOOVQY4tVq5TaQa92mWfC9COsVYMNVYyJTOYAqbIkoD3twi5A+h+tAPtAelRxkqY6/xu+jwTr0dA== +jest-diff@^27.3.1: + version "27.3.1" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-27.3.1.tgz#d2775fea15411f5f5aeda2a5e02c2f36440f6d55" + integrity sha512-PCeuAH4AWUo2O5+ksW4pL9v5xJAcIKPUPfIhZBcG1RKv/0+dvaWTQK1Nrau8d67dp65fOqbeMdoil+6PedyEPQ== dependencies: chalk "^4.0.0" diff-sequences "^27.0.6" - jest-get-type "^27.0.6" - pretty-format "^27.2.5" + jest-get-type "^27.3.1" + pretty-format "^27.3.1" jest-docblock@^27.0.6: version "27.0.6" @@ -5718,41 +5706,41 @@ jest-docblock@^27.0.6: dependencies: detect-newline "^3.0.0" -jest-each@^27.2.5: - version "27.2.5" - resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-27.2.5.tgz#378118d516db730b92096a9607b8711165946353" - integrity sha512-HUPWIbJT0bXarRwKu/m7lYzqxR4GM5EhKOsu0z3t0SKtbFN6skQhpAUADM4qFShBXb9zoOuag5lcrR1x/WM+Ag== +jest-each@^27.3.1: + version "27.3.1" + resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-27.3.1.tgz#14c56bb4f18dd18dc6bdd853919b5f16a17761ff" + integrity sha512-E4SwfzKJWYcvOYCjOxhZcxwL+AY0uFMvdCOwvzgutJiaiodFjkxQQDxHm8FQBeTqDnSmKsQWn7ldMRzTn2zJaQ== dependencies: "@jest/types" "^27.2.5" chalk "^4.0.0" - jest-get-type "^27.0.6" - jest-util "^27.2.5" - pretty-format "^27.2.5" + jest-get-type "^27.3.1" + jest-util "^27.3.1" + pretty-format "^27.3.1" -jest-environment-jsdom@^27.2.5: - version "27.2.5" - resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-27.2.5.tgz#21de3ad0e89441d961b592ba7561b16241279208" - integrity sha512-QtRpOh/RQKuXniaWcoFE2ElwP6tQcyxHu0hlk32880g0KczdonCs5P1sk5+weu/OVzh5V4Bt1rXuQthI01mBLg== +jest-environment-jsdom@^27.3.1: + version "27.3.1" + resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-27.3.1.tgz#63ac36d68f7a9303494df783494856222b57f73e" + integrity sha512-3MOy8qMzIkQlfb3W1TfrD7uZHj+xx8Olix5vMENkj5djPmRqndMaXtpnaZkxmxM+Qc3lo+yVzJjzuXbCcZjAlg== dependencies: - "@jest/environment" "^27.2.5" - "@jest/fake-timers" "^27.2.5" + "@jest/environment" "^27.3.1" + "@jest/fake-timers" "^27.3.1" "@jest/types" "^27.2.5" "@types/node" "*" - jest-mock "^27.2.5" - jest-util "^27.2.5" + jest-mock "^27.3.0" + jest-util "^27.3.1" jsdom "^16.6.0" -jest-environment-node@^27.2.5: - version "27.2.5" - resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-27.2.5.tgz#ffa1afb3604c640ec841f044d526c65912e02cef" - integrity sha512-0o1LT4grm7iwrS8fIoLtwJxb/hoa3GsH7pP10P02Jpj7Mi4BXy65u46m89vEM2WfD1uFJQ2+dfDiWZNA2e6bJg== +jest-environment-node@^27.3.1: + version "27.3.1" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-27.3.1.tgz#af7d0eed04edafb740311b303f3fe7c8c27014bb" + integrity sha512-T89F/FgkE8waqrTSA7/ydMkcc52uYPgZZ6q8OaZgyiZkJb5QNNCF6oPZjH9IfPFfcc9uBWh1574N0kY0pSvTXw== dependencies: - "@jest/environment" "^27.2.5" - "@jest/fake-timers" "^27.2.5" + "@jest/environment" "^27.3.1" + "@jest/fake-timers" "^27.3.1" "@jest/types" "^27.2.5" "@types/node" "*" - jest-mock "^27.2.5" - jest-util "^27.2.5" + jest-mock "^27.3.0" + jest-util "^27.3.1" jest-get-type@^26.3.0: version "26.3.0" @@ -5764,10 +5752,15 @@ jest-get-type@^27.0.6: resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.0.6.tgz#0eb5c7f755854279ce9b68a9f1a4122f69047cfe" integrity sha512-XTkK5exIeUbbveehcSR8w0bhH+c0yloW/Wpl+9vZrjzztCPWrxhHwkIFpZzCt71oRBsgxmuUfxEqOYoZI2macg== -jest-haste-map@^27.2.5: - version "27.2.5" - resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-27.2.5.tgz#0247b7299250643472bbcf5b4ad85c72d5178e2e" - integrity sha512-pzO+Gw2WLponaSi0ilpzYBE0kuVJstoXBX8YWyUebR8VaXuX4tzzn0Zp23c/WaETo7XYTGv2e8KdnpiskAFMhQ== +jest-get-type@^27.3.1: + version "27.3.1" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.3.1.tgz#a8a2b0a12b50169773099eee60a0e6dd11423eff" + integrity sha512-+Ilqi8hgHSAdhlQ3s12CAVNd8H96ZkQBfYoXmArzZnOfAtVAJEiPDBirjByEblvG/4LPJmkL+nBqPO3A1YJAEg== + +jest-haste-map@^27.3.1: + version "27.3.1" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-27.3.1.tgz#7656fbd64bf48bda904e759fc9d93e2c807353ee" + integrity sha512-lYfNZIzwPccDJZIyk9Iz5iQMM/MH56NIIcGj7AFU1YyA4ewWFBl8z+YPJuSCRML/ee2cCt2y3W4K3VXPT6Nhzg== dependencies: "@jest/types" "^27.2.5" "@types/graceful-fs" "^4.1.2" @@ -5777,59 +5770,59 @@ jest-haste-map@^27.2.5: graceful-fs "^4.2.4" jest-regex-util "^27.0.6" jest-serializer "^27.0.6" - jest-util "^27.2.5" - jest-worker "^27.2.5" + jest-util "^27.3.1" + jest-worker "^27.3.1" micromatch "^4.0.4" walker "^1.0.7" optionalDependencies: fsevents "^2.3.2" -jest-jasmine2@^27.2.5: - version "27.2.5" - resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-27.2.5.tgz#baaf96c69913c52bce0100000cf0721027c0fd66" - integrity sha512-hdxY9Cm/CjLqu2tXeAoQHPgA4vcqlweVXYOg1+S9FeFdznB9Rti+eEBKDDkmOy9iqr4Xfbq95OkC4NFbXXPCAQ== +jest-jasmine2@^27.3.1: + version "27.3.1" + resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-27.3.1.tgz#df6d3d07c7dafc344feb43a0072a6f09458d32b0" + integrity sha512-WK11ZUetDQaC09w4/j7o4FZDUIp+4iYWH/Lik34Pv7ukL+DuXFGdnmmi7dT58J2ZYKFB5r13GyE0z3NPeyJmsg== dependencies: "@babel/traverse" "^7.1.0" - "@jest/environment" "^27.2.5" + "@jest/environment" "^27.3.1" "@jest/source-map" "^27.0.6" - "@jest/test-result" "^27.2.5" + "@jest/test-result" "^27.3.1" "@jest/types" "^27.2.5" "@types/node" "*" chalk "^4.0.0" co "^4.6.0" - expect "^27.2.5" + expect "^27.3.1" is-generator-fn "^2.0.0" - jest-each "^27.2.5" - jest-matcher-utils "^27.2.5" - jest-message-util "^27.2.5" - jest-runtime "^27.2.5" - jest-snapshot "^27.2.5" - jest-util "^27.2.5" - pretty-format "^27.2.5" + jest-each "^27.3.1" + jest-matcher-utils "^27.3.1" + jest-message-util "^27.3.1" + jest-runtime "^27.3.1" + jest-snapshot "^27.3.1" + jest-util "^27.3.1" + pretty-format "^27.3.1" throat "^6.0.1" -jest-leak-detector@^27.2.5: - version "27.2.5" - resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-27.2.5.tgz#e2edc3b37d38e8d9a527e10e456b403c3151b206" - integrity sha512-HYsi3GUR72bYhOGB5C5saF9sPdxGzSjX7soSQS+BqDRysc7sPeBwPbhbuT8DnOpijnKjgwWQ8JqvbmReYnt3aQ== +jest-leak-detector@^27.3.1: + version "27.3.1" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-27.3.1.tgz#7fb632c2992ef707a1e73286e1e704f9cc1772b2" + integrity sha512-78QstU9tXbaHzwlRlKmTpjP9k4Pvre5l0r8Spo4SbFFVy/4Abg9I6ZjHwjg2QyKEAMg020XcjP+UgLZIY50yEg== dependencies: - jest-get-type "^27.0.6" - pretty-format "^27.2.5" + jest-get-type "^27.3.1" + pretty-format "^27.3.1" -jest-matcher-utils@^27.2.5: - version "27.2.5" - resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-27.2.5.tgz#4684faaa8eb32bf15e6edaead6834031897e2980" - integrity sha512-qNR/kh6bz0Dyv3m68Ck2g1fLW5KlSOUNcFQh87VXHZwWc/gY6XwnKofx76Qytz3x5LDWT09/2+yXndTkaG4aWg== +jest-matcher-utils@^27.3.1: + version "27.3.1" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-27.3.1.tgz#257ad61e54a6d4044e080d85dbdc4a08811e9c1c" + integrity sha512-hX8N7zXS4k+8bC1Aj0OWpGb7D3gIXxYvPNK1inP5xvE4ztbz3rc4AkI6jGVaerepBnfWB17FL5lWFJT3s7qo8w== dependencies: chalk "^4.0.0" - jest-diff "^27.2.5" - jest-get-type "^27.0.6" - pretty-format "^27.2.5" + jest-diff "^27.3.1" + jest-get-type "^27.3.1" + pretty-format "^27.3.1" -jest-message-util@^27.2.5: - version "27.2.5" - resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-27.2.5.tgz#ed8b7b0965247bb875a49c1f9b9ab2d1d0820028" - integrity sha512-ggXSLoPfIYcbmZ8glgEJZ8b+e0Msw/iddRmgkoO7lDAr9SmI65IIfv7VnvTnV4FGnIIUIjzM+fHRHO5RBvyAbQ== +jest-message-util@^27.3.1: + version "27.3.1" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-27.3.1.tgz#f7c25688ad3410ab10bcb862bcfe3152345c6436" + integrity sha512-bh3JEmxsTZ/9rTm0jQrPElbY2+y48Rw2t47uMfByNyUVR+OfPh4anuyKsGqsNkXk/TI4JbLRZx+7p7Hdt6q1yg== dependencies: "@babel/code-frame" "^7.12.13" "@jest/types" "^27.2.5" @@ -5837,14 +5830,14 @@ jest-message-util@^27.2.5: chalk "^4.0.0" graceful-fs "^4.2.4" micromatch "^4.0.4" - pretty-format "^27.2.5" + pretty-format "^27.3.1" slash "^3.0.0" stack-utils "^2.0.3" -jest-mock@^27.2.5: - version "27.2.5" - resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-27.2.5.tgz#0ec38d5ff1e49c4802e7a4a8179e8d7a2fd84de0" - integrity sha512-HiMB3LqE9RzmeMzZARi2Bz3NoymxyP0gCid4y42ca1djffNtYFKgI220aC1VP1mUZ8rbpqZbHZOJ15093bZV/Q== +jest-mock@^27.3.0: + version "27.3.0" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-27.3.0.tgz#ddf0ec3cc3e68c8ccd489bef4d1f525571a1b867" + integrity sha512-ziZiLk0elZOQjD08bLkegBzv5hCABu/c8Ytx45nJKkysQwGaonvmTxwjLqEA4qGdasq9o2I8/HtdGMNnVsMTGw== dependencies: "@jest/types" "^27.2.5" "@types/node" "*" @@ -5859,40 +5852,40 @@ jest-regex-util@^27.0.6: resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-27.0.6.tgz#02e112082935ae949ce5d13b2675db3d8c87d9c5" integrity sha512-SUhPzBsGa1IKm8hx2F4NfTGGp+r7BXJ4CulsZ1k2kI+mGLG+lxGrs76veN2LF/aUdGosJBzKgXmNCw+BzFqBDQ== -jest-resolve-dependencies@^27.2.5: - version "27.2.5" - resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-27.2.5.tgz#fcd8eca005b3d11ba32da443045c028164b83be1" - integrity sha512-BSjefped31bcvvCh++/pN9ueqqN1n0+p8/58yScuWfklLm2tbPbS9d251vJhAy0ZI2pL/0IaGhOTJrs9Y4FJlg== +jest-resolve-dependencies@^27.3.1: + version "27.3.1" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-27.3.1.tgz#85b99bdbdfa46e2c81c6228fc4c91076f624f6e2" + integrity sha512-X7iLzY8pCiYOnvYo2YrK3P9oSE8/3N2f4pUZMJ8IUcZnT81vlSonya1KTO9ZfKGuC+svE6FHK/XOb8SsoRUV1A== dependencies: "@jest/types" "^27.2.5" jest-regex-util "^27.0.6" - jest-snapshot "^27.2.5" + jest-snapshot "^27.3.1" -jest-resolve@^27.2.5: - version "27.2.5" - resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-27.2.5.tgz#04dadbfc1312a2541f5c199c5011945e9cfe5cef" - integrity sha512-q5irwS3oS73SKy3+FM/HL2T7WJftrk9BRzrXF92f7net5HMlS7lJMg/ZwxLB4YohKqjSsdksEw7n/jvMxV7EKg== +jest-resolve@^27.3.1: + version "27.3.1" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-27.3.1.tgz#0e5542172a1aa0270be6f66a65888647bdd74a3e" + integrity sha512-Dfzt25CFSPo3Y3GCbxynRBZzxq9AdyNN+x/v2IqYx6KVT5Z6me2Z/PsSGFSv3cOSUZqJ9pHxilao/I/m9FouLw== dependencies: "@jest/types" "^27.2.5" chalk "^4.0.0" - escalade "^3.1.1" graceful-fs "^4.2.4" - jest-haste-map "^27.2.5" + jest-haste-map "^27.3.1" jest-pnp-resolver "^1.2.2" - jest-util "^27.2.5" - jest-validate "^27.2.5" + jest-util "^27.3.1" + jest-validate "^27.3.1" resolve "^1.20.0" + resolve.exports "^1.1.0" slash "^3.0.0" -jest-runner@^27.2.5: - version "27.2.5" - resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-27.2.5.tgz#3d9d0626f351480bb2cffcfbbfac240c0097ebd4" - integrity sha512-n41vw9RLg5TKAnEeJK9d6pGOsBOpwE89XBniK+AD1k26oIIy3V7ogM1scbDjSheji8MUPC9pNgCrZ/FHLVDNgg== +jest-runner@^27.3.1: + version "27.3.1" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-27.3.1.tgz#1d594dcbf3bd8600a7e839e790384559eaf96e3e" + integrity sha512-r4W6kBn6sPr3TBwQNmqE94mPlYVn7fLBseeJfo4E2uCTmAyDFm2O5DYAQAFP7Q3YfiA/bMwg8TVsciP7k0xOww== dependencies: - "@jest/console" "^27.2.5" - "@jest/environment" "^27.2.5" - "@jest/test-result" "^27.2.5" - "@jest/transform" "^27.2.5" + "@jest/console" "^27.3.1" + "@jest/environment" "^27.3.1" + "@jest/test-result" "^27.3.1" + "@jest/transform" "^27.3.1" "@jest/types" "^27.2.5" "@types/node" "*" chalk "^4.0.0" @@ -5900,30 +5893,29 @@ jest-runner@^27.2.5: exit "^0.1.2" graceful-fs "^4.2.4" jest-docblock "^27.0.6" - jest-environment-jsdom "^27.2.5" - jest-environment-node "^27.2.5" - jest-haste-map "^27.2.5" - jest-leak-detector "^27.2.5" - jest-message-util "^27.2.5" - jest-resolve "^27.2.5" - jest-runtime "^27.2.5" - jest-util "^27.2.5" - jest-worker "^27.2.5" + jest-environment-jsdom "^27.3.1" + jest-environment-node "^27.3.1" + jest-haste-map "^27.3.1" + jest-leak-detector "^27.3.1" + jest-message-util "^27.3.1" + jest-resolve "^27.3.1" + jest-runtime "^27.3.1" + jest-util "^27.3.1" + jest-worker "^27.3.1" source-map-support "^0.5.6" throat "^6.0.1" -jest-runtime@^27.2.5: - version "27.2.5" - resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-27.2.5.tgz#d144c3f6889b927aae1e695b63a41a3323b7016b" - integrity sha512-N0WRZ3QszKyZ3Dm27HTBbBuestsSd3Ud5ooVho47XZJ8aSKO/X1Ag8M1dNx9XzfGVRNdB/xCA3lz8MJwIzPLLA== +jest-runtime@^27.3.1: + version "27.3.1" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-27.3.1.tgz#80fa32eb85fe5af575865ddf379874777ee993d7" + integrity sha512-qtO6VxPbS8umqhEDpjA4pqTkKQ1Hy4ZSi9mDVeE9Za7LKBo2LdW2jmT+Iod3XFaJqINikZQsn2wEi0j9wPRbLg== dependencies: - "@jest/console" "^27.2.5" - "@jest/environment" "^27.2.5" - "@jest/fake-timers" "^27.2.5" - "@jest/globals" "^27.2.5" + "@jest/console" "^27.3.1" + "@jest/environment" "^27.3.1" + "@jest/globals" "^27.3.1" "@jest/source-map" "^27.0.6" - "@jest/test-result" "^27.2.5" - "@jest/transform" "^27.2.5" + "@jest/test-result" "^27.3.1" + "@jest/transform" "^27.3.1" "@jest/types" "^27.2.5" "@types/yargs" "^16.0.0" chalk "^4.0.0" @@ -5933,14 +5925,14 @@ jest-runtime@^27.2.5: exit "^0.1.2" glob "^7.1.3" graceful-fs "^4.2.4" - jest-haste-map "^27.2.5" - jest-message-util "^27.2.5" - jest-mock "^27.2.5" + jest-haste-map "^27.3.1" + jest-message-util "^27.3.1" + jest-mock "^27.3.0" jest-regex-util "^27.0.6" - jest-resolve "^27.2.5" - jest-snapshot "^27.2.5" - jest-util "^27.2.5" - jest-validate "^27.2.5" + jest-resolve "^27.3.1" + jest-snapshot "^27.3.1" + jest-util "^27.3.1" + jest-validate "^27.3.1" slash "^3.0.0" strip-bom "^4.0.0" yargs "^16.2.0" @@ -5953,10 +5945,10 @@ jest-serializer@^27.0.6: "@types/node" "*" graceful-fs "^4.2.4" -jest-snapshot@^27.2.5: - version "27.2.5" - resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-27.2.5.tgz#8a612fe31e2967f58ad364542198dff61f92ef32" - integrity sha512-2/Jkn+VN6Abwz0llBltZaiJMnL8b1j5Bp/gRIxe9YR3FCEh9qp0TXVV0dcpTGZ8AcJV1SZGQkczewkI9LP5yGw== +jest-snapshot@^27.3.1: + version "27.3.1" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-27.3.1.tgz#1da5c0712a252d70917d46c037054f5918c49ee4" + integrity sha512-APZyBvSgQgOT0XumwfFu7X3G5elj6TGhCBLbBdn3R1IzYustPGPE38F51dBWMQ8hRXa9je0vAdeVDtqHLvB6lg== dependencies: "@babel/core" "^7.7.2" "@babel/generator" "^7.7.2" @@ -5964,23 +5956,23 @@ jest-snapshot@^27.2.5: "@babel/plugin-syntax-typescript" "^7.7.2" "@babel/traverse" "^7.7.2" "@babel/types" "^7.0.0" - "@jest/transform" "^27.2.5" + "@jest/transform" "^27.3.1" "@jest/types" "^27.2.5" "@types/babel__traverse" "^7.0.4" "@types/prettier" "^2.1.5" babel-preset-current-node-syntax "^1.0.0" chalk "^4.0.0" - expect "^27.2.5" + expect "^27.3.1" graceful-fs "^4.2.4" - jest-diff "^27.2.5" - jest-get-type "^27.0.6" - jest-haste-map "^27.2.5" - jest-matcher-utils "^27.2.5" - jest-message-util "^27.2.5" - jest-resolve "^27.2.5" - jest-util "^27.2.5" + jest-diff "^27.3.1" + jest-get-type "^27.3.1" + jest-haste-map "^27.3.1" + jest-matcher-utils "^27.3.1" + jest-message-util "^27.3.1" + jest-resolve "^27.3.1" + jest-util "^27.3.1" natural-compare "^1.4.0" - pretty-format "^27.2.5" + pretty-format "^27.3.1" semver "^7.3.2" jest-util@^27.0.0: @@ -5995,41 +5987,41 @@ jest-util@^27.0.0: is-ci "^3.0.0" picomatch "^2.2.3" -jest-util@^27.2.5: - version "27.2.5" - resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-27.2.5.tgz#88740c4024d223634a82ce7c2263e8bc6df3b3ba" - integrity sha512-QRhDC6XxISntMzFRd/OQ6TGsjbzA5ONO0tlAj2ElHs155x1aEr0rkYJBEysG6H/gZVH3oGFzCdAB/GA8leh8NQ== +jest-util@^27.3.1: + version "27.3.1" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-27.3.1.tgz#a58cdc7b6c8a560caac9ed6bdfc4e4ff23f80429" + integrity sha512-8fg+ifEH3GDryLQf/eKZck1DEs2YuVPBCMOaHQxVVLmQwl/CDhWzrvChTX4efLZxGrw+AA0mSXv78cyytBt/uw== dependencies: "@jest/types" "^27.2.5" "@types/node" "*" chalk "^4.0.0" + ci-info "^3.2.0" graceful-fs "^4.2.4" - is-ci "^3.0.0" picomatch "^2.2.3" -jest-validate@^27.2.5: - version "27.2.5" - resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-27.2.5.tgz#2d59bf1627d180f395ba58f24599b0ee0efcfbdf" - integrity sha512-XgYtjS89nhVe+UfkbLgcm+GgXKWgL80t9nTcNeejyO3t0Sj/yHE8BtIJqjZu9NXQksYbGImoQRXmQ1gP+Guffw== +jest-validate@^27.3.1: + version "27.3.1" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-27.3.1.tgz#3a395d61a19cd13ae9054af8cdaf299116ef8a24" + integrity sha512-3H0XCHDFLA9uDII67Bwi1Vy7AqwA5HqEEjyy934lgVhtJ3eisw6ShOF1MDmRPspyikef5MyExvIm0/TuLzZ86Q== dependencies: "@jest/types" "^27.2.5" camelcase "^6.2.0" chalk "^4.0.0" - jest-get-type "^27.0.6" + jest-get-type "^27.3.1" leven "^3.1.0" - pretty-format "^27.2.5" + pretty-format "^27.3.1" -jest-watcher@^27.2.5: - version "27.2.5" - resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-27.2.5.tgz#41cd3e64dc5bea8a4327083d71ba7667be400567" - integrity sha512-umV4qGozg2Dn6DTTtqAh9puPw+DGLK9AQas7+mWjiK8t0fWMpxKg8ZXReZw7L4C88DqorsGUiDgwHNZ+jkVrkQ== +jest-watcher@^27.3.1: + version "27.3.1" + resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-27.3.1.tgz#ba5e0bc6aa843612b54ddb7f009d1cbff7e05f3e" + integrity sha512-9/xbV6chABsGHWh9yPaAGYVVKurWoP3ZMCv6h+O1v9/+pkOroigs6WzZ0e9gLP/njokUwM7yQhr01LKJVMkaZA== dependencies: - "@jest/test-result" "^27.2.5" + "@jest/test-result" "^27.3.1" "@jest/types" "^27.2.5" "@types/node" "*" ansi-escapes "^4.2.1" chalk "^4.0.0" - jest-util "^27.2.5" + jest-util "^27.3.1" string-length "^4.0.1" jest-worker@27.0.0-next.5: @@ -6041,23 +6033,23 @@ jest-worker@27.0.0-next.5: merge-stream "^2.0.0" supports-color "^8.0.0" -jest-worker@^27.2.5: - version "27.2.5" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.2.5.tgz#ed42865661959488aa020e8a325df010597c36d4" - integrity sha512-HTjEPZtcNKZ4LnhSp02NEH4vE+5OpJ0EsOWYvGQpHgUMLngydESAAMH5Wd/asPf29+XUDQZszxpLg1BkIIA2aw== +jest-worker@^27.3.1: + version "27.3.1" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.3.1.tgz#0def7feae5b8042be38479799aeb7b5facac24b2" + integrity sha512-ks3WCzsiZaOPJl/oMsDjaf0TRiSv7ctNgs0FqRr2nARsovz6AWWy4oLElwcquGSz692DzgZQrCLScPNs5YlC4g== dependencies: "@types/node" "*" merge-stream "^2.0.0" supports-color "^8.0.0" -jest@^27.2.5: - version "27.2.5" - resolved "https://registry.yarnpkg.com/jest/-/jest-27.2.5.tgz#7d8a5c8781a160f693beeb7c68e46c16ef948148" - integrity sha512-vDMzXcpQN4Ycaqu+vO7LX8pZwNNoKMhc+gSp6q1D8S6ftRk8gNW8cni3YFxknP95jxzQo23Lul0BI2FrWgnwYQ== +jest@^27.3.1: + version "27.3.1" + resolved "https://registry.yarnpkg.com/jest/-/jest-27.3.1.tgz#b5bab64e8f56b6f7e275ba1836898b0d9f1e5c8a" + integrity sha512-U2AX0AgQGd5EzMsiZpYt8HyZ+nSVIh5ujQ9CPp9EQZJMjXIiSZpJNweZl0swatKRoqHWgGKM3zaSwm4Zaz87ng== dependencies: - "@jest/core" "^27.2.5" + "@jest/core" "^27.3.1" import-local "^3.0.2" - jest-cli "^27.2.5" + jest-cli "^27.3.1" jmespath@^0.15.0: version "0.15.0" @@ -6813,7 +6805,7 @@ nanoid@^3.1.25: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.25.tgz#09ca32747c0e543f0e1814b7d3793477f9c8e152" integrity sha512-rdwtIXaXCLFAQbnfqDRnI6jaRHp9fTcYBjtFKE8eezcZ7LuLjhUaQGNeMXf1HmRoCH32CLz6XwX0TtxEOS/A3Q== -nanoid@^3.1.28: +nanoid@^3.1.30: version "3.1.30" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.30.tgz#63f93cc548d2a113dc5dfbc63bfa09e2b9b64362" integrity sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ== @@ -6823,13 +6815,6 @@ napi-build-utils@^1.0.1: resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806" integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg== -native-url@0.3.4: - version "0.3.4" - resolved "https://registry.yarnpkg.com/native-url/-/native-url-0.3.4.tgz#29c943172aed86c63cee62c8c04db7f5756661f8" - integrity sha512-6iM8R99ze45ivyH8vybJ7X0yekIcPf5GgLV5K0ENCbmRcaRIDoj37BC8iLEmaaBfqqb8enuZ5p0uhY+lVAbAcA== - dependencies: - querystring "^0.2.0" - natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" @@ -6853,20 +6838,20 @@ next-tick@~1.0.0: resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c" integrity sha1-yobR/ogoFpsBICCOPchCS524NCw= -next@11.1.2: - version "11.1.2" - resolved "https://registry.yarnpkg.com/next/-/next-11.1.2.tgz#527475787a9a362f1bc916962b0c0655cc05bc91" - integrity sha512-azEYL0L+wFjv8lstLru3bgvrzPvK0P7/bz6B/4EJ9sYkXeW8r5Bjh78D/Ol7VOg0EIPz0CXoe72hzAlSAXo9hw== +next@12.0.3: + version "12.0.3" + resolved "https://registry.yarnpkg.com/next/-/next-12.0.3.tgz#325732ceb4193306a9a31912815fc570d1a66641" + integrity sha512-GGdhTBcerdMZbitrO67IVetmB+AHa2X69xrkXKClUT8SRu8pEVto/2QMSnfI+uYc5czCUWPsVtVY3aMoMRMaCA== dependencies: - "@babel/runtime" "7.15.3" + "@babel/runtime" "7.15.4" "@hapi/accept" "5.0.2" - "@next/env" "11.1.2" - "@next/polyfill-module" "11.1.2" - "@next/react-dev-overlay" "11.1.2" - "@next/react-refresh-utils" "11.1.2" - "@node-rs/helper" "1.2.1" + "@napi-rs/triples" "1.0.3" + "@next/env" "12.0.3" + "@next/polyfill-module" "12.0.3" + "@next/react-dev-overlay" "12.0.3" + "@next/react-refresh-utils" "12.0.3" + acorn "8.5.0" assert "2.0.0" - ast-types "0.13.2" browserify-zlib "0.2.0" browserslist "4.16.6" buffer "5.6.0" @@ -6879,29 +6864,28 @@ next@11.1.2: domain-browser "4.19.0" encoding "0.1.13" etag "1.8.1" + events "3.3.0" find-cache-dir "3.3.1" get-orientation "1.1.2" https-browserify "1.0.0" image-size "1.0.0" jest-worker "27.0.0-next.5" - native-url "0.3.4" node-fetch "2.6.1" node-html-parser "1.4.9" - node-libs-browser "^2.2.1" os-browserify "0.3.0" p-limit "3.1.0" path-browserify "1.0.1" - pnp-webpack-plugin "1.6.4" postcss "8.2.15" process "0.11.10" querystring-es3 "0.2.1" raw-body "2.4.1" react-is "17.0.2" react-refresh "0.8.3" + regenerator-runtime "0.13.4" stream-browserify "3.0.0" stream-http "3.1.1" string_decoder "1.3.0" - styled-jsx "4.0.1" + styled-jsx "5.0.0-beta.3" timers-browserify "2.0.12" tty-browserify "0.0.1" use-subscription "1.5.1" @@ -6909,10 +6893,17 @@ next@11.1.2: vm-browserify "1.1.2" watchpack "2.1.1" optionalDependencies: - "@next/swc-darwin-arm64" "11.1.2" - "@next/swc-darwin-x64" "11.1.2" - "@next/swc-linux-x64-gnu" "11.1.2" - "@next/swc-win32-x64-msvc" "11.1.2" + "@next/swc-android-arm64" "12.0.3" + "@next/swc-darwin-arm64" "12.0.3" + "@next/swc-darwin-x64" "12.0.3" + "@next/swc-linux-arm-gnueabihf" "12.0.3" + "@next/swc-linux-arm64-gnu" "12.0.3" + "@next/swc-linux-arm64-musl" "12.0.3" + "@next/swc-linux-x64-gnu" "12.0.3" + "@next/swc-linux-x64-musl" "12.0.3" + "@next/swc-win32-arm64-msvc" "12.0.3" + "@next/swc-win32-ia32-msvc" "12.0.3" + "@next/swc-win32-x64-msvc" "12.0.3" node-abi@^2.21.0: version "2.30.1" @@ -6963,35 +6954,6 @@ node-int64@^0.4.0: resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" integrity sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs= -node-libs-browser@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.1.tgz#b64f513d18338625f90346d27b0d235e631f6425" - integrity sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q== - dependencies: - assert "^1.1.1" - browserify-zlib "^0.2.0" - buffer "^4.3.0" - console-browserify "^1.1.0" - constants-browserify "^1.0.0" - crypto-browserify "^3.11.0" - domain-browser "^1.1.1" - events "^3.0.0" - https-browserify "^1.0.0" - os-browserify "^0.3.0" - path-browserify "0.0.1" - process "^0.11.10" - punycode "^1.2.4" - querystring-es3 "^0.2.0" - readable-stream "^2.3.3" - stream-browserify "^2.0.1" - stream-http "^2.7.2" - string_decoder "^1.0.0" - timers-browserify "^2.0.4" - tty-browserify "0.0.0" - url "^0.11.0" - util "^0.11.0" - vm-browserify "^1.0.1" - node-modules-regexp@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz#8d9dbe28964a4ac5712e9131642107c71e90ec40" @@ -7007,10 +6969,10 @@ node-releases@^1.1.76: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.76.tgz#df245b062b0cafbd5282ab6792f7dccc2d97f36e" integrity sha512-9/IECtNr8dXNmPWmFXepT0/7o5eolGesHUa3mtr0KlgnCvnZxwh2qensKL42JJY2vQKC3nIBXetFAqR+PW1CmA== -node-releases@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.0.tgz#67dc74903100a7deb044037b8a2e5f453bb05400" - integrity sha512-aA87l0flFYMzCHpTM3DERFSYxc6lv/BltdbRTOMZuxZ0cwZCD3mejE5n9vLhSJCN++/eOqr77G1IO5uXxlQYWA== +node-releases@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.1.tgz#3d1d395f204f1f2f29a54358b9fb678765ad2fc5" + integrity sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA== normalize-package-data@^2.3.2: version "2.5.0" @@ -7219,7 +7181,7 @@ optionator@^0.9.1: type-check "^0.4.0" word-wrap "^1.2.3" -os-browserify@0.3.0, os-browserify@^0.3.0: +os-browserify@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc= @@ -7354,11 +7316,6 @@ parseurl@~1.3.3: resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== -path-browserify@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.1.tgz#e6c4ddd7ed3aa27c68a20cc4e50e1a4ee83bbc4a" - integrity sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ== - path-browserify@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd" @@ -7427,11 +7384,6 @@ performance-now@^2.1.0: resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= -picocolors@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-0.2.1.tgz#570670f793646851d1ba135996962abad587859f" - integrity sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA== - picocolors@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" @@ -7516,13 +7468,6 @@ platform@1.3.6: resolved "https://registry.yarnpkg.com/platform/-/platform-1.3.6.tgz#48b4ce983164b209c2d45a107adb31f473a6e7a7" integrity sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg== -pnp-webpack-plugin@1.6.4: - version "1.6.4" - resolved "https://registry.yarnpkg.com/pnp-webpack-plugin/-/pnp-webpack-plugin-1.6.4.tgz#c9711ac4dc48a685dabafc86f8b6dd9f8df84149" - integrity sha512-7Wjy+9E3WwLOEL30D+m8TSTF7qJJUJLONBnwQp0518siuMxUQUbgZwssaFX+QKlZkjHZcw/IpZCt/H0srrntSg== - dependencies: - ts-pnp "^1.1.6" - postcss-js@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/postcss-js/-/postcss-js-3.0.3.tgz#2f0bd370a2e8599d45439f6970403b5873abda33" @@ -7590,6 +7535,15 @@ postcss@8.2.15: nanoid "^3.1.23" source-map-js "^0.6.2" +postcss@^8.3.11: + version "8.3.11" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.3.11.tgz#c3beca7ea811cd5e1c4a3ec6d2e7599ef1f8f858" + integrity sha512-hCmlUAIlUiav8Xdqw3Io4LcpA1DOt7h3LSTAC4G6JGHFFaWzI6qvFt9oilvl8BmkbBRX1IhM90ZAmpk68zccQA== + dependencies: + nanoid "^3.1.30" + picocolors "^1.0.0" + source-map-js "^0.6.2" + postcss@^8.3.5: version "8.3.8" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.3.8.tgz#9ebe2a127396b4b4570ae9f7770e7fb83db2bac1" @@ -7599,15 +7553,6 @@ postcss@^8.3.5: nanoid "^3.1.25" source-map-js "^0.6.2" -postcss@^8.3.9: - version "8.3.9" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.3.9.tgz#98754caa06c4ee9eb59cc48bd073bb6bd3437c31" - integrity sha512-f/ZFyAKh9Dnqytx5X62jgjhhzttjZS7hMsohcI7HEI5tjELX/HxCy3EFhsRxyzGvrzFF+82XPvCS8T9TFleVJw== - dependencies: - nanoid "^3.1.28" - picocolors "^0.2.1" - source-map-js "^0.6.2" - prebuild-install@^6.0.1: version "6.1.4" resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-6.1.4.tgz#ae3c0142ad611d58570b89af4986088a4937e00f" @@ -7682,10 +7627,10 @@ pretty-format@^27.0.2: ansi-styles "^5.0.0" react-is "^17.0.1" -pretty-format@^27.2.5: - version "27.2.5" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.2.5.tgz#7cfe2a8e8f01a5b5b29296a0b70f4140df0830c5" - integrity sha512-+nYn2z9GgicO9JiqmY25Xtq8SYfZ/5VCpEU3pppHHNAhd1y+ZXxmNPd1evmNcAd6Hz4iBV2kf0UpGth5A/VJ7g== +pretty-format@^27.3.1: + version "27.3.1" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.3.1.tgz#7e9486365ccdd4a502061fa761d3ab9ca1b78df5" + integrity sha512-DR/c+pvFc52nLimLROYjnXPtolawm+uWDxr4FjuLDLUn+ktWnSN851KoHwHzzqq6rfCOjkzN8FLgDrSub6UDuA== dependencies: "@jest/types" "^27.2.5" ansi-regex "^5.0.1" @@ -7762,21 +7707,11 @@ pump@^3.0.0: end-of-stream "^1.1.0" once "^1.3.1" -punycode@1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" - integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0= - punycode@2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.0.tgz#5f863edc89b96db09074bad7947bf09056ca4e7d" integrity sha1-X4Y+3Im5bbCQdLrXlHvwkFbKTn0= -punycode@^1.2.4: - version "1.4.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" - integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= - punycode@^2.1.0, punycode@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" @@ -7835,21 +7770,11 @@ query-string@^6.13.5: split-on-first "^1.0.0" strict-uri-encode "^2.0.0" -querystring-es3@0.2.1, querystring-es3@^0.2.0: +querystring-es3@0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" integrity sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM= -querystring@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" - integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= - -querystring@^0.2.0: - version "0.2.1" - resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.1.tgz#40d77615bb09d16902a85c3e38aa8b5ed761c2dd" - integrity sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg== - queue-microtask@^1.2.2: version "1.2.3" resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" @@ -7955,12 +7880,12 @@ react-input-autosize@^3.0.0: dependencies: prop-types "^15.5.8" -react-is@17.0.2, react-is@^17.0.1: +react-is@17.0.2, react-is@^17.0.1, react-is@^17.0.2: version "17.0.2" resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== -react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.1: +react-is@^16.7.0, react-is@^16.8.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -7987,17 +7912,17 @@ react-modal@^3.14.3: react-lifecycles-compat "^3.0.0" warning "^4.0.3" -react-redux@^7.2.5: - version "7.2.5" - resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.5.tgz#213c1b05aa1187d9c940ddfc0b29450957f6a3b8" - integrity sha512-Dt29bNyBsbQaysp6s/dN0gUodcq+dVKKER8Qv82UrpeygwYeX1raTtil7O/fftw/rFqzaf6gJhDZRkkZnn6bjg== +react-redux@^7.2.6: + version "7.2.6" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.6.tgz#49633a24fe552b5f9caf58feb8a138936ddfe9aa" + integrity sha512-10RPdsz0UUrRL1NZE0ejTkucnclYSgXp5q+tB5SWx2qeG2ZJQJyymgAhwKy73yiL/13btfB6fPr+rgbMAaZIAQ== dependencies: - "@babel/runtime" "^7.12.1" - "@types/react-redux" "^7.1.16" + "@babel/runtime" "^7.15.4" + "@types/react-redux" "^7.1.20" hoist-non-react-statics "^3.3.2" loose-envify "^1.4.0" prop-types "^15.7.2" - react-is "^16.13.1" + react-is "^17.0.2" react-refresh@0.8.3: version "0.8.3" @@ -8017,10 +7942,10 @@ react-select@4.3.1: react-input-autosize "^3.0.0" react-transition-group "^4.3.0" -react-toastify@^8.0.3: - version "8.0.3" - resolved "https://registry.yarnpkg.com/react-toastify/-/react-toastify-8.0.3.tgz#7fbf65f69ec357aab8dd03c1496f9177aa92409a" - integrity sha512-rv3koC7f9lKKSkdpYgo/TGzgWlrB/aaiUInF1DyV7BpiM4kyTs+uhu6/r8XDMtBY2FOIHK+FlK3Iv7OzpA/tCA== +react-toastify@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/react-toastify/-/react-toastify-8.1.0.tgz#acaca4e8c4415c8474562dd84a14e6f390ed7f17" + integrity sha512-M+Q3rTmEw/53Csr7NsV/YnldJe4c7uERcY7Tma9mvLU98QT2VhIkKwjBzzxZkJRk/oBKyUAtkyMjMgO00hx6gQ== dependencies: clsx "^1.1.1" @@ -8059,7 +7984,7 @@ read-pkg@^3.0.0: normalize-package-data "^2.3.2" path-type "^3.0.0" -readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.3.3, readable-stream@^2.3.6: +readable-stream@^2.0.6: version "2.3.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== @@ -8123,6 +8048,11 @@ redux@^4.0.0, redux@^4.1.0: dependencies: "@babel/runtime" "^7.9.2" +regenerator-runtime@0.13.4: + version "0.13.4" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.4.tgz#e96bf612a3362d12bb69f7e8f74ffeab25c7ac91" + integrity sha512-plpwicqEzfEyTQohIKktWigcLzmNStMGwbOUbykx51/29Z3JOGYldaaNGK7ngNXV+UcoqvIMmloZ48Sr74sd+g== + regenerator-runtime@^0.13.4: version "0.13.7" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55" @@ -8204,6 +8134,11 @@ resolve-from@^5.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== +resolve.exports@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-1.1.0.tgz#5ce842b94b05146c0e03076985d1d0e7e48c90c9" + integrity sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ== + resolve@^1.10.0, resolve@^1.13.1, resolve@^1.17.0, resolve@^1.20.0: version "1.20.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" @@ -8424,10 +8359,10 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -shell-quote@1.7.2: - version "1.7.2" - resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.2.tgz#67a7d02c76c9da24f99d20808fcaded0e0e04be2" - integrity sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg== +shell-quote@1.7.3: + version "1.7.3" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.3.tgz#aa40edac170445b9a431e17bb62c0b881b9c4123" + integrity sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw== side-channel@^1.0.4: version "1.0.4" @@ -8641,14 +8576,6 @@ stream-browserify@3.0.0: inherits "~2.0.4" readable-stream "^3.5.0" -stream-browserify@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b" - integrity sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg== - dependencies: - inherits "~2.0.1" - readable-stream "^2.0.2" - stream-http@3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-3.1.1.tgz#0370a8017cf8d050b9a8554afe608f043eaff564" @@ -8659,17 +8586,6 @@ stream-http@3.1.1: readable-stream "^3.6.0" xtend "^4.0.2" -stream-http@^2.7.2: - version "2.8.3" - resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.3.tgz#b2d242469288a5a27ec4fe8933acf623de6514fc" - integrity sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw== - dependencies: - builtin-status-codes "^3.0.0" - inherits "^2.0.1" - readable-stream "^2.3.6" - to-arraybuffer "^1.0.0" - xtend "^4.0.0" - stream-parser@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/stream-parser/-/stream-parser-0.3.1.tgz#1618548694420021a1182ff0af1911c129761773" @@ -8774,7 +8690,7 @@ string.prototype.trimstart@^1.0.4: call-bind "^1.0.2" define-properties "^1.1.3" -string_decoder@1.3.0, string_decoder@^1.0.0, string_decoder@^1.1.1: +string_decoder@1.3.0, string_decoder@^1.1.1: version "1.3.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== @@ -8788,12 +8704,12 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -strip-ansi@6.0.0, strip-ansi@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" - integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== +strip-ansi@6.0.1, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== dependencies: - ansi-regex "^5.0.0" + ansi-regex "^5.0.1" strip-ansi@^3.0.0, strip-ansi@^3.0.1: version "3.0.1" @@ -8816,12 +8732,12 @@ strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" -strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== +strip-ansi@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" + integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== dependencies: - ansi-regex "^5.0.1" + ansi-regex "^5.0.0" strip-bom@^3.0.0: version "3.0.0" @@ -8862,10 +8778,10 @@ strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== -styled-jsx@4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-4.0.1.tgz#ae3f716eacc0792f7050389de88add6d5245b9e9" - integrity sha512-Gcb49/dRB1k8B4hdK8vhW27Rlb2zujCk1fISrizCcToIs+55B4vmUM0N9Gi4nnVfFZWe55jRdWpAqH1ldAKWvQ== +styled-jsx@5.0.0-beta.3: + version "5.0.0-beta.3" + resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.0.0-beta.3.tgz#400d16179b5dff10d5954ab8be27a9a1b7780dd2" + integrity sha512-HtDDGSFPvmjHIqWf9n8Oo54tAoY/DTplvlyOH2+YOtD80Sp31Ap8ffSmxhgk5EkUoJ7xepdXMGT650mSffWuRA== dependencies: "@babel/plugin-syntax-jsx" "7.14.5" "@babel/types" "7.15.0" @@ -8961,10 +8877,10 @@ table@^6.0.9: string-width "^4.2.3" strip-ansi "^6.0.1" -tailwindcss@^2.2.17: - version "2.2.17" - resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-2.2.17.tgz#c6332731f9ff1b6628ff589c95c38685347775e3" - integrity sha512-WgRpn+Pxn7eWqlruxnxEbL9ByVRWi3iC10z4b6dW0zSdnkPVC4hPMSWLQkkW8GCyBIv/vbJ0bxIi9dVrl4CfoA== +tailwindcss@^2.2.19: + version "2.2.19" + resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-2.2.19.tgz#540e464832cd462bb9649c1484b0a38315c2653c" + integrity sha512-6Ui7JSVtXadtTUo2NtkBBacobzWiQYVjYW0ZnKaP9S1ZCKQ0w7KVNz+YSDI/j7O7KCMHbOkz94ZMQhbT9pOqjw== dependencies: arg "^5.0.1" bytes "^3.0.0" @@ -9077,7 +8993,7 @@ timed-out@^4.0.0, timed-out@^4.0.1: resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f" integrity sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8= -timers-browserify@2.0.12, timers-browserify@^2.0.4: +timers-browserify@2.0.12: version "2.0.12" resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.12.tgz#44a45c11fbf407f34f97bccd1577c652361b00ee" integrity sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ== @@ -9112,11 +9028,6 @@ tmpl@1.0.x: resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw== -to-arraybuffer@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" - integrity sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M= - to-fast-properties@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" @@ -9184,10 +9095,10 @@ ts-jest@^27.0.7: semver "7.x" yargs-parser "20.x" -ts-node@^10.3.0: - version "10.3.0" - resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.3.0.tgz#a797f2ed3ff50c9a5d814ce400437cb0c1c048b4" - integrity sha512-RYIy3i8IgpFH45AX4fQHExrT8BxDeKTdC83QFJkNzkvt8uFB6QJ8XMyhynYiKMLxt9a7yuXaDBZNOYS3XjDcYw== +ts-node@^10.4.0: + version "10.4.0" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.4.0.tgz#680f88945885f4e6cf450e7f0d6223dd404895f7" + integrity sha512-g0FlPvvCXSIO1JDF6S232P5jPYqBkRL9qly81ZgAOSU7rwI0stphCgd2kLiCrU9DjQCrJMWEqcNSjQL02s6d8A== dependencies: "@cspotcode/source-map-support" "0.7.0" "@tsconfig/node10" "^1.0.7" @@ -9213,11 +9124,6 @@ ts-node@^8.4.1: source-map-support "^0.5.17" yn "3.1.1" -ts-pnp@^1.1.6: - version "1.2.0" - resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.2.0.tgz#a500ad084b0798f1c3071af391e65912c86bca92" - integrity sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw== - tsconfig-paths@^3.9.0: version "3.10.1" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.10.1.tgz#79ae67a68c15289fdf5c51cb74f397522d795ed7" @@ -9239,11 +9145,6 @@ tsutils@^3.21.0: dependencies: tslib "^1.8.1" -tty-browserify@0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" - integrity sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY= - tty-browserify@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.1.tgz#3f05251ee17904dfd0677546670db9651682b811" @@ -9416,14 +9317,6 @@ url-to-options@^1.0.1: resolved "https://registry.yarnpkg.com/url-to-options/-/url-to-options-1.0.1.tgz#1505a03a289a48cbd7a434efbaeec5055f5633a9" integrity sha1-FQWgOiiaSMvXpDTvuu7FBV9WM6k= -url@^0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" - integrity sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE= - dependencies: - punycode "1.3.2" - querystring "0.2.0" - use-subscription@1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/use-subscription/-/use-subscription-1.5.1.tgz#73501107f02fad84c6dd57965beb0b75c68c42d1" @@ -9448,13 +9341,6 @@ util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= -util@0.10.3: - version "0.10.3" - resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9" - integrity sha1-evsa/lCAUkZInj23/g7TeTNqwPk= - dependencies: - inherits "2.0.1" - util@0.12.4, util@^0.12.0: version "0.12.4" resolved "https://registry.yarnpkg.com/util/-/util-0.12.4.tgz#66121a31420df8f01ca0c464be15dfa1d1850253" @@ -9467,13 +9353,6 @@ util@0.12.4, util@^0.12.0: safe-buffer "^5.1.2" which-typed-array "^1.1.2" -util@^0.11.0: - version "0.11.1" - resolved "https://registry.yarnpkg.com/util/-/util-0.11.1.tgz#3236733720ec64bb27f6e26f421aaa2e1b588d61" - integrity sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ== - dependencies: - inherits "2.0.3" - utils-merge@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" @@ -9530,7 +9409,7 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" -vm-browserify@1.1.2, vm-browserify@^1.0.1: +vm-browserify@1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== From 2f05edbaea9674a4f5822b71f1a0a23ffd677bfc Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Sun, 14 Nov 2021 18:03:26 +0100 Subject: [PATCH 20/30] Attempt to prefetch token images --- src/layout/AppLayout.tsx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/layout/AppLayout.tsx b/src/layout/AppLayout.tsx index c4edbd1..7b00e05 100644 --- a/src/layout/AppLayout.tsx +++ b/src/layout/AppLayout.tsx @@ -1,10 +1,14 @@ import { useContractKit } from '@celo-tools/use-contractkit' +import Image from 'next/image' import { PropsWithChildren, useEffect } from 'react' import Modal from 'react-modal' import { Footer } from 'src/components/nav/Footer' import { Header } from 'src/components/nav/Header' import { NULL_ADDRESS } from 'src/config/consts' import { PollingWorker } from 'src/features/polling/PollingWorker' +import CeloIcon from 'src/images/tokens/CELO.svg' +import cEURIcon from 'src/images/tokens/cEUR.svg' +import cUSDIcon from 'src/images/tokens/cUSD.svg' import { HeadMeta } from 'src/layout/HeadMeta' interface Props { @@ -35,9 +39,21 @@ export function AppLayout({ pathName, children }: PropsWithChildren) { >
{children}
+
) } + +// A hack to get Next to pre-fetch some images that may not yet be in dom tree +function ImagePrefetch() { + return ( +
+ celo + cUSD + cEUR +
+ ) +} From 8713c198745e2658478fd32ae45130e3ecef0ddb Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Sun, 14 Nov 2021 19:00:35 +0100 Subject: [PATCH 21/30] Set CSP headers in next.config --- next.config.js | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/next.config.js b/next.config.js index aad7cb9..8cb0e66 100644 --- a/next.config.js +++ b/next.config.js @@ -1,5 +1,32 @@ const { version } = require('./package.json') +const isDev = process.env.NODE_ENV !== 'production' + +const securityHeaders = [ + { + key: 'X-XSS-Protection', + value: '1; mode=block', + }, + { + key: 'X-Frame-Options', + value: 'DENY', + }, + { + key: 'X-Content-Type-Options', + value: 'nosniff', + }, + { + key: 'Referrer-Policy', + value: 'strict-origin-when-cross-origin', + }, + { + key: 'Content-Security-Policy', + value: `default-src 'self'; script-src 'self'${ + isDev ? " 'unsafe-eval'" : '' + }; connect-src 'self' https://*.celo.org https://*.celo-testnet.org wss://walletconnect.celo.org wss://relay.walletconnect.org; img-src 'self' data: https://raw.githubusercontent.com; style-src 'self' 'unsafe-inline'; font-src 'self' data:; base-uri 'self'; form-action 'self'`, + }, +] + module.exports = { webpack: (config, { webpack }) => { config.resolve.fallback = { @@ -22,6 +49,16 @@ module.exports = { ] }, + async headers() { + return [ + { + // Apply these headers to all routes in your application. + source: '/(.*)', + headers: securityHeaders, + }, + ] + }, + env: { NEXT_PUBLIC_VERSION: version, }, From ad45a0cd25512378d8a609f1e579cf984bb25c11 Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Sun, 14 Nov 2021 19:31:03 +0100 Subject: [PATCH 22/30] Add error handling to switch network, still requires use-ck fix --- src/components/nav/NetworkModal.tsx | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/components/nav/NetworkModal.tsx b/src/components/nav/NetworkModal.tsx index 8f604b7..ea5db4f 100644 --- a/src/components/nav/NetworkModal.tsx +++ b/src/components/nav/NetworkModal.tsx @@ -1,5 +1,6 @@ import { Alfajores, Baklava, Mainnet, Network, useContractKit } from '@celo-tools/use-contractkit' import ReactModal from 'react-modal' +import { toast } from 'react-toastify' import { useAppDispatch, useAppSelector } from 'src/app/hooks' import { IconButton } from 'src/components/buttons/IconButton' import { reset as accountReset } from 'src/features/accounts/accountSlice' @@ -23,14 +24,20 @@ export function NetworkModal({ isOpen, close }: Props) { const allNetworks = [Mainnet, Alfajores, Baklava] const dispatch = useAppDispatch() - const switchToNetwork = (n: Network) => { - logger.debug('Resetting and switching to network', n.name) - updateNetwork(n) - dispatch(blockReset()) - dispatch(accountReset()) - dispatch(grandaReset()) - dispatch(swapReset()) - dispatch(resetTokenPrices()) + const switchToNetwork = async (n: Network) => { + try { + logger.debug('Resetting and switching to network', n.name) + await updateNetwork(n) + dispatch(blockReset()) + dispatch(accountReset()) + dispatch(grandaReset()) + dispatch(swapReset()) + dispatch(resetTokenPrices()) + } catch (error) { + // TODO fix use-ck so it throws on update fail (i.e due to metamask) + logger.error('Error updating network', error) + toast.error('Could not switch network, is Metamask using the right network?') + } } return ( From 12b218240ef28146675b8ddb5bb1bedb4ad151c6 Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Sun, 14 Nov 2021 19:38:43 +0100 Subject: [PATCH 23/30] Add beta note in about page --- src/pages/about.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pages/about.tsx b/src/pages/about.tsx index 7e0c145..6da4b4e 100644 --- a/src/pages/about.tsx +++ b/src/pages/about.tsx @@ -47,7 +47,9 @@ export default function AboutPage() { .

-

{`Version: ${config.version || 'Unknown'}`}

+

{`Version: ${ + config.version || 'Unknown' + } [BETA]`}

) From 934c0b0c12d0a64a4a8727f20a744be12732f03a Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Sun, 14 Nov 2021 19:48:03 +0100 Subject: [PATCH 24/30] Remove dead table code --- next.config.js | 1 - src/components/table/Table.module.css | 0 src/components/table/Table.tsx | 117 -------------------------- 3 files changed, 118 deletions(-) delete mode 100644 src/components/table/Table.module.css delete mode 100644 src/components/table/Table.tsx diff --git a/next.config.js b/next.config.js index 8cb0e66..ca1a4fb 100644 --- a/next.config.js +++ b/next.config.js @@ -52,7 +52,6 @@ module.exports = { async headers() { return [ { - // Apply these headers to all routes in your application. source: '/(.*)', headers: securityHeaders, }, diff --git a/src/components/table/Table.module.css b/src/components/table/Table.module.css deleted file mode 100644 index e69de29..0000000 diff --git a/src/components/table/Table.tsx b/src/components/table/Table.tsx deleted file mode 100644 index 0331f99..0000000 --- a/src/components/table/Table.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import { ReactElement, useMemo, useState } from 'react' -import { Spinner } from 'src/components/animation/Spinner' -import { ChevronIcon } from 'src/components/Chevron' - -export interface TableColumn { - id: string // its key in the data - header: string - renderer?: (dataCell: any) => string | ReactElement -} - -type DataElement = { id: string } & Record - -interface Props { - columns: TableColumn[] - data: T[] - onRowClick?: (id: string) => void - initialSortBy?: string // column id - isLoading?: boolean -} - -export function Table(props: Props) { - const { columns, data, onRowClick, initialSortBy, isLoading } = props - - const [sortBy, setSortBy] = useState(initialSortBy ?? columns[0].id) - const [sortDesc, setSortDesc] = useState(true) - - const sortedData = useMemo(() => { - return sortDataBy(data, sortBy, sortDesc) - }, [data, sortBy, sortDesc]) - - const onColumnClick = (columnId: string) => { - if (columnId === sortBy) { - setSortDesc(!sortDesc) - } else { - setSortBy(columnId) - setSortDesc(true) - } - } - - return ( -
- - - - {columns.map((column) => { - const isSelected = column.id === sortBy - return ( - - ) - })} - - - - - - - {sortedData.map((row, i) => { - return ( - onRowClick(row.id) : undefined} - key={`table-row-${i}`} - > - {columns.map((column, j) => { - return ( - - ) - })} - - ) - })} - -
onColumnClick(column.id)} - className={`font-medium text-center px-2 pb-2 border-b - border-gray-400 cursor-pointer first:pl-0 last:pr-0`} - > - - {column.header} - {isSelected && ( - - )} - -
-
- {column.renderer ? column.renderer(row) : row[column.id]} -
-
- {isLoading && ( -
- -
- )} -
- ) -} - -function sortDataBy(data: T[], columnId: string, descending: boolean) { - return [...data].sort((a, b) => { - let aVal = a[columnId] - let bVal = b[columnId] - if (typeof aVal === 'string') { - aVal = aVal.toLowerCase() - bVal = bVal.toLowerCase() - } - const order = descending ? aVal > bVal : aVal <= bVal - return order ? -1 : 1 - }) -} From d870abce52f0634d81e10cd45d7546ce10878f6e Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Sat, 20 Nov 2021 11:40:58 +0100 Subject: [PATCH 25/30] Finish proposalConfirm screen and remove image prefetch --- src/features/granda/ProposalConfirm.tsx | 36 +++++++++++++++++++++---- src/features/granda/ProposalForm.tsx | 2 +- src/features/granda/ProposalView.tsx | 12 ++++++--- src/features/swap/SwapConfirm.tsx | 7 ++--- src/features/swap/useFormValidator.ts | 6 +++-- src/images/tokens/TokenIcon.tsx | 2 +- src/layout/AppLayout.tsx | 16 ----------- 7 files changed, 50 insertions(+), 31 deletions(-) diff --git a/src/features/granda/ProposalConfirm.tsx b/src/features/granda/ProposalConfirm.tsx index 1c532a3..4c9dd32 100644 --- a/src/features/granda/ProposalConfirm.tsx +++ b/src/features/granda/ProposalConfirm.tsx @@ -1,5 +1,6 @@ import { useContractKit } from '@celo-tools/use-contractkit' import { ContractKit, StableToken } from '@celo/contractkit' +import Image from 'next/image' import { useEffect } from 'react' import { toast } from 'react-toastify' import { useAppDispatch, useAppSelector } from 'src/app/hooks' @@ -15,6 +16,7 @@ import { fetchOracleRates } from 'src/features/granda/fetchOracleRates' import { setFormValues } from 'src/features/granda/grandaSlice' import { getExchangeValues } from 'src/features/granda/utils' import { SwapConfirmSummary } from 'src/features/swap/SwapConfirm' +import InfoCircle from 'src/images/icons/info-circle.svg' import { FloatingBox } from 'src/layout/FloatingBox' import { getAdjustedAmount } from 'src/utils/amount' import { logger } from 'src/utils/logger' @@ -41,7 +43,7 @@ export function ProposalConfirm() { fromAmount, fromTokenId, toTokenId, - config?.spread, + config.spread, oracleRates ) const tokenBalance = balances[fromTokenId] @@ -106,9 +108,9 @@ export function ProposalConfirm() { if (err.message === PROMISE_TIMEOUT) { toast.error('Action timed out') } else { - toast.error('Unable to complete swap') + toast.error('Unable to complete proposal') } - logger.error('Failed to execute swap', err) + logger.error('Failed to execute proposal', err) } } @@ -133,12 +135,36 @@ export function ProposalConfirm() {

Confirm Proposal

- + + +
+
+
Spread:
+
{`${config.spread}%`}
+
+
+
Veto Period:
+
{(config.vetoPeriodSeconds / 86400).toFixed(2) + ' days'}
+
+
- Swap + Propose
) } + +function InfoTip() { + return ( +
+
+ info +
+ Funds will be transferred out of your account until the exchange is executed or cancelled. +
+
+
+ ) +} diff --git a/src/features/granda/ProposalForm.tsx b/src/features/granda/ProposalForm.tsx index 415c314..29f250c 100644 --- a/src/features/granda/ProposalForm.tsx +++ b/src/features/granda/ProposalForm.tsx @@ -22,7 +22,7 @@ export function ProposalForm() { dispatch(setFormValues(values)) } - const validateForm = useFormValidator(balances, exchangeLimits) + const validateForm = useFormValidator(balances) const valueFormatter: ExchangeValueFormatter = (fromAmount, fromTokenId, toTokenId) => getExchangeValues(fromAmount, fromTokenId, toTokenId, spread, oracleRates) diff --git a/src/features/granda/ProposalView.tsx b/src/features/granda/ProposalView.tsx index dfb6ca1..272a37f 100644 --- a/src/features/granda/ProposalView.tsx +++ b/src/features/granda/ProposalView.tsx @@ -158,8 +158,14 @@ function SwapDetails({ } return ( -
- + <> +
Proposer:
@@ -181,7 +187,7 @@ function SwapDetails({
{p.state.toUpperCase()}
-
+ ) } diff --git a/src/features/swap/SwapConfirm.tsx b/src/features/swap/SwapConfirm.tsx index dea96b3..942d8bf 100644 --- a/src/features/swap/SwapConfirm.tsx +++ b/src/features/swap/SwapConfirm.tsx @@ -142,7 +142,7 @@ export function SwapConfirm(props: Props) {

Confirm Swap

- +
Max Slippage:
@@ -167,14 +167,15 @@ interface SwapConfirmSummaryProps { to: ExchangeValues['to'] rate: ExchangeValues['rate'] stableTokenId: NativeTokenId + mt?: string } -export function SwapConfirmSummary({ from, to, rate, stableTokenId }: SwapConfirmSummaryProps) { +export function SwapConfirmSummary({ from, to, rate, stableTokenId, mt }: SwapConfirmSummaryProps) { const fromToken = NativeTokens[from.token] const toToken = NativeTokens[to.token] return ( -
+
diff --git a/src/features/swap/useFormValidator.ts b/src/features/swap/useFormValidator.ts index 280d0fb..0a17a17 100644 --- a/src/features/swap/useFormValidator.ts +++ b/src/features/swap/useFormValidator.ts @@ -1,6 +1,7 @@ import { FormikErrors } from 'formik' import { useCallback } from 'react' import { MIN_ROUNDED_VALUE } from 'src/config/consts' +import { NativeTokenId } from 'src/config/tokens' import { AccountBalances } from 'src/features/accounts/fetchBalances' import { SizeLimits } from 'src/features/granda/types' import { SwapFormValues } from 'src/features/swap/types' @@ -21,8 +22,9 @@ export function useFormValidator(balances: AccountBalances, sizeLimits?: SizeLim return { fromAmount: 'Amount exceeds balance' } } if (sizeLimits) { - const fromTokenId = values.fromTokenId - const limits = sizeLimits[fromTokenId] + const stableTokenId = + values.fromTokenId === NativeTokenId.CELO ? values.toTokenId : values.fromTokenId + const limits = sizeLimits[stableTokenId] if (limits?.min && weiAmount.lt(limits?.min)) return { fromAmount: 'Amount below minimum' } if (limits?.max && weiAmount.gt(limits?.max)) return { fromAmount: 'Amount exceeds maximum' } diff --git a/src/images/tokens/TokenIcon.tsx b/src/images/tokens/TokenIcon.tsx index 6da52b7..c4bd19c 100644 --- a/src/images/tokens/TokenIcon.tsx +++ b/src/images/tokens/TokenIcon.tsx @@ -22,7 +22,7 @@ function _TokenIcon({ token, size = 'm' }: Props) { return ( {token.symbol}) { >
{children}
-
) } - -// A hack to get Next to pre-fetch some images that may not yet be in dom tree -function ImagePrefetch() { - return ( -
- celo - cUSD - cEUR -
- ) -} From 09bc3f8ce102302f9be6c36cdcd8fda481085d9a Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Sat, 20 Nov 2021 11:44:36 +0100 Subject: [PATCH 26/30] Add back sizelimit check --- .gitignore | 3 +++ src/features/granda/ProposalForm.tsx | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 1437c53..0d18882 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,9 @@ /.pnp .pnp.js +# Typescript build +tsconfig.tsbuildinfo + # testing /coverage diff --git a/src/features/granda/ProposalForm.tsx b/src/features/granda/ProposalForm.tsx index 29f250c..415c314 100644 --- a/src/features/granda/ProposalForm.tsx +++ b/src/features/granda/ProposalForm.tsx @@ -22,7 +22,7 @@ export function ProposalForm() { dispatch(setFormValues(values)) } - const validateForm = useFormValidator(balances) + const validateForm = useFormValidator(balances, exchangeLimits) const valueFormatter: ExchangeValueFormatter = (fromAmount, fromTokenId, toTokenId) => getExchangeValues(fromAmount, fromTokenId, toTokenId, spread, oracleRates) From 6683e49813f86891efee6d239ea9bf4e6c8c4b28 Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Sat, 20 Nov 2021 13:17:27 +0100 Subject: [PATCH 27/30] Fix minor issues with proposal flow --- src/features/granda/ProposalList.tsx | 15 +++++++++------ src/features/granda/ProposalView.tsx | 20 ++++++++++++++------ src/features/granda/fetchProposals.ts | 2 +- src/features/granda/utils.ts | 6 +++--- 4 files changed, 27 insertions(+), 16 deletions(-) diff --git a/src/features/granda/ProposalList.tsx b/src/features/granda/ProposalList.tsx index 9cf9bd9..d65e0b4 100644 --- a/src/features/granda/ProposalList.tsx +++ b/src/features/granda/ProposalList.tsx @@ -1,3 +1,4 @@ +import BigNumber from 'bignumber.js' import Image from 'next/image' import { useAppDispatch, useAppSelector } from 'src/app/hooks' import { Spinner } from 'src/components/animation/Spinner' @@ -24,7 +25,7 @@ export function ProposalList() { return ( -
+

Granda Mento Proposals

@@ -56,7 +57,7 @@ function EmptyList({ onClickCreate }: { onClickCreate: () => void }) { } function ProposalRows({ proposals }: { proposals: GrandaProposal[] }) { - const sorted = proposals.sort((a, b) => b.approvalTimestamp - a.approvalTimestamp) + const sorted = proposals.sort((a, b) => new BigNumber(b.id).minus(a.id).toNumber()) const dispatch = useAppDispatch() const onRowClick = (id: string) => { @@ -71,16 +72,18 @@ function ProposalRows({ proposals }: { proposals: GrandaProposal[] }) { const stateColor = proposalStateToColor(p.state) return (