RVyXML#}Z>SGo4jr*65zkf(pBfQk|
zswBf?6hd2eN<$Tz6RlzP`3v#TE=`#)EwwPS(({_9@Y{C_#*a=1`;Q7AKH>+d*md@#
zJsqs5yBy8BC`Jc0Kb4TcC%XDJC1;N9ouogJpKA;%
zihVoa=IC#W{8f-KYU#?>|tWyzP(_Z7V9W2>=sZy6_8q$TGakl
zM#2MIT2{hv@ec!1K95kwRQ!MbT?$g7|L#vcgJT|S{7$j7ei+yNr@HvfRL`HU@A~IS
zO}l@-za4DabYpt%NJZuFQcqLe#Bcd?=D6F6AxGpbmheYm9Ncb`^*$al`_UTV?;^GG
z6g|Up2S8y$mu=d)LU$)^zOS7d1dy^*wBzba1)_O}bzY~UWxYBBNeR{M6*;5qJu-1U
zGAHd7e?)%RaldCYT^Z2xqd$KQvDdm;lb7OP}a`oZLt&qqy1z&b@h3o#oVNeDckG
zE4nWE9s(6@6WlMCa{bn+dXxK>DR+S9Z7ramms|%SGUQa<)
z*Q}}nyB=1k%n$3JzYM49FKCvMA1p6amO7X=RigSw|5FN?MZaGGP5X;qVwx)As|_#O
zIaiVmvgQv$$MG(Bj&Tv}%IiX@g!gIMz$N{;C*D5Obo6{MM#pK%Q9}_qM8W;RaC#jp
zjg{54HN)3`GVWgevmDqH&%+H(t}m0*GIlNZ^(#8fJMxcDV_!)g&wiGb&wtuF%kYFV
zu}4n;6fW+S&oRP@XL%JiZht`6<8%ee;{3+K>czEpOT3rY>>5m=;zcfT;nu5K{DSjZ
zwaQxSo80Rap6HlnA9&oD@)7<1&50#7A!8F$qaD9l#O;nZpUz1)4p^6hf~v78+57Xc!e6IPD|O?3#h=i
zds=1OBId>iY#41&C4B`s6AGfStH!iv;3ryQF-Frtv?=<+hTFz*52I+Nf3)R=A^7Tq
z!3_rgJ*twVcG7V64H<4R+Dab3J#Eg)Gme=5Lc|h%Gq}**zuS?t{2*V0&L^5@y_C9v
z8PPojV!r1rte(qc_1YG?;@s57sC##?H%bIO28Acm#$t-j&OJ)lCOlwQ-7;Lcr{0JCn*Yc@YlPquy}QZpqC0$IUY+fjZ7ai;umr2hH_#C~_O2>bi}s5d
zAh+dytpEfx00+tDBcX}v2T)(VOy4w=&&Z2pSGA=C%4eejZY4?my-9=*;CU&pI5BBG
zfyc6(E#pD4rHA}vKvy(7(P18bI!gQZvS=$I2+5f|Z?fmIFkD}U4Vn0GP$`%TD&g33
zDK>IfasQD{^h%_FQFW-kxZdoOBhcvDi>A$yXXvxu
zd|QWf5ws@UgQaAhpV~jP;g=TToPUQY$H|s1+wln)bpGcrw($r?J!a0l<5NK71YFAw
zpj7hUS)z~Zd(XiQ4(H%tG_%hWy_vIx@hVRkgr!6A92X0o~h4+b(S#4PboCfS(zekrdU*J?
zP#txD%B2)nBakKGMS|XcAFu0fRYI&jtn}KY3^gv=`0h4Xwx)f_KWNEmhzSIE4G7nD
z)tPM_kYOKOuQEd$su_sCz;EwZBZGzb7;_(l%Arm?Te8;bgq28@IKD(MOZXimS@b79&$L;Ix@%UK}KP
zZP6OnnN$DqiMdc*7!azOSd&k5uy;Q`y=kI+u}Up=dv7=OylXeyk64q!BpU2-WQj7{
zm&u_f8}^pS2Qya*kQUV8?+M_&8^kNq{A4ohrv@DUbVa{MU7=4t0QE*fOfu5=IFN)_
zBYTX0$ckY+NTVp_)_Bn4N8OmBwV27tMOOdi<$elgUZd6N0eS@heCV)x2NxlsHAlHk
zM8o)N3qjQb{Dwz{8Wf+MDa-y1;l+((exlVIaYo91&jO2=<%Unts9t(1y{q-nqkVXf
zlZ6-byBCp}PC=9|_(g%mqy-zU9MsT(zj?+JOleDYdv=^FD7@>3FW8rEQ8;{WVK=;0dj=RnZe+Du+%eW1-gTa
zmohvF-Wf7Neiq{rgXGg`b@-j^Ba_h|IX$Ok9FGa??&(jlPgIJ$EXjGq*FL%*AOd8s
znbY^+;5qk(S_Y!~w67940x4K}k#+XPK3$}lL12+P_y|oNs;MZ&-Kd7kTAz&wSEVpYX3x&E5`*ie_ua%)EsU4QZpKn2%1yEs6^eS%oZZTg0nPJo
z(23%xsrluSjbGL~QhVlt>(+P2o=bwCU^6)9Z$3CvY$)10e`Zy9Kz3(gJoxoPCYv$A
zI}e%M?8SjH?{%#DRO?jlvyKh83ud6#*uyeJdsy+H!f?jmkR+F^SS!F&%L;g9$~!rj
zO&Y4PN#zLUDa}bWC`yyd7>YH4-DZR=0zdc9y1=h6yF0p)hsN8Fjqr9NyIfQKYl;tU
zkWJnQt~Rsid{M(RM%CT>%0NoA!hf(lvXkm-3Z&85C+h5UF(^IU0(LY9%3kl9Pwug$v7LFVQk|D|J2xG4hPt}WBr`Hg#G{E7{#vKmEjAX_b_bwv@
z`vKq1uK;6>+a+&)(~`u0E_;&S)_zkC+qzz6s?7LiE>z?eZwlkhYHCKS(KLs54tvvd
z;WOer7Ce97fxIA9r=U+xd9>gG+D4+e-90I>o8cdENJ}KbQ>FTcWUw1OX&_Rb+^7w|
z0HSm7jNE^(?$W+mQVd)ez~7GJq7e>LTfD)cyF9DG+Z=Hd^nmXbTEx!Ie0W&;ejRn{
zFE($xu!5
z)V|mT<0RAMyDDG5V#odSXHxF&JXf3pVV~QXMu2sv6f)@%Cj$zGu_eZ6;{gJZg>`0T
zd%sLLARIrb#7nB~fx1qgv_zYa2L3{Jx8(EAQ>PM)(CS|k)GB+ubkSe|1RfjWydpFg
zv?nwlkoxgsO-N|R?{A}bTPrz1jq=y=>ylM=v4*31O>SVF>7UYB6VzfKr1UZ=N?G@FxtC?Fw%HwaBN{?w%cud+}ipJkyrLD)>Hkr$L$Xg
z;u&;i!GzT9obTCnJ1xjU+bzEyHW+BZJ;O|f`R1`~Y2Nep+3$i6$+HTeeSR6Z)G0qq
zdyv=(PIpuXJJ7+bxT9W$@$mE9Bs?$D}P
zsGOmk_Dg%-m%~h}%2!I)pGk^DM+2JkJFs!Jct+>r9ZAmK+Zl90U}izal$+Mm(zg38
z9m6$l2~i)GWio64QTV3|Ugw$4#P(Ld0V?rZxAOG7nbUpz{dPgDj6O75WXmvwv$=q_
zcWp6pkHXSgyf^*g+I^q(C_lXOknFohA08j99?W}lUqnwEkcU2U*nw*?WSQ)kILKk7
zg3l~ma#fP8$@x1ggPGok2&X!jl9IKgAX~2V-Q2j4sgcc00otTIW-*dz0#ZaQ=Y~hp
zLX>N@X&TF
z_q_;TuU0m(88uqj*u1i}4b?l>PB<@x*1xdQDS5aPYRiOYAhY&&rYk2_^Kl(!wunq?
zcZ_<2V}jfwG^sWbR|0@YU!PD`@{eJmgXUnG*_(a0gGOphhb3lKNJ}LQgtNSEr7&lg
zNuPeGTB`H)+l=gEsg0r9OKIJrkw{W(xvt`~WzU(GTMH(TXQ>4TH>NTuoZp2eij1~i
zYFgnjUcMj7(L?ZiJg)Ecqs3;LYsa|`vz|}$K;&;OKT@$5q%SbnP@)^07O%KN=@a%G
zG%K=Le};>Dmugv}EOMtUhUJwx(WT<6Im;Dlt`yI2FW0_=CMtMoGLTHI%bZ4h@V;J`
z%MTntm_(*#ivxjBy)xUSZU;P;={^}0RoH9??PbatWgdZed&oHmz;7)QZZWrXK_|Gn
zIRW}QAyf(AXDeo2tdJg2MX!JSw@mw){nav+Ve)1xEdYLi|M!4BUSZxb>4
zqVt06B@?lYR~zhLJx)z7pT-c?p5>W_@eLJq9lhCfrF1)KY><7?G}jsD+WgM(o-i#M
zO3iF6DZ2G3J@sYwBLZX}S_8L7Yk|5fYiIYhV=_eX#oKZF;v;MLD8e
zunb#WeZx=E*|i$p-K=nBiItCZN6L}?=3~DWcBO~D;WO*?=z14Ek~Caw#3Z#NYOkD)
zm}s$CvK*>{>-UND<#l0w7~kDh%-b=2-S><-+YvHT;U3zXuRWb|#__ZqQ%y;yv(Nq_
z$tv$8=Xdjt8rPks@w7!r2AgY%Hz<*+ja^Kr=tZGvd}Z@X_}q=pZhpKsFYVjli?DTm
zB^85V_=6u*CJn7G(mJTyp2>P7(~4Q>
zmq6#b#AeeLS>Lkd2~GIL;_8E#NI|KQm{f$=zn5Qz6#Kwt!LssnAS<2m)MH?`(^8l8
zPdmX~kct;+3?8kr*}uFdZ*wGE;GSpE&T)7hab0j<)bOnSD(
zT8};jTPn3L^m?kcFs?Kgt=<76*y-vLsD(r0wY9{p1*Js2a)S;9P=TPzd2tpG-Ebd
zT{Fab6^l(x^%v__{DRuqOi0{nZc3n=xNGr!zsv3sN)MM23a7k#)f?kTMxV6_
ztB{8BP2#;%Q`@(-y)0F^i0Us^W1b1M=!}b4>)v*~Jt4%Em*Q(rG4~u4erKL(+2o`P#tP7g8~}}^J<9VynwkF
zj{M?ixUQS{@7oF707XWhJ{ac$^}I7pi7K=%gT;XDJ)A)Qx0Ydz%3z;wH1ZYCZ|%eF
z2n~Zr9_y8_lieDV-Tn<*aQeM&xZqzfOw2(rQsk=U)gWF2-2oZBsiB$-w%9}@nvSG$
zeZ$ci(Cz>1k<_vQpc(P%DluNcUI6XQ;o91`An^qHKyvA(@n1^CzJq&4a6zq(?f
zrL}tzbgV<$^#?%c!h%S+@zTbI7~+x<=rI60L!2z7^JM9_<{zGCIF+u&lkOybl3z5^o0dp`hl
zZ}ADr5f9}|!mYPXSoE>>kC|?*&2Klf`Yjb3O5pJ1*3VzZq>SpmySQhoUREXPJ5xb~
zXd#rwDF8=kN>WMc2Rvw=zQJfqXemKj@A$fXhdJ4@#DTnu=Rk)K?ri0&E1WsAf@w8&
zO{ma+M4^MJQs3BuU7tB9y?pudc%$>_0(C`=XTacf_+p>pgC~PLPL@njePG=YdY2Yv
zm!h>N8&6~iA@7Ww5S3Bm@&jk=u4{#+e>)J*4!aav%*pLPc`ch~(w^cpA9&DioP%~4
zW?UhlqO+)g!MD!Ah-BB9M6=N|Kmx6_9g&*tg3fVO^J6>DMq^B3`S62%{Rq5m&vG$utRTLU*@r3F2z7(AyV=yEEDNO`
ztXa^yOcNG*ds=#|bFi9F*|*L5my`S7)pf=S9W!_!uxGb`i@OUeFFy8ni2LX!lC%p`
zv-XG?bcC_&efD>)dQZRU5=w&o278UeT@9&dWPh-+xg;cb{EU<>6ra2biDTU5L>yL;
z6r#qv)z8u@X%XRrwZj$Elio9Ykehj~Tv=wXYShtj0vY53y4GSJ`{S96arLHEMuBd*
zPsLNat4oddt!Y9Tv}~1}R=7t$6O0E6y$A4?&IZxX-*j+8ipIl{Iw!1e*%QuKVPBII
zrp%|fN3&;UNa82lbnI<2T~6o5e^qfRgz;UoI$a+Qk8tO6^&feNnzwRs3~280!H`!q
z=C?hG<%q^vM0Fr(!KX``%#n>sS-jY_Q=wrR&KP}XVx=85_j9P()#Q=nm(-hn+zA%$
zh%s$46R^0!h)%FYhe}-dDv;1FX2aG1X8VhbcO(eV6x4T8C9(MYxB0HOqYj;b(FOH)
zM&J(xcaRb0OMe{zrBwPaW^ZSz6fTKhduVfT!r85uiQ;gTxQGf8aVcjsb?WsgSB&h`
zP!hg=`{ouQ>Z_{1^W?G%Po!PE3~q}|)@HC~V}1UBb|yjtDWK7trV+)fQO2JyHz}TW
zqCPy59$%bxU+o&;#UJ&@Hn$T9n>~@!*_~=*JqGX3XoEHaN#mDm>-#aFucLvU)5}u@
z!9gVW_Xk<*UI2{0>J#(@
z3sYOqNMlp2*dP=XS$i7gfCeV^15fyJbNh56zAz;W=>mi9=DEV+%oxND7296p#$nm~
zAI-cje+31?z*!0pG;rkgeWs)j6Yu|qvQ}kI_-&^r<}2tsZuvNULaZcypP1;5S{J%n
zLh;Ko*vuT!@vC@}KTuz}CJVfhsPP?)8cCh;>|hUMQ_({Hv~w
zleI}WOF(#da(mC{!`bAXD;s^o+5U=+LxUuS*8m*l6%*}KOk1BE*X8Iqmi2X@2jDR8
z=!j^s0SfRyy}FAPPfu|cz@`6Sgq@uT10-4q6Yv7_iJJ*woLQouGBvdI
z><-c1DV#g4+yYrhUqO=Xih8dwfhVq)$zX_%ZR2(A64Q=BXv}SmO`C{t87}V&Am}bsHD5S@pBlb0h62Yb|DAjv17ajMN`}7?HKs;D3y-K
z-kE`Ftf-IEmLw2JVtI1y+HhZ)Gn$w01v_=tRsI>ZTvy*@0V^7$Ya<7(!*{|PMBG(Y
z#^zzURp;GCEbeYU3ic;!+IVU95md@i@DG{&Rq38?+hAqQR?qG))Zh?P;)To|_r3z2
zMO$N{K_*VoNtdBs!f6pjRpp})07AD5?i=&CN%}jenPiE(90!1K-EU!LYF<^Mb_1`O(FB1NJQHt5D
ztjUe7+fvZ%B({fqOn!>Rh-?2&PhUk}OI^NFt1c=qIMt$%kgvDcm~cPJ&Y@Qo)gi++
zA=V=2>i4F+_ZeubF%v0kHoKbbP@5SxsC5hk#cr){J6Pfwtd^**_5Js?`*b^0<)F0L
z;0SS0I1lA8$6x<|kVNx@7$t%Bf+)Jtg_ykVn8bCb$MRKAZ-{XJoa7Y#udUg?Kqcok
zJ^;{tAw0WgXQ!AGmPwXgMBhTsdU|^nvAsKp*B7gl)JR=Wkj{Ql>kDXF(-VYI0ns8j
z`j^uCsPvl6Rh0=!DJ3Rxk^@!JNh`z4|D~9Ty|wn#=LSDAbqE(TzDInQZLsa&dvtBG
z>=W1O#w$5O+uHe@`-r*VQ>z^qlAt%zNfv+l!_nfbym@3N@~B{B>s$au3uL5o
zR(643H^OpLVWFwD
zQ%u={O5Mfm(q@y}335iFZ@rc91N$)#<9&E*KfiYV!z~^lcaPBzFZRjg%e!JvMrgTP
zrh>9_!vl=2Xs!Csg~=lGt189GC;BBbal6T=C@(t;(KjveA+Y
z;s(vUXTfR+y>@d!z0PvGCsIl#zqG$x6ibssRZ{Mpz2f<%pA*!#c)lC>p8*ql_LJFF
zfs?^7-9p*6#om8Dn6^@S$eD7jYh+XJ5B@8TKg~h(
zOTVg^l7LApgaG_|BE97L6_RL^k!f!|IC>P;X=0QO;?TkRkfL_`UJn$^E4b6aQ4T<2
zhN9Lv*d($Z7;7RTZYafMrahd$_X)w9HDhUO4PG60;7mxp=WEu_AW?4I>WUbfveFy;
zF(xYiJqo?gc5;3xl^lz-wAc16@UkSGvj;
zb(3wrm>69_v$FLc5EQJ7dIY++{VN~#WG^me7iQL`z^m~Vw2-E8?u0V
zj!S?jCRNpI_b?nW4roy?w{FlC3E)&{LW4M1f8b`)*{~Ch4c#0)jqzYVv&&7u3``*DY9c3=57m;Qf0K7dQx1p>OBZ!r}iTH)e(vq(Ra8F9zm&Wzv;{hq?>#UE}89lsT+66y?Z5W&z6}^-y%%279bTViGBAV;OT9)CgDt#MXD*wl-ko<
znlX|iz=zfN)-BUS7hVmcE6>;zER6?Wcmf9)TtZ8VP?4m3tS1*r8ZZSDC^$9BlTnI_
z5+8ASJyZbMeqGVa2kdLI8_kE$7C*tkZgMH2dfk9k!h&VS?}`hrOuG)Ao+Yxo<=n`C
zOCa>+(vcfc0gA1y0M7_7)R$%?>)*cWj<+`8K(r97Qm?
zQEX!KjUv;h;mw^PYFpLI=Hhlf2215KpVrO&I7SHcLfp
zmi}v+9#2=dqf2DcS^vxc(b0JZp(h^LYq2riw#Ud!M_;c^o7OjEVqc13^_(5JfxZ1N
z&}H15RwmaK5ZUD2LG2C`Y8q4B1R96~wtS|IY+zPhOk|rB<|yy&S6sm}DJq|8BJ^`N
za4xWH%wW_OIw8S;R^MzkM%%aQj###0ER6j=x}$MMtSujQG7t~!<^$595om5jfysOB
ze7K0J4iTkyDS5Z&nzH>9hA6snHJYBHpORS@ZcGm-kw3AvI@r(>-H1NF(o*ATAy-TG
z3yWjNL*B;+hhBd%Wntc`v3x}_439hA*<~_lZJ}nHqvU6BsRsth!r=TiRrP}$@r%j$
z3Y<}*50hAy$jhkI4`wN;_$IJo*az6%U~sa6Mnf*@nR|A6Ds@P)81B@|8Rx}q_k~)R
zJZxRke|md$7{M+Y)4k>%Nz)W4R&g!~{qiz>zbT5%GRUNrMZ=45N?n$+
z5F-I-wwpd(sz_;UVZr>RchBzaxCS$GWh6l3mH5CfJN&=H=;l0p7n9+Lj!zNFS&0u0
z>Ur_f1s_X`RU$FC#Ps)Dd@F7+fbF>;cruy(OCz_y?shY{^c8zSFUj^Ku=&<;4jfc+
zh6%;O=pQQdB4La3K2f;TltMdHhWbAqFjEAxMf&n)n$}}HGjMQj=Y4~cs|!q!NT94
z@KKI%dpRA0p;&r}37M2JS>;s!F1*yXXSi{^F`^4WLB
zrupV?fMa__@NKaokaoK<;N}v3Q&R;e(PI~*?PqVntw|;byZL
zCn3jP&iq~(u7Cq#VYMj}iA9#5ap)iM&1j@Ogmz~sKR0VKvM041qWi?YmISrtc_Bbm
zTsY(61}2z&YLB`QU{Q&R@a@a%d}?qDEvB;Cl$?j{rIeOy!GgI4mm;#FLKFAAJRf2k
z_Z&GWF-cd8WBRf~OBPFA&9IP?lBctW;W&M+Xtu$nM(V*s;H)r_1?}!;Z
z(#7yG9M~!c;~^eIfRmO1*P8=%VfIJ_8lj&3gqai|($`mKXFJ@4@l8OPk{9_+OpkWx
zP-@t1mmpptP+Ai*uiHbFM=Ub+y&z?(%PbV|uV?(Ae@R(9!%ZwIuiG&<70DCck5D6e
z=+mX+dNFEfFiC|H@d8WUk*xjk>7{}0kLov>l-Z&y}&pqx$QM9IF_s3U(kwe$d;dIyA1&
zT}hkbBZ}5ke+`f?5rNq{`0#xrq_I7cf<5r4aB~!ay(`>X56j26`*rfhXOR;Cx!s`Q
zRH_jIzG9{z1fsL~OCNToTgB?Ee6K_bW7hJx?a+jsWP)kikU>LP^So4%-);ce+#UXO
z=bLE{UOo6+!;@B`DhkdQCf)^d4<5{VV{ZbGP~@cDDv_nlI-*znvAh9o)(pVjfc9GQ
z>B;A}U2x?FRX^_w%^=o94<4u}o(}Xl6J9c^P89T9_q`2pKu>2h^nflRY%MFeiSMZP
zc1BIWth6Mji7z}AOAUI};Kbc9kx&eRr1SIhEsw!X8|Od>U{{xz+AlhM{`NM$d>NOl
z36{+2rNfq=>~1nJ?kUuAf*l;zlkpArAR~JVl!P2*|1SwwR(^QJ;-jGYuRbFfN>l}t
zw%I5NAo)LtwO<-ZyPB}Wa3s`-sN+Zrd5hT6_re(Q!+IyZpY)f1^W`3F=gg`B<2e^4
zQuzzZ*{B)2gB?X617gO5C&a`-F)mr#vor68!HxC*A|aW+-6`;afpK9V+8r?$)<86C
z@&Ote&;sl1no$u>V-`oTn>0Vpi+jvoM1SIrXiyAcLkQOL*MLBA#&U?m7;@287t89u
z@pS9vZ|=lmnD!3S43q-zgMR-S?6Qyld-ya?zlcl1O2w$Kvh0AnW}v33mfn-$kn-cK
z7~ephJxgQ;{mM8k`V{zmse`?hxhyUc{#oNOrNlgfwWkS%DrS#
zzvth-?(Wp2`q(Y6K5aI!yRHuf`XArb&p!TsLa!Y$@`Vl(^vLc9zfgX)FLSidi-`eR
zb2&sIR1Mdz22CLI5oWIMn;songU4xT3YGkW{QVuD{(y9I
z{idkuLbWR9#OFUi7*}S6vww%fKYf20So0*aO|xNTg+9YS83PR$f!;(ylN?D`aA#IO
zZC6&u2@0wwtG^y9>mt5)2j2U3S3x1{OnEem{253zA%1x)3XQ}4I5%Fc9f@dDr$&88
z2Q`_y^Vk&j(jnZREI{!;y9$Sh-WHdi8yd@%d!SfJD=utEE-m}7mm42n9{>4mwc!1C
zJy2oeC;Rqp;Ptf+^19e7KlcB$2ZqYiqona|c73?f|M)Ilo~l3c{osIKd(adzmF<&V
zP05FwMEeX4a!!Ghisk>Mb}oY>Aa41e3Fh+q)6UjN*5XKZU#iJK&)=h&A734E^KYNF
ze*Kg%lhtc9DjvVp8_Gbfvt{^2SLhCwuYCFXZ1z7L&yH`vnd5bFnpGWtl(+b>{0%Kl
zaSPoWTCJj4nsv39Hwlr1qZ+)j!q$l0XQqHxoa=ZJllpL`>jD}tj>n~^URf_J-c}GIqR|D`0
z0^HCkYG9dz3l$?expc=1FDjVJ7!Kcxy_HNJee>V@o*cKbuC!~-KP0$Q(k9=Mz0Bjw
zEk{HjPp09lEiQgo1M=0GI=2jLS>xJIzdYtG#b!@<
z*s_>6i+?0!3rwxHHN9B+A`$eQ(dV1_1oI~%6k^Rf3Cm7oe};lohQ*I$J)9mNBa$wg
zzffIyQ)SI!TC)6{uo`9%;mmqi-0MG3&)Z#gg%q0Qq*f5abGH14B`=F&+!jnP5R?+*
zb^pUPTI!`3lDM4Cr<=5Kx7T!(YC%!L*h-^um*3!
zBUo@n)*_vI0g;@(Q)jBM*}BRPnn`S7bZ{u*dj4CfWw0G!ECzbKSDuBM^=>PFHj`DJ
zrL!yK1R%3c5G?b#)U{Z2GS0?DU|B`+)kSZ2ImjHKTCW%#@~(61##51jCU?n=t94<67Z1PCyQ~wLYC}6n%N8DRQB@UA@}I
z>Mi?2zRd6ZL=PuC`Mk<4Rsp|F_e3`*X$!u3Lomk$nnZDiF9DIyapOnt2RV$Ry?P`)
z{34>JV4US)WEeh|#XN@tiPGvPvjniNTgx&zpQ94nN`zd3o7ka!|Pxf#v=SYS)b-w{uVwM02|ekaD@$wiwm
z;;8`Clt^vjl=0_dnllaSkQd02D-6X%=bQ&4X{HEKyu8GI}q+XI`l&+_1Uv%#WNAW>@gx8yD&h}Tg@$ELZe;UO6&IYsllfdqKg~fE4vuo4_dzC85JF*?W97L)$MpuL0m-dQdVgR
zPhimy4ox3N6y5Ct5#Jk#(A0O&z105AO(zi~X2ouHEvnX7&yb3%>zoSB(Zov*malf>MNCU8z4
zVOAh$Faq!CYd-IYkL?{IMvVabG1vmvt~O54HD~*dC4Ary6$(;mgXL9Mq39d_lvvgE
z-{LB2O5q}i2(GcIZUcLi2IGdb@7wiaq_4(AokfP?uI#h4}S%=*uZRF`CT6;#7e9
z{cq{N$Lzefvg(HSVc<*==q0vc8KL@^3&u4UPf$+-PQaC$??Css6~
zps-$4Dd(%d(sMxSTijyHt`CR6hL?~R78o{;tWSY~SOIUG-@*{aZS!k?ZF$&ArrYl(
zdYc)+4@*$fKCf+D>`Ap4gMV^MpZ2aU*kJmOo8#F@ejlasTb!wZV_ssP>$exgFXvkE
zydv?%?n*A5?5$=ex`wS8-L1tQrxSCotYz*WCI*-u-XHvw<#gav_zy-=UezDq-_h@QV<+
zAog-OnlcgWYXcV>DY3U)i;&WBZ60|)S`73&Z|Mp|e{?bE$>eV-sSrl!2!@PNNq6(G
zuCn~4z4utoSxF^8HD`fI=aWkz*O_pvBQO~zW({_-9aaC|a?%PnWL)RVb^)F!-BXgv
zLPB$OLo@&2(d+cGd-&_B9T%^Di!F}KKLXWsEu8>AFnPNw1i1Hx|CQ45UI0_VeFV`>-}
zdtPa~nniq@KRs+MbUal|reuy)%K-hhOLTkl?|tWQ7nD!u)0~3NX$5*o^L?UM8Si1n
z2vG)Zj!4fOWvlP{Y=iBTIMJqeT>YZAdJ|?Dn|_C7|&RJjl_`u@aS28w^fHY^(VjKL;RgZtcoi)~C=<3M~A^`tsX6G=>RY-l9LApZ&b
z6yn{|T7#T>FaK-vpE&;2TiModMnX+QvtKt`ZQ-2sn~q9-L9E~n&4=Nm{PUxbn9{_y
zQ=`yLzh3c@%^{F8_Vm~5N1Wmlf&XRZO^|tPf_~e)_$k(M@j^#plo2aBsdv4-^>A}G
zus=oN666N^kIfAjmzB>6>cV(-VEIls47x--Spl-`CThC|LpxIu%+8{$L;RAR(8K!+
zBxb^oX0B^bbN1FA>Xs0$x2^p@DP{1&ZS&q*s2BELuoxSeu5=zB7yy7I(jo;~pL##m
zXmzAK(zqeMq#sLPhFJvyA>x`W{<&uz8%ld$;eT*)Imai?x8+qQE`xL3d0HjSLADy1
zEecb=XTCDP+a9+U&mNBisTG8sgr)3qB&2x+3j%s@Xbq=e70A5<7-|qsZ(@dItX;GT2~FX
z9?G!ScnAmn@R64w883T{s>Sw)Vz$YC6AGm~g!is=IIgU%W}m;JYZE4>+|e
zC>kyC3qFq2n^&%xJ#%?9FxA(74?Jz|Ss%I^x10wFh+#-vF4cEB^BH=xR4WW1^llD4
zFN7yokYq;oU*@h{&pFVZ34X)bHbbf7>0qMy{gbLJn!ez<39>=RLa1zW&N8YuEaSZe
zFZ#vhx+C?t3JFpdhX2X{BAIdPSc#}(eTvK2bY6P`;*kVe#yi?b{`HuX(ZZdrE{5C6
z#e$uVTCttHyV=qqYp*uubW#-FJif!qPBUr&YKZ*~ifC^wXip(ac4ca3RRAZ0GL=(3
zT~@*A!z*CE>ob+M<~_3S!QVt7+jo27A~PcQui4Xry9JKhfygJN0$z
zkk^-cg+$i(Af4W2-Blws`$5QxTf(5F2xr!hDX2PV13W2I0x%i4^mmdKPXB_riN>qE
z!aX{!v+gdo0?qNWfTx9?7r;T2L$rk8)&?pK;AaJN;xZqK&s7B*j5P;0z`vrO>-VP8
z0#^#L8Kyuh2gAXXD9uF<}d#un(p-8v{bal<3-#eeWsEO#{I5z)9d)@{rA>C#+gxgr&Y
zhSgyEK>o_>O%oo84WSuzEL}7sRp;EvI{(UN8Ux6*VdkNz$f!Z%6fRCyhbPnf%P;@iP0e-o1#!|?(O88eaDv3nLlt#%%|TLb*A
zo3Xstnfyb3lCI>rBeW^P^F=wx3H^a5~9XHB@
z(TNSQZb-g|a%Qd!rTE_1xP~6T^U@qDKQ
z;GSMz!JRZpda7(lL)}rX40LkfTMPe~lnaR(CEvM0mzi8&lixR7zz7*(CC_~13pN+n
z9Fl@0b1*!_I4g~t4oyZu+DA@auJ%Z!^;<~vQ}_{+lML~&cb*8M+dp+W<{@oMvf9<`
z|9o0M+0Gad6G7^mTIT#Ov~8HMe|~=bot(lECHu-pjz|mKJb~5Tc*(7%WH8jGLKfo0
zw>K0~2JMCKa<+e}*uq3TFN#V%mHE@-ziw$9#rlYXNu=@yCzI#GH`9`O4F+==&A}0Z
zIkz536bQ}NR2XxCx4#gc7KXmFP3xBoof;F2?_J7uZd>GtHuj)*-UpBhEYB{5w{jdYX@P)}
zWi?qmp1~(C=~6}80r6vNnMs{0!r#`PyVm8iG)^qlZUFyh&??|rwOKUm#qcRqb#9lR
zx5P4&fFGo|cFjn#YO-)%(wQi&Pd>SsGA@rs^tGDsO=x7bjSnV?u&)a!?QZ-HI?W2K
zh9l-eTGx`bAHNGCmVFBx6^xb<)aCM(u)EDA77Li^!HbB%-q(E`~-#i9q`PVWd#pzw{L7Hcq+A-jbp%5CmI){{H4>s#)))jls%sePi(n
z4Jd!Vch`^T`Q_|s#?ERi7ke#auQ@PrkwJEd9Q$scgK?000W^}#08Ll_J>;`N^czj-
zPkGTcr$jT)=6
zP#bhnjZvxoomsQX>3V@l{UP&D0*j;MDI6Q#HDhO<9
z>d2?7W1c90e1ns&LluwAcxspz(>}CqechTac`3E#7Z*%uyU99%-INGS8jgOWt&}IIcN~lDOXBW24#JvB0vr`M%wh
z@7PS!S+@Pu8={j|d#{fDmJV*A+8OgXb2?w0bBAD~OEe-JDIKdbZBQxQ30Io+q@fI9
zB-rNq7T~z%7>(%!Ai)uc=IdJb8;;Alt-c^ho3FcI^=9YYyQA!04C8g`^)Ak5sN6N8
zv!_p=fRE~IyG@%-UMI6bb5NSQvQV8ebOc4~GZ0N>P(L%h_7e_mOb5{~Uu?dA*d2D&
z=Ii!x`8<6O9L>YD^=?)&;9W-^y@&F+_U>RVZ%wQIQMBj_pN`$;Tr{j_k1<(aR2Oph6fg)Bi*`8lD4k6_T2)
z?$$&OY^_9ygu`AlocO%Q4aYuY;c-oFp+dqeJ)K3TS_VKJfQSpX9N+{U`Fuv@cQ8}6
zDPY+P3(J`^XPGm5M&ybUSO_zQ=d*ridV~{AHI0@f?SA7!*>tLSNr;RAJxX)D41G|X
zZgqIkIq2saV#fhzll7$CNiQBhesWgYd3W$yRGo=MGAzuoy~eR5WbysypFXXSe7b*Q
z$&ycELmaw1eKqRj!-vzSPn|kp_@MuhslJ~9(Fy6sTDg%_<(Tw;I9
z7DI4co<}?(Ju_Xd3eN9yPQJA|{o>5ghR^Ln7?^|!QrOtNy*dNu{SnGA=*;{qWJG0Z
zYO1olwx1=J#}#M;XpIypU}
zGk>wi)`-pK=H^wA$-Z-k#gU5w6B84YGU6d(v{+nOjgzDbB1b519L8;quA;R%+#5GG
zy0$r#+K!@Tt5dK`hmyhZdHf|CBRGiQ_{{+hWLOQ-Y{hyWL%NgljO}W31}q7N*cwT3
zv4<&U)1*@QV8{K58xyr*1Z!t`vQpuUWCEf2UPdT)7@c&&F!WBX+0}sfuo?l{l2|oN
zH~476Ro0Tb~@&RBNh0*f@85|qz-NEDfpX
zAr2k=W4)G?tWJ6Ce${!%x3P(95oC}lECP1a1`!#=d}VDe*nNyV?|fWAh31)FuUM>s
zCq{qr=NXaa_{E`z3UOzt-so-zvaeLDM@XYtw#^EO<6YEIBL4=dvc=h&R-NH%x?21B
z7h9Z8P#O={4!>3uU67#=*ShBB))7hv7frEUfHGzxs
z@U-gOB8~Wb|I4(9;iTQlN2i&-&dCE$UXJ@Z_cQ)woqeETEKqS4K9^IM-+Y}5*cdI2
zfH*q9;rP}?dbnN^)r}6yllS6aUv|#X;)svs;f&JR#-+A8Fmx^wH?x(oI=Hp9v7kIY
zk$XL~T(w7`8JtN=!*T5s**{ha)=t~|sa0qH)6F0-I;~%gjk{jk!`>ju1D_shqp_l&%
z3A~0~&Om!UC2!J^YoYy%ZQ2|j=%l-MVZ(QG6CUUY&i;w3Wz(JN7I3_4ZhMkGj%&2G
zwzlKFbEI5mqm9JaI6B}FvJ2^Yy!H2tnb7D{%~xjxG=rMs0+pKGW>h1My2uy0)(*Hx
z!1e0(Ob}#N7jE==n}fsHlhWFpuW)WHEzWoiGz@__vavdcfb;akvJ7l9#PQZ^agy>}
zLJSU`G$ewKQ>5(W=nHBiI{Yqd-pJ?_Q<~Lel|x&;Bim5utq2AbC+ea$rR-4V7EFZ5
z(+1C2NFBd6?wRTAQK;z1L5?n_zu?Z{a$5$qr6tgTPp+$iJzNo3oJ?_?B(SO7epo7B*;ht2
zs5M!hR;b<4D3Es)x+FL^l*XLE;m%H}X5QE}qjQuxDb7UEjBEYsJmjp_l7#;#<3p13
zi{~|XWwXA%L!&p8hd9nMZO3GtLFsg$>;uIy2e$lI1a~VA)+AgB*z^mbJ2X|42e;y+
zI@%lZSr{GIEY6R^z(M37$A*h(PC8@38f4O>I>(M3hR*Y^9XddpiH~EMb^J+($2sUu
zsqX_Eg<<9qK3|c1xXuttB#g!3D_K$#4wc0fO^r%%uGJa!n&a^VttcvIXp$(_vj~Et
z!&q3SPD^mq$E}XLmfj@}b1h3_7t=Gzhj>d@{jBQOUq5#E$dRK4XL9nzCTJFt>V{4_
z&RUv0Ss#k@|BMBQ`DXt76>bQg7!jGruW*7L4pWg^`c$m#%XLK%hVEM0Z%&Nj=uX_g
z*6Hwmg3LlK?slWVVqj>4P9acjH`uFd=>>BXD$Tj^mD3!aB*5Nq<(zr5UGNwjbO$&G
z_S0tanHQ6UM}Y;bRzdIxfz`OGf9W;;=WI
zbbwP@J3*UQj1P2xbL8mrhs5TYv-Kna&Vt2xXyEyq1R)M}Zh`3-=jr$(1&)yrULgDz~uM_&EOvP6cC`j}zb>
z@?J544slQ%v3Z8so5YxktGk2lE_4rY>=o*CaI*~%BOqbpLyg
zkUvhSI69Zy87D#-yoJE-37go_?QDhPh}qM?eNxrC3~S@=QRu&da}gdc&d(M{-ar>P
zw0ZljL;D|neDX2UoIQJ$I18L}{QK8NQW!VXF+K#cMvPH0Fiu+}!jm2~U6M6UE`cX9FA(I)aF@4amGLc9PfggHisHEYGZJe#*6PF;g?dJ&5N7)PdV&;
z=&cC6<#034K|umjm?R&CwSsUyR&ew2ReEbdW|&MpE4Nc-m52GPA8pLY4gt8
zSsQ*x`|+!w!Gd53k>#3K9W9HaKEt6G;@mwp|2n>pE!p>?c`@)!p6{~ID6CP
z?9yh*F4x@ZXjK%3n~rMM=8SB9VVu^u-9d3KvN-j9`CkqFzvcwER8#hM58~2`)?JGu
zK-@G&M~_17$u;O`U}JWFe(nT#*c;e<4ja~nG|chi5pfDj(i{(Gh&Wb6c@_s3hctM&
zNE8qo0aBebz|m1H_em$y9Ira#an$X4ICqgEPRnbKBQvhcH6Dcu4s>qzZ7Rd|X3tyi
zoI1zTo|tdgo1fl4NE&PN0BB|wI!n?Vy;Q9O8!b%umERoVcs2~5_?17PN$i|saE5Zn
z8Fj}sL*pbwb?Rih=Y&|U&4{)IvP#xRapWjeE*l(7TT{8gmqurL`7|$tvNojofiyPX
z9dE*Brm*yjR>%7W
z%~2l0h>Ys^&5`2do}vF}yLZ>9f-nl;2rk+Pf>;;~+F2N53OK~BUe?fY;Q_w(2
zsTdFiF$9ed6dz$XNVE`9yRfG55fn+aDcp#H53tyXo^!u%_BcDcF6!pY>|GTR<>xmu
zcjj)gph>3(&VDS|sMFuapK-fUTjA0==!ht;oI?Qz0tSDwwQEoV+WSY4aTs*V!zOHY(S|yVwg}>E+Fdmq?x0z;!x`OyMl{aD
zh$pQauA^x)`0VUoIjkK%`_z2%2cB>geGJbWtiVx!mUMdD=&Ug>f6tv(Ayk17L#6NE
z_|j%;BzapLivn%8KS_SUAYn
zuAH|4=W@|clBdXv6VIOUXB8%o1Em0!<|lBVjw2TwD)zv1BMQh6uP5UjjNh=4nx%Reg
z^L9MY#HpbBOq8`{a=YX($}5ve8m9JxPj?_3+dA%Q`qd0j$YLpM7?w?a-v-g-d6pv+
z{g-RXk>%MN^%Vl~(dgCHFUTr>!XK{GNpnyr5lq4ZoMU#QGew()H4SQxFdUT58i%w{IKojf_RM;PL}&!E
z*t=p%Y^7jk;~HbPIRpYv%F+jh`Sc0@W_&_t9pmNDfN6$GMuY!=dG<1ybovCN*zzod
zYIREuxiLGt+~YP?6OVX=!y6s^kqck%$c8xO@?zPti0&;biw7^zv`eVyljg!s4ptEB
z#-vt=Bb@*Qp!4NzyhRgzmAAry&
zsk+#>jA%M>rjERPt3l0h$TCWipo18X3(KI^WwtVitb|Ob?8Re5q$yU_
zcyk&QP~c&2Pe<WqJfQ-jjE@@SB`E49o0Lqndoc<2KGi
zAdk;IIbMMAE+4Q^hD`PxdyErwWaBPu5V6k2uHDX09Mu}8Gdvg?){TnB(+W5$osp>#
z^rZc3;pk4sqOT@tI2zO}`}vE_<*R6pUVS?CF?7MsR)RdP8;3m#Dkh_praHt^2w~Rw|R1O
zUSgb*-IErZ1PIG#X&M+10ukakqCw5jC6O^5J1DqCoWQgFKW=Q|)iayN28@}JHV%Cb
zcp30by}MELE+BT%go|_%oHg0Zkj8=JQ?~9l?jxfpU8fxmY|U!*`t|Dd>T^5;x^(^=
zL2d&ndHjYttQ-QRKCmgyrW=iU!)cUu{11&}1Vb9yAe)7Q(-xa_B%n9-Kk7(GDu1Y)
zHQ>nRUSg9hsujV=C$|<-0t-%g!nlS{eO8yM0AdI>Q8VE;&S|^WWtg#<ze0O$Fq&6HP{f2(MVt
zZ3N>qvN_bokL?_k@f=9MDHvr$^Bc|`6%KUp
zB2VvCn<|Gqp^3%{M>eI&kSnYj1vID|8`Z7`8?mtc9zwBrUUSNsmku)Y7dJLc?;*`$
zL_?fk?c7gjTvZqa@D3;iAyDYbLNvQ>-MY%Es{x@9aGOQl4hAha5Ct;}LDWUljcBc!
zMWxz>fi7wx$e@dE2VF@wBE$-b)Cz^PX+7tD-<|Wme6KUDR;g{yz3*n?9}4|(?z=OS
z3EnmH;DevYX9%ra3xA#DGNl)|C@;istTF%*iL1Db+fnauLlKXZQEpwyB*#oF@Y*cF%Dd4%^teqQQtKwVJzL9Ol++86wKTQO~+|WMl=&;#yF(G6*Mqw@NKJ@n|<;sVqm5t
z6b{LdMmRTqbsVFSjq@y8b!u^5^70&4ad<(8Jbs_ehXL(t$~hHB&1u=wbk9L^d{^7p
zs5*u-JDp}`-sATi(gdFFy>zbbEsm)WM^oYV3Qaje?7xTEC#Hr7sN#I`)CXIRbDhFg-t&^=sx?<^u~JUHyeP+|+~v8R=jEhTT~t4K8mE
zOb{!N>vZS2=3PxW?J}`A`7l76_Udq3yXG>yd}o`PPQ%z+w^hc6_O#R+%kELf2`EWM
z5uj+SLE^y>4@S*Z3x3P&)|TO*IMQ(+%th7)3tTu!;}a&BT(GB^PPzPsBb$hGT{QL-
zhsCKn7-~*?N^?HquI4Gt!JM7=#KvV{Gmp{GsXH34(3_?W{xZ(o-re0jdK5@sLugv$
zL(g0u`DC7}OLUMmUi1VZ~u{7HwbS+O
zU5Gj+L>cobDi9)z^I$SMOP8UZg6A}U%Z^@Di^pi`v=E*D;`uQQrgRS7
z|#Jolu>evTDeyq{mCd-KCW#rUPMB(LYeKNZD^gLi`*%-HJmc}MgIQ#(G
z;6ZQ56J=<_s5eZtxg4uA-G~282}muM89!|>w{P>$%FXfeP)0Er+XOPr)1^CM@e$r^s|hp#h=XtG5a;Te0-e@VfFa1iNCmf^sZ4sgiBTq?nzU^eZ=;q{iYAYFUVwGrdRp-mSz9N8=x&N6)X
z`Nco#FNMUg*ss642MaGur2YuO^)0Ur!j(8_Uz_QjII+@a>{F5;G9KXoW=vCIur8JX
z7*33M&k;@D)-G#b6VC)0MmD68jBJFHn}f(Fx-6#d%%eT?_)?q0?VdcOSuPp&``f1&
z>Nt&lQ3QWAp2jlYe2suC=3_K{e(-m=4GZXa+=etBL$IIt
zd?N-n=dVK~Z*25A)Pf@(Wp!nBb#)n`)RilK@2U5kx=m1rJOI*RE$?d7iFX|ir}4!&
zsG@%^8rg^jGTPN*bG$WN1QUGJoLC)Bf75`sV&mb*<%KgE{rZLT$i`{LKZ}bA7|evT
zy%l7vIqAH1sDabxRek!H(CM#^b9VJC!w)7ikh1dF#s;Cv<2JcDktc3$@?q2%$t0U}
z$DxgMU9tZlfviv6ziyF!E2|RGpWvu{bfrDLx(HEC|PF
zu&;4pZ$N{!+HiP&IQv5*kUGwQDcp37rnNfKkxxVlGNCo8I5m+5CY*Uw(?XH_G$+1M
zZN*sv4aUG|Txag)HZ%alIF8fMCCY^2{NXmaI+!y$t@V1U%}Qfy>yrjTtd4YC28(
z=4j;j$?DVul8?$$eKJsPjBG>`c-T|vs5Rxv)u}Vk(8hE
zXyWgN;HB=o{L9*CR8{g#XI9TZ7kaTbb|w%HS^9-$*=)!oo6K{PS2J(DHfG8?&LbmU
zd?2^>)}-D<8tFurxT}Rt&1k~UMu}zs9c*n%-+18j=PLkW#xjC&6`Quavi&>$uhH5l
z$TaUd2Qv<1IvnPMdyaKy*_(5v$do10a5|2Sab7$58YTuFdKyp=l93+B#br1!)BT3y
zRw|C!7!B8UpAe&=PFWa!lF9=YEZp1LeM%#!;!7?KC$s(g<|c8dGs?{&kM^~KGjbUY
zHD{G40&QxwH&+0|=`^7@8HP*YVJc4nL!8R3f|Oirq}m{G(PABjRzVhF)Y`Lxdk2gNG2WCCh_FV>##-_%HWcWjXl-j@QfgIj6yr_JoE0mP_h8!{ntPI
z@VjqzDC0CtwGmH#sxvdEXP}6E@#a`@oaXGZ?P-~ZgULF)s*7~6s5<78Y{;Wr{P?1U
zTsjAOvW~+jlCfb+s*abYWJx^1$8{o+gnGRN5Yb3+mtzYHy-{zZSbcORU~{yul^i^H
z=+Ml};ls}%eVBNRg$u(yjTdDSOzRydZez(vJU*ighh%KzT)BuR!`0ZQG|
zrX8eRM?NXynOq)Pn~ry<>2sE&EFnt_@9n`P7bo%%1&a^ZP>42hW2;Dk$7X-kctTcq
z4pCIoP2SSfnh8KE4=%yjFq)I&jcIdT7K;m(06tm_!=0hGv=d}k+r(?A`0;<`M
z3yz{aO*T{GfHs;D&q*teXrx0Rw{f3FR_bVGRa{Yq$FwDs1`B(mk%8Q(6-f0F5nymL
z!sOQzqdo>Q1Pd3+V5mBJ%k6JRJK)R_ScTJY%ygN&vr!o;Hb_I8$P=5R;shDYloPx-
zkv`*Gp)`-FYNaPi|_EfXZq@9F!T3Z1{sUW*GLhhntR5ArHpGL>uR6Y;86;
zoraeWvvD5jBpxH-g0(&1B9?Kb$Oema6h9pVnh6^o4`X6!r0)<;lEGCs;88kk5Y!LT`)*wqH+
z(>d@cq@F7zm4Qqr3r%Q_&T1-7`@W{$w6iRf#fE>u_k4)>x9-I_2U%Q8M0MDbvp(Dy
ztgjC+@Y3`n7`19vNbVDNHj}}Xc~qTHAF+t1C!fqi6_e3w>5XmLQoW>=c=lw2b1-CQ
z2IZPP@QA0QR>5jaKHE!AMqgXq
z61&rGQ(?5J1(n33h(;o2G$s1)pF8llX8hv%5lXcks70gZAVJ7jc
zzd84PuX3xoIZ^J9O?SN)Q7ewW8
zmFba+)9g1#dFV$#)+#@-^hX&)5A^Lm&c7J7DoZ7hwtu>h^Pi>D$*oc{+mrPQ!6TLN
zOG}bUG|1!h!sq`s)+#>&Qf43L5e-`9Rx_Qy0Gp}?6PZwM;s5{u07*qoM6N<$f)NzE
AHvj+t
literal 0
HcmV?d00001
diff --git a/apps/web/src/components/CurrencyInput/index.tsx b/apps/web/src/components/CurrencyInput/index.tsx
new file mode 100644
index 0000000000000..631dfc44eccc5
--- /dev/null
+++ b/apps/web/src/components/CurrencyInput/index.tsx
@@ -0,0 +1,62 @@
+import { useMemo, useCallback, ReactNode, MouseEvent } from 'react'
+import { Currency, CurrencyAmount } from '@pancakeswap/sdk'
+import { CurrencyLogo } from '@pancakeswap/widgets-internal'
+import { BalanceInput, Text, Flex, Button } from '@pancakeswap/uikit'
+
+interface Props {
+ value: string | number
+ onChange: (val: string) => void
+ currency?: Currency
+ balance?: CurrencyAmount
+ balanceText?: ReactNode
+ maxText?: ReactNode
+}
+
+export function CurrencyInput({ currency, balance, value, onChange, balanceText, maxText = 'Max', ...rest }: Props) {
+ const isMax = useMemo(() => balance && value && balance.toExact() === value, [balance, value])
+ const onMaxClick = useCallback(
+ (e: MouseEvent) => {
+ e.stopPropagation()
+ e.preventDefault()
+ onChange?.(balance?.toExact() || '')
+ },
+ [onChange, balance],
+ )
+
+ const currencyDisplay = currency ? (
+
+
+
+ {currency.symbol}
+
+
+ ) : null
+
+ const balanceDisplay = balance ? (
+
+
+ {balanceText}
+
+
+
+ ) : null
+
+ return (
+
+ )
+}
diff --git a/apps/web/src/components/Menu/config/config.ts b/apps/web/src/components/Menu/config/config.ts
index 56301655d15be..675d30d467b06 100644
--- a/apps/web/src/components/Menu/config/config.ts
+++ b/apps/web/src/components/Menu/config/config.ts
@@ -1,5 +1,6 @@
import { ContextApi } from '@pancakeswap/localization'
import { SUPPORTED_CHAIN_IDS as POOL_SUPPORTED_CHAINS } from '@pancakeswap/pools'
+import { SUPPORTED_CHAIN_IDS as POSITION_MANAGERS_SUPPORTED_CHAINS } from '@pancakeswap/position-managers'
import {
NftIcon,
NftFillIcon,
@@ -114,6 +115,12 @@ const config: (
href: '/pools',
supportChainIds: POOL_SUPPORTED_CHAINS,
},
+ {
+ label: t('Position Manager'),
+ href: '/position-managers',
+ supportChainIds: POSITION_MANAGERS_SUPPORTED_CHAINS,
+ status: { text: t('New'), color: 'success' },
+ },
{
label: t('Liquid Staking'),
href: '/liquid-staking',
diff --git a/apps/web/src/hooks/useContract.ts b/apps/web/src/hooks/useContract.ts
index e4936149740d5..057a8beca4184 100644
--- a/apps/web/src/hooks/useContract.ts
+++ b/apps/web/src/hooks/useContract.ts
@@ -12,6 +12,8 @@ import {
getBCakeFarmBoosterContract,
getBCakeFarmBoosterProxyFactoryContract,
getBCakeFarmBoosterV3Contract,
+ getPositionManagerWrapperContract,
+ getPositionManagerAdapterContract,
getBCakeProxyContract,
getBunnyFactoryContract,
getCakeFlexibleSideVaultV2Contract,
@@ -313,6 +315,24 @@ export function useBCakeFarmBoosterV3Contract() {
return useMemo(() => getBCakeFarmBoosterV3Contract(signer ?? undefined, chainId), [signer, chainId])
}
+export function usePositionManagerWrapperContract(address: Address) {
+ const { chainId } = useActiveChainId()
+ const { data: signer } = useWalletClient()
+ return useMemo(
+ () => getPositionManagerWrapperContract(address, signer ?? undefined, chainId),
+ [signer, chainId, address],
+ )
+}
+
+export function usePositionManagerAdepterContract(address: Address) {
+ const { chainId } = useActiveChainId()
+ const { data: signer } = useWalletClient()
+ return useMemo(
+ () => getPositionManagerAdapterContract(address, signer ?? undefined, chainId),
+ [signer, chainId, address],
+ )
+}
+
export function useBCakeFarmBoosterProxyFactoryContract() {
const { data: signer } = useWalletClient()
return useMemo(() => getBCakeFarmBoosterProxyFactoryContract(signer ?? undefined), [signer])
@@ -433,5 +453,5 @@ export const useFixedStakingContract = () => {
const { data: signer } = useWalletClient()
- return useMemo(() => getFixedStakingContract(signer, chainId), [chainId, signer])
+ return useMemo(() => getFixedStakingContract(signer ?? undefined, chainId), [chainId, signer])
}
diff --git a/apps/web/src/hooks/usePositionPrices.ts b/apps/web/src/hooks/usePositionPrices.ts
new file mode 100644
index 0000000000000..29d6aeaa3a2d6
--- /dev/null
+++ b/apps/web/src/hooks/usePositionPrices.ts
@@ -0,0 +1,80 @@
+import { Currency } from '@pancakeswap/sdk'
+import { tickToPrice } from '@pancakeswap/v3-sdk'
+import { useCallback, useMemo, useState } from 'react'
+
+interface PositionInfo {
+ currencyA?: Currency
+ currencyB?: Currency
+ tickLower?: number
+ tickUpper?: number
+ tickCurrent?: number
+}
+
+export function usePositionPrices({
+ currencyA: initialBaseCurrency,
+ currencyB: initialQuoteCurrency,
+ tickLower,
+ tickUpper,
+ tickCurrent,
+}: PositionInfo) {
+ const [invert, setInvert] = useState(false)
+ const toggleInvert = useCallback(() => setInvert(!invert), [invert])
+ const currencyA = useMemo(
+ () => (invert ? initialQuoteCurrency : initialBaseCurrency),
+ [invert, initialBaseCurrency, initialQuoteCurrency],
+ )
+ const currencyB = useMemo(
+ () => (invert ? initialBaseCurrency : initialQuoteCurrency),
+ [invert, initialBaseCurrency, initialQuoteCurrency],
+ )
+
+ const sorted = useMemo(
+ () =>
+ Boolean(
+ currencyA?.wrapped &&
+ currencyB?.wrapped &&
+ !currencyA.wrapped.equals(currencyB.wrapped) &&
+ currencyA.wrapped.sortsBefore(currencyB.wrapped),
+ ),
+ [currencyA, currencyB],
+ )
+
+ const tickLowerPrice = useMemo(
+ () =>
+ currencyA?.wrapped &&
+ currencyB?.wrapped &&
+ typeof tickLower === 'number' &&
+ tickToPrice(currencyA.wrapped, currencyB.wrapped, tickLower),
+ [tickLower, currencyA, currencyB],
+ )
+ const tickUpperPrice = useMemo(
+ () =>
+ currencyA?.wrapped &&
+ currencyB?.wrapped &&
+ typeof tickUpper === 'number' &&
+ tickToPrice(currencyA.wrapped, currencyB.wrapped, tickUpper),
+ [tickUpper, currencyA, currencyB],
+ )
+ const [priceLower, priceUpper] = useMemo(
+ () => (sorted ? [tickLowerPrice, tickUpperPrice] : [tickUpperPrice, tickLowerPrice]),
+ [sorted, tickLowerPrice, tickUpperPrice],
+ )
+ const priceCurrent = useMemo(
+ () =>
+ currencyA?.wrapped &&
+ currencyB?.wrapped &&
+ typeof tickCurrent === 'number' &&
+ tickToPrice(currencyA.wrapped, currencyB.wrapped, tickCurrent),
+ [tickCurrent, currencyA, currencyB],
+ )
+
+ return {
+ currencyA,
+ currencyB,
+ priceLower,
+ priceUpper,
+ priceCurrent,
+ invert: toggleInvert,
+ inverted: invert,
+ }
+}
diff --git a/apps/web/src/pages/position-managers/[[...slug]].tsx b/apps/web/src/pages/position-managers/[[...slug]].tsx
new file mode 100644
index 0000000000000..13bcedb56d1ae
--- /dev/null
+++ b/apps/web/src/pages/position-managers/[[...slug]].tsx
@@ -0,0 +1,32 @@
+import { SUPPORTED_CHAIN_IDS } from '@pancakeswap/position-managers'
+import type { GetStaticPaths, GetStaticProps } from 'next'
+
+import { PositionManagers } from 'views/PositionManagers'
+
+const Page = () =>
+
+Page.chains = SUPPORTED_CHAIN_IDS
+
+export const getStaticProps: GetStaticProps = async () => {
+ return { props: {} }
+}
+
+export const getStaticPaths: GetStaticPaths = async () => {
+ return {
+ paths: [
+ {
+ params: {
+ slug: [],
+ },
+ },
+ {
+ params: {
+ slug: ['history'],
+ },
+ },
+ ],
+ fallback: false,
+ }
+}
+
+export default Page
diff --git a/apps/web/src/utils/contractHelpers.ts b/apps/web/src/utils/contractHelpers.ts
index c43e260cded90..6034a73648dde 100644
--- a/apps/web/src/utils/contractHelpers.ts
+++ b/apps/web/src/utils/contractHelpers.ts
@@ -61,6 +61,7 @@ import { affiliateProgramABI } from 'config/abi/affiliateProgram'
import { bCakeFarmBoosterABI } from 'config/abi/bCakeFarmBooster'
import { bCakeFarmBoosterProxyFactoryABI } from 'config/abi/bCakeFarmBoosterProxyFactory'
import { bCakeFarmBoosterV3ABI } from 'config/abi/bCakeFarmBoosterV3'
+import { positionManagerAdapterABI, positionManagerWrapperABI } from '@pancakeswap/position-managers'
import { bCakeProxyABI } from 'config/abi/bCakeProxy'
import { bunnyFactoryABI } from 'config/abi/bunnyFactory'
import { chainlinkOracleABI } from 'config/abi/chainlinkOracle'
@@ -247,6 +248,24 @@ export const getBCakeFarmBoosterV3Contract = (signer?: WalletClient, chainId?: n
return getContract({ abi: bCakeFarmBoosterV3ABI, address: getBCakeFarmBoosterV3Address(chainId), signer, chainId })
}
+export const getPositionManagerWrapperContract = (address: `0x${string}`, signer?: WalletClient, chainId?: number) => {
+ return getContract({
+ abi: positionManagerWrapperABI,
+ address,
+ signer,
+ chainId,
+ })
+}
+
+export const getPositionManagerAdapterContract = (address: `0x${string}`, signer?: WalletClient, chainId?: number) => {
+ return getContract({
+ abi: positionManagerAdapterABI,
+ address,
+ signer,
+ chainId,
+ })
+}
+
export const getBCakeFarmBoosterProxyFactoryContract = (signer?: WalletClient) => {
return getContract({
abi: bCakeFarmBoosterProxyFactoryABI,
@@ -348,7 +367,7 @@ export const getMasterChefV3Contract = (signer?: WalletClient, chainId?: number)
return mcv3Address
? getContract({
abi: masterChefV3ABI,
- address: getMasterChefV3Address(chainId),
+ address: mcv3Address,
chainId,
signer,
})
diff --git a/apps/web/src/views/PositionManagers/components/AddLiquidity.tsx b/apps/web/src/views/PositionManagers/components/AddLiquidity.tsx
new file mode 100644
index 0000000000000..4192c7bda31e7
--- /dev/null
+++ b/apps/web/src/views/PositionManagers/components/AddLiquidity.tsx
@@ -0,0 +1,459 @@
+import { useTranslation } from '@pancakeswap/localization'
+import { MANAGER } from '@pancakeswap/position-managers'
+import { Currency, CurrencyAmount, Percent } from '@pancakeswap/sdk'
+import { Button, Flex, LinkExternal, ModalV2, RowBetween, Text, useToast } from '@pancakeswap/uikit'
+import tryParseAmount from '@pancakeswap/utils/tryParseAmount'
+import { FeeAmount } from '@pancakeswap/v3-sdk'
+import { useWeb3React } from '@pancakeswap/wagmi'
+import { ConfirmationPendingContent } from '@pancakeswap/widgets-internal'
+import BigNumber from 'bignumber.js'
+import { CurrencyInput } from 'components/CurrencyInput'
+import { ToastDescriptionWithTx } from 'components/Toast'
+import { ApprovalState, useApproveCallback } from 'hooks/useApproveCallback'
+import useCatchTxError from 'hooks/useCatchTxError'
+import { usePositionManagerWrapperContract } from 'hooks/useContract'
+import { memo, useCallback, useMemo, useState } from 'react'
+import { styled } from 'styled-components'
+import { formatCurrencyAmount } from 'utils/formatCurrencyAmount'
+import { Address } from 'viem'
+import { DYORWarning } from 'views/PositionManagers/components/DYORWarning'
+import { SingleTokenWarning } from 'views/PositionManagers/components/SingleTokenWarning'
+import { StyledModal } from 'views/PositionManagers/components/StyledModal'
+import { FeeTag } from 'views/PositionManagers/components/Tags'
+import { useApr } from 'views/PositionManagers/hooks/useApr'
+import { AprDataInfo } from '../hooks'
+import { AprButton } from './AprButton'
+
+interface Props {
+ id: string | number
+ manager: {
+ id: MANAGER
+ name: string
+ }
+ isOpen?: boolean
+ onDismiss?: () => void
+ vaultName: string
+ feeTier: FeeAmount
+ currencyA: Currency
+ currencyB: Currency
+ ratio: number
+ isSingleDepositToken: boolean
+ allowDepositToken0: boolean
+ allowDepositToken1: boolean
+ onAmountChange?: (info: { value: string; currency: Currency; otherAmount: CurrencyAmount }) => {
+ otherAmount: CurrencyAmount
+ }
+ refetch?: () => void
+ contractAddress: Address
+ userCurrencyBalances: {
+ token0Balance: CurrencyAmount | undefined
+ token1Balance: CurrencyAmount | undefined
+ }
+ userVaultPercentage?: Percent
+ poolToken0Amount?: bigint
+ poolToken1Amount?: bigint
+ token0PriceUSD?: number
+ token1PriceUSD?: number
+ rewardPerSecond: string
+ earningToken: Currency
+ aprDataInfo: {
+ info: AprDataInfo | undefined
+ isLoading: boolean
+ }
+ rewardEndTime: number
+ rewardStartTime: number
+ onAdd?: (params: { amountA: CurrencyAmount; amountB: CurrencyAmount }) => Promise
+ totalAssetsInUsd: number
+ totalStakedInUsd: number
+ userLpAmounts?: bigint
+ totalSupplyAmounts?: bigint
+ precision?: bigint
+ strategyInfoUrl?: string
+ learnMoreAboutUrl?: string
+}
+
+const StyledCurrencyInput = styled(CurrencyInput)`
+ flex: 1;
+`
+
+export const AddLiquidity = memo(function AddLiquidity({
+ id,
+ manager,
+ ratio,
+ isOpen,
+ vaultName,
+ currencyA,
+ currencyB,
+ feeTier,
+ isSingleDepositToken,
+ allowDepositToken1,
+ allowDepositToken0,
+ contractAddress,
+ userCurrencyBalances,
+ poolToken0Amount,
+ poolToken1Amount,
+ token0PriceUSD,
+ token1PriceUSD,
+ rewardPerSecond,
+ earningToken,
+ aprDataInfo,
+ rewardEndTime,
+ rewardStartTime,
+ refetch,
+ onDismiss,
+ totalAssetsInUsd,
+ userLpAmounts,
+ totalSupplyAmounts,
+ precision,
+ totalStakedInUsd,
+ strategyInfoUrl,
+ learnMoreAboutUrl,
+}: Props) {
+ const [valueA, setValueA] = useState('')
+ const [valueB, setValueB] = useState('')
+ const {
+ t,
+ currentLanguage: { locale },
+ } = useTranslation()
+ const { account, chain } = useWeb3React()
+ const tokenPairName = useMemo(() => `${currencyA.symbol}-${currencyB.symbol}`, [currencyA, currencyB])
+
+ const onInputChange = useCallback(
+ ({
+ value,
+ setValue,
+ setOtherValue,
+ isToken0,
+ }: {
+ value: string
+ currency: Currency
+ otherValue: string
+ otherCurrency: Currency
+ setValue: (value: string) => void
+ setOtherValue: (value: string) => void
+ isToken0: boolean
+ }) => {
+ setValue(value)
+ setOtherValue((Number(value) * (isToken0 ? 1 / ratio : ratio)).toString())
+ },
+ [ratio],
+ )
+
+ const onCurrencyAChange = useCallback(
+ (value: string) =>
+ onInputChange({
+ value,
+ currency: currencyA,
+ otherValue: valueB,
+ otherCurrency: currencyB,
+ setValue: setValueA,
+ setOtherValue: setValueB,
+ isToken0: true,
+ }),
+ [currencyA, currencyB, valueB, onInputChange],
+ )
+
+ const onCurrencyBChange = useCallback(
+ (value: string) =>
+ onInputChange({
+ value,
+ currency: currencyB,
+ otherValue: valueA,
+ otherCurrency: currencyA,
+ setValue: setValueB,
+ setOtherValue: setValueA,
+ isToken0: false,
+ }),
+ [currencyA, currencyB, valueA, onInputChange],
+ )
+
+ const amountA = useMemo(
+ () => tryParseAmount(valueA, currencyA) || CurrencyAmount.fromRawAmount(currencyA, '0'),
+ [valueA, currencyA],
+ )
+ const amountB = useMemo(
+ () => tryParseAmount(valueB, currencyB) || CurrencyAmount.fromRawAmount(currencyB, '0'),
+ [valueB, currencyB],
+ )
+
+ const userVaultPercentage = useMemo(() => {
+ const totalPoolToken0Usd = new BigNumber(amountA?.toSignificant() ?? 0).times(token0PriceUSD ?? 0)?.toNumber()
+ const totalPoolToken1Usd = new BigNumber(amountB?.toSignificant() ?? 0).times(token1PriceUSD ?? 0)?.toNumber()
+ const userTotalDepositUSD =
+ (allowDepositToken0 ? totalPoolToken0Usd : 0) + (allowDepositToken1 ? totalPoolToken1Usd : 0)
+
+ return ((userTotalDepositUSD + totalAssetsInUsd) / (totalStakedInUsd + userTotalDepositUSD)) * 100
+ }, [
+ allowDepositToken0,
+ allowDepositToken1,
+ amountA,
+ amountB,
+ token0PriceUSD,
+ token1PriceUSD,
+ totalStakedInUsd,
+ totalAssetsInUsd,
+ ])
+
+ const apr = useApr({
+ currencyA,
+ currencyB,
+ poolToken0Amount,
+ poolToken1Amount,
+ token0PriceUSD,
+ token1PriceUSD,
+ rewardPerSecond,
+ earningToken,
+ avgToken0Amount: aprDataInfo?.info?.token0 ?? 0,
+ avgToken1Amount: aprDataInfo?.info?.token1 ?? 0,
+ rewardEndTime,
+ rewardStartTime,
+ })
+
+ const displayBalanceText = useCallback(
+ (balanceAmount: CurrencyAmount | undefined) =>
+ balanceAmount ? `Balances: ${balanceAmount?.toSignificant(6)}` : '',
+ [],
+ )
+
+ const onDone = useCallback(() => {
+ onDismiss?.()
+ refetch?.()
+ }, [onDismiss, refetch])
+
+ const disabled = useMemo(() => {
+ const balanceAmountMoreThenValueA =
+ allowDepositToken0 &&
+ amountA.greaterThan('0') &&
+ Number(userCurrencyBalances?.token0Balance?.toSignificant()) < Number(amountA?.toSignificant())
+
+ const balanceAmountMoreThenValueB =
+ allowDepositToken1 &&
+ amountB.greaterThan('0') &&
+ Number(userCurrencyBalances?.token1Balance?.toSignificant()) < Number(amountB?.toSignificant())
+ return (
+ (allowDepositToken0 && (amountA.equalTo('0') || balanceAmountMoreThenValueA)) ||
+ (allowDepositToken1 && (amountB.equalTo('0') || balanceAmountMoreThenValueB))
+ )
+ }, [allowDepositToken0, allowDepositToken1, amountA, amountB, userCurrencyBalances])
+
+ const positionManagerWrapperContract = usePositionManagerWrapperContract(contractAddress)
+ const { fetchWithCatchTxError, loading: pendingTx } = useCatchTxError()
+ const { toastSuccess } = useToast()
+
+ const mintThenDeposit = useCallback(async () => {
+ const receipt = await fetchWithCatchTxError(() =>
+ positionManagerWrapperContract.write.mintThenDeposit(
+ [allowDepositToken0 ? amountA?.numerator ?? 0n : 0n, allowDepositToken1 ? amountB?.numerator ?? 0n : 0n, '0x'],
+ {
+ account: account ?? '0x',
+ chain,
+ },
+ ),
+ )
+
+ if (receipt?.status) {
+ toastSuccess(
+ `${t('Staked')}!`,
+
+ {t('Your funds have been staked in position manager.')}
+ ,
+ )
+ onDone()
+ }
+ }, [
+ amountA,
+ amountB,
+ positionManagerWrapperContract,
+ account,
+ chain,
+ toastSuccess,
+ t,
+ fetchWithCatchTxError,
+ onDone,
+ allowDepositToken0,
+ allowDepositToken1,
+ ])
+
+ const translationData = useMemo(
+ () => ({
+ amountA: allowDepositToken0 ? formatCurrencyAmount(amountA, 4, locale) : '',
+ symbolA: allowDepositToken0 ? currencyA.symbol : '',
+ amountB: allowDepositToken1 ? formatCurrencyAmount(amountB, 4, locale) : '',
+ symbolB: allowDepositToken1 ? currencyB.symbol : '',
+ }),
+ [allowDepositToken0, allowDepositToken1, amountA, amountB, currencyA.symbol, currencyB.symbol, locale],
+ )
+
+ const pendingText = useMemo(
+ () =>
+ !isSingleDepositToken
+ ? t('Supplying %amountA% %symbolA% and %amountB% %symbolB%', translationData)
+ : t('Supplying %amountA% %symbolA% %amountB% %symbolB%', translationData),
+ [t, isSingleDepositToken, translationData],
+ )
+
+ return (
+
+
+ {pendingTx ? (
+
+ ) : (
+ <>
+
+ {t('Adding')}:
+
+
+ {tokenPairName}
+
+
+ {vaultName}
+
+
+
+
+ {allowDepositToken0 && (
+
+
+
+ )}
+ {allowDepositToken1 && (
+
+
+
+ )}
+
+
+ {t('Your share in the vault')}:
+ {`${userVaultPercentage?.toFixed(2)}%`}
+
+
+ {t('APR')}:
+
+
+
+ {isSingleDepositToken && }
+
+
+
+
+ >
+ )}
+
+
+ )
+})
+
+interface AddLiquidityButtonProps {
+ amountA: CurrencyAmount | undefined
+ amountB: CurrencyAmount | undefined
+ contractAddress: `0x${string}`
+ disabled?: boolean
+ onAddLiquidity?: () => void
+ isLoading?: boolean
+ learnMoreAboutUrl?: string
+}
+
+export const AddLiquidityButton = memo(function AddLiquidityButton({
+ amountA,
+ amountB,
+ contractAddress,
+ disabled,
+ onAddLiquidity,
+ isLoading,
+ learnMoreAboutUrl,
+}: AddLiquidityButtonProps) {
+ const { t } = useTranslation()
+
+ const { approvalState: approvalStateToken0, approveCallback: approveCallbackToken0 } = useApproveCallback(
+ amountA,
+ contractAddress,
+ )
+ const { approvalState: approvalStateToken1, approveCallback: approveCallbackToken1 } = useApproveCallback(
+ amountB,
+ contractAddress,
+ )
+
+ const showAmountButtonA = useMemo(
+ () => amountA && approvalStateToken0 === ApprovalState.NOT_APPROVED,
+ [amountA, approvalStateToken0],
+ )
+ const showAmountButtonB = useMemo(
+ () => amountB && approvalStateToken1 === ApprovalState.NOT_APPROVED,
+ [amountB, approvalStateToken1],
+ )
+ const isConfirmButtonDisabled = useMemo(
+ () =>
+ disabled ||
+ (amountA && approvalStateToken0 !== ApprovalState.APPROVED) ||
+ (amountB && approvalStateToken1 !== ApprovalState.APPROVED),
+ [amountA, amountB, disabled, approvalStateToken0, approvalStateToken1],
+ )
+
+ return (
+ <>
+ {showAmountButtonA && (
+
+ )}
+ {showAmountButtonB && (
+
+ )}
+
+
+ {t('Learn more about the strategy')}
+
+ >
+ )
+})
diff --git a/apps/web/src/views/PositionManagers/components/AprButton.tsx b/apps/web/src/views/PositionManagers/components/AprButton.tsx
new file mode 100644
index 0000000000000..a1c110b09cf0f
--- /dev/null
+++ b/apps/web/src/views/PositionManagers/components/AprButton.tsx
@@ -0,0 +1,113 @@
+import { useTranslation } from '@pancakeswap/localization'
+import { Flex, RoiCalculatorModal, Skeleton, Text, useModal, useTooltip } from '@pancakeswap/uikit'
+import BigNumber from 'bignumber.js'
+import { useCakePrice } from 'hooks/useCakePrice'
+import { memo, useMemo } from 'react'
+import { styled } from 'styled-components'
+import { useAccount } from 'wagmi'
+import { AprResult } from '../hooks'
+
+interface Props {
+ id: number | string
+ apr: AprResult
+ isAprLoading: boolean
+ lpSymbol: string
+ totalAssetsInUsd: number
+ userLpAmounts?: bigint
+ totalSupplyAmounts?: bigint
+ precision?: bigint
+}
+
+const AprText = styled(Text)`
+ text-underline-offset: 0.125em;
+ text-decoration: dotted underline;
+ cursor: pointer;
+`
+
+export const AprButton = memo(function YieldInfo({
+ id,
+ apr,
+ isAprLoading,
+ totalAssetsInUsd,
+ lpSymbol,
+ userLpAmounts,
+ precision,
+}: Props) {
+ const { t } = useTranslation()
+
+ const { address: account } = useAccount()
+ const cakePriceBusd = useCakePrice()
+ const tokenBalance = useMemo(
+ () => new BigNumber(Number(((userLpAmounts ?? 0n) * 10000n) / (precision ?? 1n)) / 10000 ?? 0),
+ [userLpAmounts, precision],
+ )
+
+ const tokenPrice = useMemo(
+ () => totalAssetsInUsd / (Number(((userLpAmounts ?? 0n) * 10000n) / (precision ?? 1n)) / 10000 ?? 0),
+ [userLpAmounts, precision, totalAssetsInUsd],
+ )
+ const { targetRef, tooltip, tooltipVisible } = useTooltip(
+ <>
+
+ {t('Combined APR')}:{' '}
+
+ {`${apr.combinedApr}%`}
+
+
+
+ {apr.isInCakeRewardDateRange && (
+ -
+ {t('CAKE APR')}:{' '}
+
+ {`${apr.cakeYieldApr}%`}
+
+
+ )}
+ -
+ {t('LP APR')}:{' '}
+
+ {apr.lpApr}%
+
+
+
+
+ {t('Calculated based on previous 7 days average data.')}
+
+ >,
+ {
+ placement: 'top',
+ },
+ )
+
+ const [onPresentApyModal] = useModal(
+ ,
+ false,
+ true,
+ `PositionManagerModal${id}`,
+ )
+
+ return (
+
+ {apr && !isAprLoading ? (
+
+ {`${apr.combinedApr}%`}
+ {tooltipVisible && tooltip}
+
+ ) : (
+
+ )}
+
+ )
+})
diff --git a/apps/web/src/views/PositionManagers/components/CardLayout.tsx b/apps/web/src/views/PositionManagers/components/CardLayout.tsx
new file mode 100644
index 0000000000000..42b2506e2cbed
--- /dev/null
+++ b/apps/web/src/views/PositionManagers/components/CardLayout.tsx
@@ -0,0 +1,15 @@
+import { styled } from 'styled-components'
+import { FlexLayout, CardHeader as CardHeaderComp } from '@pancakeswap/uikit'
+
+export const CardLayout = styled(FlexLayout)`
+ justify-content: flex-start;
+`
+
+export const CardHeader = styled(CardHeaderComp)`
+ background: none;
+ display: flex;
+ justify-content: space-between;
+ flex-direction: row;
+ align-items: flex-start;
+ padding: 1.5em 1.5em 0 1.5em;
+`
diff --git a/apps/web/src/views/PositionManagers/components/CardSection.tsx b/apps/web/src/views/PositionManagers/components/CardSection.tsx
new file mode 100644
index 0000000000000..bf49ca29b8a05
--- /dev/null
+++ b/apps/web/src/views/PositionManagers/components/CardSection.tsx
@@ -0,0 +1,30 @@
+import { styled } from 'styled-components'
+import { SpaceProps } from 'styled-system'
+import { PropsWithChildren, ReactNode, memo } from 'react'
+import { Box, Text } from '@pancakeswap/uikit'
+
+interface Props extends SpaceProps {
+ title: ReactNode
+}
+
+const Section = styled(Box)`
+ & + & {
+ margin-top: 1em;
+ }
+`
+
+const Title = styled(Text).attrs({
+ color: 'textSubtle',
+ textTransform: 'uppercase',
+ fontSize: '12px',
+ bold: true,
+})``
+
+export const CardSection = memo(function CardSection({ title, children, ...props }: PropsWithChildren) {
+ return (
+
+ )
+})
diff --git a/apps/web/src/views/PositionManagers/components/CardTitle.tsx b/apps/web/src/views/PositionManagers/components/CardTitle.tsx
new file mode 100644
index 0000000000000..fea242ea049fd
--- /dev/null
+++ b/apps/web/src/views/PositionManagers/components/CardTitle.tsx
@@ -0,0 +1,68 @@
+import { memo, useMemo } from 'react'
+import { Currency } from '@pancakeswap/sdk'
+import { FeeAmount } from '@pancakeswap/v3-sdk'
+import { Flex, Text } from '@pancakeswap/uikit'
+
+import { CardHeader } from './CardLayout'
+import { TokenPairLogos } from './TokenPairLogos'
+import { FeeTag, FarmTag, SingleTokenTag } from './Tags'
+
+interface Props {
+ currencyA: Currency
+ currencyB: Currency
+ vaultName: string
+ feeTier: FeeAmount
+ isSingleDepositToken: boolean
+ allowDepositToken1: boolean
+ autoFarm?: boolean
+ autoCompound?: boolean
+}
+
+export const CardTitle = memo(function CardTitle({
+ currencyA,
+ currencyB,
+ vaultName,
+ feeTier,
+ isSingleDepositToken,
+ autoFarm,
+ autoCompound,
+ allowDepositToken1,
+}: Props) {
+ const isTokenDisplayReverse = useMemo(
+ () => isSingleDepositToken && allowDepositToken1,
+ [isSingleDepositToken, allowDepositToken1],
+ )
+ const displayCurrencyA = useMemo(
+ () => (isTokenDisplayReverse ? currencyB : currencyA),
+ [isTokenDisplayReverse, currencyA, currencyB],
+ )
+ const displayCurrencyB = useMemo(
+ () => (isTokenDisplayReverse ? currencyA : currencyB),
+ [isTokenDisplayReverse, currencyA, currencyB],
+ )
+ const tokenPairName = useMemo(
+ () => `${displayCurrencyA.symbol}-${displayCurrencyB.symbol}`,
+ [displayCurrencyA, displayCurrencyB],
+ )
+
+ return (
+
+
+
+
+
+ {tokenPairName}
+
+
+ {vaultName}
+
+
+
+
+ {autoFarm && }
+ {isSingleDepositToken && }
+
+
+
+ )
+})
diff --git a/apps/web/src/views/PositionManagers/components/ControlsContainer.tsx b/apps/web/src/views/PositionManagers/components/ControlsContainer.tsx
new file mode 100644
index 0000000000000..22436bb6ae7b0
--- /dev/null
+++ b/apps/web/src/views/PositionManagers/components/ControlsContainer.tsx
@@ -0,0 +1,37 @@
+import { Flex } from '@pancakeswap/uikit'
+import { styled } from 'styled-components'
+
+export const ControlsContainer = styled(Flex).attrs({
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ flexDirection: 'column',
+})`
+ width: 100%;
+ position: relative;
+
+ margin-bottom: 2em;
+
+ ${({ theme }) => theme.mediaQueries.sm} {
+ flex-direction: row;
+ flex-wrap: wrap;
+ padding: 1em 2em;
+ margin-bottom: 0;
+ }
+`
+
+export const ControlGroup = styled(Flex)`
+ width: 100%;
+ align-items: ${(props) => props.alignItems || 'center'};
+ flex-direction: ${(props) => props.flexDirection || 'row'};
+ justify-content: ${(props) => props.justifyContent || 'space-between'};
+ margin-bottom: 1em;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+
+ ${({ theme }) => theme.mediaQueries.sm} {
+ width: auto;
+ margin-bottom: 0;
+ }
+`
diff --git a/apps/web/src/views/PositionManagers/components/DYORWarning.tsx b/apps/web/src/views/PositionManagers/components/DYORWarning.tsx
new file mode 100644
index 0000000000000..a4578ccfac7bc
--- /dev/null
+++ b/apps/web/src/views/PositionManagers/components/DYORWarning.tsx
@@ -0,0 +1,52 @@
+import { useMemo } from 'react'
+import { useTranslation } from '@pancakeswap/localization'
+import { Message, MessageText, Text, Flex, Box, Link } from '@pancakeswap/uikit'
+import { MANAGER, baseManagers, BaseManager } from '@pancakeswap/position-managers'
+
+interface DYORWarningProps {
+ manager: {
+ id: MANAGER
+ name: string
+ }
+}
+
+export const DYORWarning: React.FC = ({ manager }) => {
+ const { t } = useTranslation()
+ const managerInfo: BaseManager = useMemo(() => baseManagers[manager.id], [manager])
+
+ if (!managerInfo?.name && !managerInfo?.introLink) {
+ return null
+ }
+
+ return (
+
+
+
+
+
+ {t('You are providing liquidity via a 3rd party liquidity manager')}
+
+
+ {managerInfo.name}
+
+
+ {t('which is responsible for managing the underlying assets.')}
+
+
+
+ {t('Please always DYOR before depositing your assets.')}
+
+
+
+
+ )
+}
diff --git a/apps/web/src/views/PositionManagers/components/DuoTokenVaultCard.tsx b/apps/web/src/views/PositionManagers/components/DuoTokenVaultCard.tsx
new file mode 100644
index 0000000000000..b820342e18a48
--- /dev/null
+++ b/apps/web/src/views/PositionManagers/components/DuoTokenVaultCard.tsx
@@ -0,0 +1,230 @@
+import { MANAGER, Strategy } from '@pancakeswap/position-managers'
+import { Card, CardBody } from '@pancakeswap/uikit'
+import { Currency, Percent, Price, CurrencyAmount } from '@pancakeswap/sdk'
+import { FeeAmount } from '@pancakeswap/v3-sdk'
+import { Address } from 'viem'
+import { ReactNode, memo, PropsWithChildren, useMemo } from 'react'
+import { styled } from 'styled-components'
+import { useApr } from 'views/PositionManagers/hooks/useApr'
+import { CardTitle } from './CardTitle'
+import { YieldInfo } from './YieldInfo'
+import { ManagerInfo } from './ManagerInfo'
+import { LiquidityManagement } from './LiquidityManagement'
+import { getVaultName } from '../utils'
+import { ExpandableSection } from './ExpandableSection'
+import { VaultInfo } from './VaultInfo'
+import { VaultLinks } from './VaultLinks'
+import { AprDataInfo } from '../hooks'
+
+const StyledCard = styled(Card)`
+ align-self: baseline;
+ max-width: 100%;
+ margin: 0 0 24px 0;
+ ${({ theme }) => theme.mediaQueries.sm} {
+ max-width: 350px;
+ margin: 0 12px 46px;
+ }
+`
+
+interface Props {
+ currencyA: Currency
+ currencyB: Currency
+ earningToken: Currency
+ name: string
+ id: string | number
+ feeTier: FeeAmount
+ ratio: number
+ strategy: Strategy
+ manager: {
+ id: MANAGER
+ name: string
+ }
+ managerFee?: Percent
+ autoFarm?: boolean
+ autoCompound?: boolean
+ info?: ReactNode
+ isSingleDepositToken: boolean
+ allowDepositToken0?: boolean
+ allowDepositToken1?: boolean
+ contractAddress: Address
+ poolToken0Amount?: bigint
+ poolToken1Amount?: bigint
+ stakedToken0Amount?: bigint
+ stakedToken1Amount?: bigint
+ token0PriceUSD?: number
+ token1PriceUSD?: number
+ pendingReward: bigint | undefined
+ userVaultPercentage?: Percent
+ managerAddress: Address
+ managerInfoUrl: string
+ strategyInfoUrl: string
+ projectVaultUrl?: string
+ learnMoreAboutUrl?: string
+ rewardPerSecond: string
+ aprDataInfo: {
+ info: AprDataInfo | undefined
+ isLoading: boolean
+ }
+ rewardEndTime: number
+ rewardStartTime: number
+ refetch?: () => void
+ totalAssetsInUsd: number
+ totalStakedInUsd: number
+ userLpAmounts?: bigint
+ totalSupplyAmounts?: bigint
+ precision?: bigint
+}
+
+export const DuoTokenVaultCard = memo(function DuoTokenVaultCard({
+ currencyA,
+ currencyB,
+ earningToken,
+ name,
+ id,
+ feeTier,
+ autoFarm,
+ autoCompound,
+ manager,
+ managerFee,
+ strategy,
+ ratio,
+ isSingleDepositToken,
+ allowDepositToken0 = true,
+ allowDepositToken1 = true,
+ contractAddress,
+ stakedToken0Amount,
+ stakedToken1Amount,
+ poolToken0Amount,
+ poolToken1Amount,
+ token0PriceUSD,
+ token1PriceUSD,
+ pendingReward,
+ managerInfoUrl,
+ strategyInfoUrl,
+ projectVaultUrl,
+ rewardPerSecond,
+ aprDataInfo,
+ rewardEndTime,
+ refetch,
+ rewardStartTime,
+ totalAssetsInUsd,
+ userLpAmounts,
+ totalSupplyAmounts,
+ precision,
+ managerAddress,
+ totalStakedInUsd,
+ learnMoreAboutUrl,
+}: PropsWithChildren) {
+ const apr = useApr({
+ currencyA,
+ currencyB,
+ poolToken0Amount,
+ poolToken1Amount,
+ token0PriceUSD,
+ token1PriceUSD,
+ rewardPerSecond,
+ earningToken,
+ avgToken0Amount: aprDataInfo?.info?.token0 ?? 0,
+ avgToken1Amount: aprDataInfo?.info?.token1 ?? 0,
+ rewardEndTime,
+ rewardStartTime,
+ })
+
+ const price = new Price(currencyA, currencyB, 100000n, 100000n)
+ const vaultName = useMemo(() => getVaultName(id, name), [name, id])
+ const staked0Amount = stakedToken0Amount ? CurrencyAmount.fromRawAmount(currencyA, stakedToken0Amount) : undefined
+ const staked1Amount = stakedToken1Amount ? CurrencyAmount.fromRawAmount(currencyB, stakedToken1Amount) : undefined
+
+ const withCakeReward: boolean = useMemo(() => earningToken.symbol === 'CAKE', [earningToken])
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+})
diff --git a/apps/web/src/views/PositionManagers/components/ExpandableSection.tsx b/apps/web/src/views/PositionManagers/components/ExpandableSection.tsx
new file mode 100644
index 0000000000000..a908b56bb570e
--- /dev/null
+++ b/apps/web/src/views/PositionManagers/components/ExpandableSection.tsx
@@ -0,0 +1,24 @@
+import { SpaceProps } from 'styled-system'
+import { PropsWithChildren, memo, useCallback, useState } from 'react'
+import { Flex, ExpandableLabel, Text } from '@pancakeswap/uikit'
+import { useTranslation } from '@pancakeswap/localization'
+
+export const ExpandableSection = memo(function ExpandableSection({
+ children,
+ ...props
+}: PropsWithChildren) {
+ const { t } = useTranslation()
+ const [expanded, setExpanded] = useState(false)
+ const toggle = useCallback(() => setExpanded(!expanded), [expanded])
+
+ return (
+
+
+
+ {expanded ? t('Hide') : t('Info')}
+
+
+ {expanded ? children : null}
+
+ )
+})
diff --git a/apps/web/src/views/PositionManagers/components/Filters.tsx b/apps/web/src/views/PositionManagers/components/Filters.tsx
new file mode 100644
index 0000000000000..ba33c2951ef50
--- /dev/null
+++ b/apps/web/src/views/PositionManagers/components/Filters.tsx
@@ -0,0 +1,85 @@
+import { Flex, Text, Select, OptionProps, SearchInput } from '@pancakeswap/uikit'
+import { styled } from 'styled-components'
+import { useTranslation } from '@pancakeswap/localization'
+import { useMemo, useCallback, ChangeEvent } from 'react'
+
+import { useSearch, useSortBy } from '../hooks'
+
+const LabelWrapper = styled.div`
+ > ${Text} {
+ font-size: 12px;
+ }
+`
+
+const ControlStretch = styled(Flex)`
+ > div {
+ flex: 1;
+ }
+`
+
+export function SortFilter() {
+ const { t } = useTranslation()
+ const [sortBy, setSortBy] = useSortBy()
+ const options = useMemo(
+ () => [
+ {
+ label: t('Hot'),
+ value: 'hot',
+ },
+ {
+ label: t('APR'),
+ value: 'apr',
+ },
+ {
+ label: t('Earned'),
+ value: 'earned',
+ },
+ {
+ label: t('Total staked'),
+ value: 'totalStaked',
+ },
+ {
+ label: t('Latest'),
+ value: 'latest',
+ },
+ ],
+ [t],
+ )
+ const selected = useMemo(() => {
+ const index = options.findIndex((option) => option.value === sortBy)
+ // FIXME weird design of Select component. Need further refactor
+ return index >= 0 ? index + 1 : 0
+ }, [options, sortBy])
+
+ const handleSortOptionChange = useCallback((option: OptionProps) => setSortBy(option.value), [setSortBy])
+
+ return (
+
+
+ {t('Sort by')}
+
+
+
+
+
+ )
+}
+
+export function SearchFilter() {
+ const { t } = useTranslation()
+ const [search, setSearch] = useSearch()
+
+ const onChange = useCallback(
+ (e: ChangeEvent) => setSearch(e.target.value || undefined),
+ [setSearch],
+ )
+
+ return (
+
+
+ {t('Search')}
+
+
+
+ )
+}
diff --git a/apps/web/src/views/PositionManagers/components/Header.tsx b/apps/web/src/views/PositionManagers/components/Header.tsx
new file mode 100644
index 0000000000000..0a30bb79eea98
--- /dev/null
+++ b/apps/web/src/views/PositionManagers/components/Header.tsx
@@ -0,0 +1,32 @@
+import { memo } from 'react'
+import { PageHeader, Flex, Heading, useMatchBreakpoints } from '@pancakeswap/uikit'
+import { useTranslation } from '@pancakeswap/localization'
+import Image from 'next/image'
+
+export const Header = memo(function Header() {
+ const { t } = useTranslation()
+ const { isDesktop } = useMatchBreakpoints()
+
+ return (
+
+
+
+
+ {t('Position Manager')}
+
+
+ {t('Automated your PancakeSwap V3 liquidity')}
+
+
+ {isDesktop && (
+
+ )}
+
+
+ )
+})
diff --git a/apps/web/src/views/PositionManagers/components/InnerCard.tsx b/apps/web/src/views/PositionManagers/components/InnerCard.tsx
new file mode 100644
index 0000000000000..734dc462c5d0f
--- /dev/null
+++ b/apps/web/src/views/PositionManagers/components/InnerCard.tsx
@@ -0,0 +1,22 @@
+import { memo, PropsWithChildren } from 'react'
+import { Card, CardBody } from '@pancakeswap/uikit'
+import { styled } from 'styled-components'
+
+const LightGreyCard = styled(Card)`
+ border: 1px solid ${({ theme }) => theme.colors.cardBorder};
+ background-color: ${({ theme }) => theme.colors.background};
+`
+
+const InnerCardBody = styled(CardBody)`
+ padding: 1em;
+ background-color: ${({ theme }) => theme.colors.background};
+`
+
+// Card within a card container
+export const InnerCard = memo(function InnerCard({ children }: PropsWithChildren) {
+ return (
+
+ {children}
+
+ )
+})
diff --git a/apps/web/src/views/PositionManagers/components/LiquidityManagement.tsx b/apps/web/src/views/PositionManagers/components/LiquidityManagement.tsx
new file mode 100644
index 0000000000000..105af1358fb73
--- /dev/null
+++ b/apps/web/src/views/PositionManagers/components/LiquidityManagement.tsx
@@ -0,0 +1,208 @@
+import { memo, useCallback, useMemo, useState } from 'react'
+import { Button } from '@pancakeswap/uikit'
+import { Address } from 'viem'
+import { MANAGER, BaseAssets } from '@pancakeswap/position-managers'
+import { useTranslation } from '@pancakeswap/localization'
+import { Currency, CurrencyAmount, Percent, Price } from '@pancakeswap/sdk'
+import { FeeAmount } from '@pancakeswap/v3-sdk'
+import { useCurrencyBalances } from 'state/wallet/hooks'
+
+import ConnectWalletButton from 'components/ConnectWalletButton'
+import { useAccount } from 'wagmi'
+import { CardSection } from './CardSection'
+import { AddLiquidity } from './AddLiquidity'
+import { InnerCard } from './InnerCard'
+import { StakedAssets } from './StakedAssets'
+import { RewardAssets } from './RewardAssets'
+import { RemoveLiquidity } from './RemoveLiquidity'
+import { AprDataInfo } from '../hooks'
+
+interface Props {
+ id: string | number
+ manager: {
+ id: MANAGER
+ name: string
+ }
+ currencyA: Currency
+ currencyB: Currency
+ earningToken: Currency
+ staked0Amount?: CurrencyAmount
+ staked1Amount?: CurrencyAmount
+ vaultName: string
+ feeTier: FeeAmount
+ price?: Price
+ ratio: number
+ isSingleDepositToken: boolean
+ allowDepositToken0: boolean
+ allowDepositToken1: boolean
+ contractAddress: Address
+ token0PriceUSD?: number
+ token1PriceUSD?: number
+ pendingReward: bigint | undefined
+ userVaultPercentage?: Percent
+ poolToken0Amount?: bigint
+ poolToken1Amount?: bigint
+ rewardPerSecond: string
+ aprDataInfo: {
+ info: AprDataInfo | undefined
+ isLoading: boolean
+ }
+ rewardEndTime: number
+ rewardStartTime: number
+ totalAssetsInUsd: number
+ totalStakedInUsd: number
+ refetch?: () => void
+ // TODO: replace with needed returned information
+ onAddLiquidity?: (amounts: CurrencyAmount[]) => Promise
+
+ // TODO: replace with needed returned information
+ onRemoveLiquidity?: (params: { assets: BaseAssets; percentage: Percent }) => Promise
+ userLpAmounts?: bigint
+ totalSupplyAmounts?: bigint
+ precision?: bigint
+ isInCakeRewardDateRange: boolean
+ strategyInfoUrl?: string
+ learnMoreAboutUrl?: string
+}
+
+export const LiquidityManagement = memo(function LiquidityManagement({
+ id,
+ manager,
+ currencyA,
+ currencyB,
+ earningToken,
+ vaultName,
+ feeTier,
+ price,
+ ratio,
+ isSingleDepositToken,
+ allowDepositToken0,
+ allowDepositToken1,
+ contractAddress,
+ staked0Amount,
+ staked1Amount,
+ token0PriceUSD,
+ token1PriceUSD,
+ pendingReward,
+ userVaultPercentage,
+ poolToken0Amount,
+ poolToken1Amount,
+ rewardPerSecond,
+ aprDataInfo,
+ rewardEndTime,
+ rewardStartTime,
+ refetch,
+ totalAssetsInUsd,
+ userLpAmounts,
+ totalSupplyAmounts,
+ precision,
+ isInCakeRewardDateRange,
+ totalStakedInUsd,
+ strategyInfoUrl,
+ learnMoreAboutUrl,
+}: Props) {
+ const { t } = useTranslation()
+ const [addLiquidityModalOpen, setAddLiquidityModalOpen] = useState(false)
+ const [removeLiquidityModalOpen, setRemoveLiquidityModalOpen] = useState(false)
+ const hasStaked = useMemo(() => Boolean(staked0Amount) || Boolean(staked1Amount), [staked0Amount, staked1Amount])
+ const { address: account } = useAccount()
+ const showAddLiquidityModal = useCallback(() => setAddLiquidityModalOpen(true), [])
+ const hideAddLiquidityModal = useCallback(() => setAddLiquidityModalOpen(false), [])
+
+ const showRemoveLiquidityModal = useCallback(() => setRemoveLiquidityModalOpen(true), [])
+ const hideRemoveLiquidityModal = useCallback(() => setRemoveLiquidityModalOpen(false), [])
+
+ const relevantTokenBalances = useCurrencyBalances(
+ account ?? undefined,
+ useMemo(() => [currencyA ?? undefined, currencyB ?? undefined], [currencyA, currencyB]),
+ )
+ const userCurrencyBalances = {
+ token0Balance: relevantTokenBalances[0],
+ token1Balance: relevantTokenBalances[1],
+ }
+
+ return (
+ <>
+ {hasStaked ? (
+ <>
+
+
+
+
+ >
+ ) : !account ? (
+
+ ) : (
+
+
+
+ )}
+
+
+ >
+ )
+})
diff --git a/apps/web/src/views/PositionManagers/components/LiveSwitch.tsx b/apps/web/src/views/PositionManagers/components/LiveSwitch.tsx
new file mode 100644
index 0000000000000..18e6e1397ef85
--- /dev/null
+++ b/apps/web/src/views/PositionManagers/components/LiveSwitch.tsx
@@ -0,0 +1,44 @@
+import { memo, useCallback } from 'react'
+import { useTranslation } from '@pancakeswap/localization'
+import { Flex, ButtonMenu, NotificationDot, ButtonMenuItem } from '@pancakeswap/uikit'
+import { styled } from 'styled-components'
+import { usePositionManagerStatus, PositionManagerStatus } from '../hooks'
+
+const Wrapper = styled(Flex).attrs({
+ justifyContent: 'center',
+ alignItems: 'center',
+})`
+ ${({ theme }) => theme.mediaQueries.sm} {
+ margin-left: 1em;
+ }
+`
+
+interface Props {
+ notifyFinished?: boolean
+}
+
+export const LiveSwitch = memo(function LiveSwitch({ notifyFinished }: Props) {
+ const { t } = useTranslation()
+ const { status, setStatus } = usePositionManagerStatus()
+
+ const onItemClick = useCallback(
+ (index: number) => setStatus(index === 0 ? PositionManagerStatus.LIVE : PositionManagerStatus.FINISHED),
+ [setStatus],
+ )
+
+ return (
+
+
+ {t('Live')}
+
+ {t('Finished')}
+
+
+
+ )
+})
diff --git a/apps/web/src/views/PositionManagers/components/ManagerInfo.tsx b/apps/web/src/views/PositionManagers/components/ManagerInfo.tsx
new file mode 100644
index 0000000000000..bd125d110f4e1
--- /dev/null
+++ b/apps/web/src/views/PositionManagers/components/ManagerInfo.tsx
@@ -0,0 +1,47 @@
+import { styled } from 'styled-components'
+import { SpaceProps } from 'styled-system'
+import { memo, useMemo } from 'react'
+import { Text, Row, VerifiedIcon } from '@pancakeswap/uikit'
+import { useTranslation } from '@pancakeswap/localization'
+import { MANAGER, Strategy, isManagerVerified } from '@pancakeswap/position-managers'
+
+import { CardSection } from './CardSection'
+import { getStrategyName } from '../utils'
+
+interface Props extends SpaceProps {
+ id: MANAGER
+ name: string
+ strategy: Strategy
+}
+
+const ManagerName = styled(Text).attrs({
+ color: 'text',
+ bold: true,
+ fontSize: '1em',
+})``
+
+export const ManagerInfo = memo(function ManagerInfo({ id, name, strategy, ...props }: Props) {
+ const { t } = useTranslation()
+ const verified = useMemo(() => isManagerVerified(id), [id])
+ const strategyName = useMemo(() => getStrategyName(t, strategy), [t, strategy])
+
+ return (
+
+
+ {name}
+ {verified && }
+
+
+
+ {t('With')}
+
+
+ {strategyName}
+
+
+ {t('strategy')}
+
+
+
+ )
+})
diff --git a/apps/web/src/views/PositionManagers/components/PercentSlider.tsx b/apps/web/src/views/PositionManagers/components/PercentSlider.tsx
new file mode 100644
index 0000000000000..b2971c01f5178
--- /dev/null
+++ b/apps/web/src/views/PositionManagers/components/PercentSlider.tsx
@@ -0,0 +1,47 @@
+import { styled } from 'styled-components'
+import { SpaceProps } from 'styled-system'
+import { memo, useCallback, useMemo } from 'react'
+import { Slider, Box, Flex, Button } from '@pancakeswap/uikit'
+import { useDebouncedChangeHandler } from '@pancakeswap/hooks'
+import { useTranslation } from '@pancakeswap/localization'
+
+interface Props extends SpaceProps {
+ percent: number
+ onChange?: (percent: number) => void
+}
+
+const QuickInputButton = styled(Button).attrs({
+ variant: 'tertiary',
+ scale: 'sm',
+})``
+
+export const PercentSlider = memo(function PercentSlider({ percent, onChange, ...props }: Props) {
+ const { t } = useTranslation()
+ const [percentForSlider, onPercentSelectForSlider] = useDebouncedChangeHandler(percent, onChange)
+
+ const handleChangePercent = useCallback(
+ (value: number) => onPercentSelectForSlider(Math.ceil(value)),
+ [onPercentSelectForSlider],
+ )
+ const sliderLabel = useMemo(() => `${percentForSlider}%`, [percentForSlider])
+
+ return (
+
+
+
+ onChange?.(25)}>25%
+ onChange?.(50)}>50%
+ onChange?.(75)}>75%
+ onChange?.(100)}>{t('Max')}
+
+
+ )
+})
diff --git a/apps/web/src/views/PositionManagers/components/RemoveLiquidity.tsx b/apps/web/src/views/PositionManagers/components/RemoveLiquidity.tsx
new file mode 100644
index 0000000000000..ac09eb61e0c62
--- /dev/null
+++ b/apps/web/src/views/PositionManagers/components/RemoveLiquidity.tsx
@@ -0,0 +1,193 @@
+import { useTranslation } from '@pancakeswap/localization'
+import { Currency, CurrencyAmount } from '@pancakeswap/sdk'
+import type { AtomBoxProps } from '@pancakeswap/uikit'
+import { Box, Button, Flex, ModalV2, RowBetween, Text, useToast } from '@pancakeswap/uikit'
+import { formatAmount } from '@pancakeswap/utils/formatFractions'
+import { FeeAmount } from '@pancakeswap/v3-sdk'
+import { useWeb3React } from '@pancakeswap/wagmi'
+import { ConfirmationPendingContent, CurrencyLogo } from '@pancakeswap/widgets-internal'
+import BigNumber from 'bignumber.js'
+import { ToastDescriptionWithTx } from 'components/Toast'
+import useCatchTxError from 'hooks/useCatchTxError'
+import { usePositionManagerWrapperContract } from 'hooks/useContract'
+import { memo, useCallback, useMemo, useState } from 'react'
+import { SpaceProps } from 'styled-system'
+import { Address } from 'viem'
+
+import { InnerCard } from './InnerCard'
+import { PercentSlider } from './PercentSlider'
+import { StyledModal } from './StyledModal'
+import { FeeTag } from './Tags'
+
+interface Props {
+ isOpen?: boolean
+ onDismiss?: () => void
+ vaultName: string
+ feeTier: FeeAmount
+ currencyA: Currency
+ currencyB: Currency
+ staked0Amount?: CurrencyAmount
+ staked1Amount?: CurrencyAmount
+ token0PriceUSD?: number
+ token1PriceUSD?: number
+ contractAddress: Address
+ refetch?: () => void
+ onRemove?: (params: {
+ amountA: CurrencyAmount
+ amountB: CurrencyAmount
+ liquidity: bigint
+ }) => Promise
+}
+
+export const RemoveLiquidity = memo(function RemoveLiquidity({
+ isOpen,
+ vaultName,
+ onDismiss,
+ currencyA,
+ currencyB,
+ staked0Amount,
+ staked1Amount,
+ token0PriceUSD,
+ token1PriceUSD,
+ feeTier,
+ contractAddress,
+ refetch,
+}: Props) {
+ const { t } = useTranslation()
+ const { account, chain } = useWeb3React()
+ const [percent, setPercent] = useState(0)
+ const tokenPairName = useMemo(() => `${currencyA.symbol}-${currencyB.symbol}`, [currencyA, currencyB])
+ const wrapperContract = usePositionManagerWrapperContract(contractAddress)
+ const { fetchWithCatchTxError, loading: pendingTx } = useCatchTxError()
+ const { toastSuccess } = useToast()
+
+ const amountA = useMemo(() => staked0Amount?.multiply(percent)?.divide(100), [staked0Amount, percent])
+ const amountB = useMemo(() => staked1Amount?.multiply(percent)?.divide(100), [staked1Amount, percent])
+
+ const withdrawThenBurn = useCallback(async () => {
+ const userInfoAmount = await wrapperContract.read.userInfo([account ?? '0x'], {})
+
+ const receipt = await fetchWithCatchTxError(() => {
+ const withdrawAmount = new BigNumber(userInfoAmount?.[0]?.toString() ?? 0)
+ .multipliedBy(percent)
+ .div(100)
+ .toNumber()
+
+ const avoidDecimalsProblem = percent === 100 ? BigInt(userInfoAmount?.[0]) : BigInt(Math.floor(withdrawAmount))
+ return wrapperContract.write.withdrawThenBurn([avoidDecimalsProblem, '0x'], { account: account ?? '0x', chain })
+ })
+
+ if (receipt?.status) {
+ refetch?.()
+ onDismiss?.()
+ toastSuccess(
+ `${t('Unstaked')}!`,
+
+ {t('Your funds have been unstaked in position manager.')}
+ ,
+ )
+ }
+ }, [chain, wrapperContract, percent, account, fetchWithCatchTxError, refetch, onDismiss, toastSuccess, t])
+
+ return (
+
+
+ {pendingTx ? (
+
+ ) : (
+ <>
+
+ {t('Removing')}:
+
+
+ {tokenPairName}
+
+
+ {vaultName}
+
+
+
+
+
+
+
+
+
+
+
+ {t('Token amounts displayed above are estimations. The final amount of tokens received may vary.')}
+ {' '}
+ >
+ )}
+
+
+ )
+})
+
+interface CurrencyAmountDisplayProps extends AtomBoxProps {
+ amount?: CurrencyAmount
+ currency: Currency
+ priceUSD?: number
+}
+
+const CurrencyAmountDisplay = memo(function CurrencyAmountDisplay({
+ amount,
+ currency,
+ priceUSD,
+ ...rest
+}: CurrencyAmountDisplayProps) {
+ const currencyDisplay = amount?.currency || currency
+ const amountInUsd = useMemo(() => new BigNumber(formatAmount(amount) ?? 0).times(priceUSD ?? 0), [amount, priceUSD])
+
+ const amountDisplay = useMemo(() => formatAmount(amount) || '0', [amount])
+ const amountInUsdDisplay = useMemo(() => {
+ const usdValue = amountInUsd.isNaN() ? '0' : amountInUsd?.toFixed(2)
+ return `(~$${usdValue})`
+ }, [amountInUsd])
+
+ return (
+
+
+
+
+ {currencyDisplay.symbol}
+
+
+
+
+ {amountDisplay}
+
+
+ {amountInUsdDisplay}
+
+
+
+ )
+})
+
+interface RemoveLiquidityButtonProps extends SpaceProps {
+ onClick?: () => void
+ isLoading?: boolean
+ disabled?: boolean
+}
+
+export const RemoveLiquidityButton = memo(function RemoveLiquidityButton({
+ onClick,
+ isLoading,
+ disabled,
+ ...rest
+}: RemoveLiquidityButtonProps) {
+ const { t } = useTranslation()
+ return (
+
+
+
+ )
+})
diff --git a/apps/web/src/views/PositionManagers/components/RewardAssets.tsx b/apps/web/src/views/PositionManagers/components/RewardAssets.tsx
new file mode 100644
index 0000000000000..89920d86881e2
--- /dev/null
+++ b/apps/web/src/views/PositionManagers/components/RewardAssets.tsx
@@ -0,0 +1,94 @@
+import { useCallback, useMemo } from 'react'
+import { Address } from 'viem'
+import { Button, Text, Box, Flex, Balance, useToast } from '@pancakeswap/uikit'
+import { useTranslation } from '@pancakeswap/localization'
+import BigNumber from 'bignumber.js'
+import { useWeb3React } from '@pancakeswap/wagmi'
+import { Currency } from '@pancakeswap/sdk'
+import useCatchTxError from 'hooks/useCatchTxError'
+import { usePositionManagerWrapperContract } from 'hooks/useContract'
+import { ToastDescriptionWithTx } from 'components/Toast'
+import { InnerCard } from './InnerCard'
+import { useEarningTokenPriceInfo } from '../hooks'
+
+interface RewardAssetsProps {
+ contractAddress: Address
+ earningToken: Currency
+ pendingReward: bigint | undefined
+ isInCakeRewardDateRange: boolean
+ refetch?: () => void
+}
+
+export const RewardAssets: React.FC = ({
+ contractAddress,
+ pendingReward,
+ earningToken,
+ refetch,
+ isInCakeRewardDateRange,
+}) => {
+ const { t } = useTranslation()
+ const { account, chain } = useWeb3React()
+ const { toastSuccess } = useToast()
+ const { fetchWithCatchTxError, loading: pendingTx } = useCatchTxError()
+ const { earningUsdValue, earningsBalance } = useEarningTokenPriceInfo(earningToken, pendingReward)
+
+ const wrapperContract = usePositionManagerWrapperContract(contractAddress)
+
+ const isDisabled = useMemo(() => pendingTx || new BigNumber(earningsBalance).lte(0), [pendingTx, earningsBalance])
+
+ const onClickHarvest = useCallback(async () => {
+ const receipt = await fetchWithCatchTxError(() =>
+ wrapperContract.write.deposit([BigInt(0)], {
+ account: account ?? '0x',
+ chain,
+ }),
+ )
+
+ if (receipt?.status) {
+ refetch?.()
+ toastSuccess(
+ `${t('Harvested')}!`,
+
+ {t('Your %symbol% earnings have been sent to your wallet!', { symbol: earningToken.symbol })}
+ ,
+ )
+ }
+ }, [earningToken, account, chain, wrapperContract, t, refetch, fetchWithCatchTxError, toastSuccess])
+ if (!isInCakeRewardDateRange && earningsBalance <= 0) return null
+ return (
+
+
+
+
+
+ {earningToken.symbol}
+
+
+ {t('Earned')}
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/apps/web/src/views/PositionManagers/components/SingleTokenWarning.tsx b/apps/web/src/views/PositionManagers/components/SingleTokenWarning.tsx
new file mode 100644
index 0000000000000..6eb27511dc51b
--- /dev/null
+++ b/apps/web/src/views/PositionManagers/components/SingleTokenWarning.tsx
@@ -0,0 +1,31 @@
+import { useTranslation } from '@pancakeswap/localization'
+import { Message, MessageText, Text, Link } from '@pancakeswap/uikit'
+import { memo } from 'react'
+
+export const SingleTokenWarning: React.FC<{ strategyInfoUrl?: string }> = memo(({ strategyInfoUrl }) => {
+ const { t } = useTranslation()
+
+ return (
+
+
+
+ {t(
+ 'Single token deposits only. The final position may consist with both tokens. Learn more about the strategy',
+ )}
+
+
+ {t('here')}
+
+
+
+ )
+})
diff --git a/apps/web/src/views/PositionManagers/components/StakedAssets.tsx b/apps/web/src/views/PositionManagers/components/StakedAssets.tsx
new file mode 100644
index 0000000000000..8604b45f28d76
--- /dev/null
+++ b/apps/web/src/views/PositionManagers/components/StakedAssets.tsx
@@ -0,0 +1,113 @@
+import { memo, useMemo } from 'react'
+import { Button, Text, RowBetween, Row, Flex, MinusIcon, AddIcon } from '@pancakeswap/uikit'
+import { CurrencyLogo } from '@pancakeswap/widgets-internal'
+import type { AtomBoxProps } from '@pancakeswap/uikit'
+import { useTranslation } from '@pancakeswap/localization'
+import { formatAmount } from '@pancakeswap/utils/formatFractions'
+import { Currency, CurrencyAmount, Price } from '@pancakeswap/sdk'
+import { styled } from 'styled-components'
+import { useTotalAssetInUsd } from '../hooks'
+
+const Title = styled(Text).attrs({
+ bold: true,
+ fontSize: '12px',
+ textTransform: 'uppercase',
+})``
+
+const ActionButton = styled(Button)`
+ padding: 0.75em;
+`
+
+interface StakedAssetsProps {
+ currencyA: Currency
+ currencyB: Currency
+ staked0Amount?: CurrencyAmount
+ staked1Amount?: CurrencyAmount
+ token0PriceUSD?: number
+ token1PriceUSD?: number
+ price?: Price
+ onAdd?: () => void
+ onRemove?: () => void
+}
+
+export const StakedAssets = memo(function StakedAssets({
+ currencyA,
+ currencyB,
+ onAdd,
+ onRemove,
+ staked0Amount,
+ staked1Amount,
+ token0PriceUSD,
+ token1PriceUSD,
+}: StakedAssetsProps) {
+ const { t } = useTranslation()
+
+ const totalAssetsInUsd = useTotalAssetInUsd(staked0Amount, staked1Amount, token0PriceUSD, token1PriceUSD)
+
+ return (
+ <>
+
+
+
+ {t('Liquidity')}
+
+ {t('Staked')}
+
+
+
+ ~${totalAssetsInUsd.toFixed(2)}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ )
+})
+
+interface CurrencyAmountDisplayProps extends AtomBoxProps {
+ amount?: CurrencyAmount
+ currency: Currency
+ priceUSD?: number
+}
+
+export const CurrencyAmountDisplay = memo(function CurrencyAmountDisplay({
+ amount,
+ currency,
+ priceUSD,
+ ...rest
+}: CurrencyAmountDisplayProps) {
+ const currencyDisplay = amount?.currency || currency
+ const amountDisplay = useMemo(() => formatAmount(amount) || '0', [amount])
+
+ const amountInUsd = useMemo(() => {
+ return Number(formatAmount(amount)) * (priceUSD ?? 0)
+ }, [amount, priceUSD])
+
+ return (
+
+
+
+
+ {currencyDisplay.symbol}
+
+
+
+ {amountDisplay}
+
+ (~${amountInUsd.toFixed(2)})
+
+
+
+ )
+})
diff --git a/apps/web/src/views/PositionManagers/components/StyledModal.tsx b/apps/web/src/views/PositionManagers/components/StyledModal.tsx
new file mode 100644
index 0000000000000..5fc89c150733d
--- /dev/null
+++ b/apps/web/src/views/PositionManagers/components/StyledModal.tsx
@@ -0,0 +1,8 @@
+import { styled } from 'styled-components'
+import { Modal } from '@pancakeswap/uikit'
+
+export const StyledModal = styled(Modal)`
+ ${({ theme }) => theme.mediaQueries.md} {
+ max-width: 350px;
+ }
+`
diff --git a/apps/web/src/views/PositionManagers/components/Tags.tsx b/apps/web/src/views/PositionManagers/components/Tags.tsx
new file mode 100644
index 0000000000000..b623e359f714a
--- /dev/null
+++ b/apps/web/src/views/PositionManagers/components/Tags.tsx
@@ -0,0 +1,26 @@
+import { memo } from 'react'
+import { Tag, TagProps } from '@pancakeswap/uikit'
+import { useTranslation } from '@pancakeswap/localization'
+import { FarmWidget } from '@pancakeswap/widgets-internal'
+
+const { V3FeeTag, CompoundingPoolTag } = FarmWidget.Tags
+
+export const FarmTag = memo(function FarmTag(props: TagProps) {
+ const { t } = useTranslation()
+ return (
+
+ {t('Farm')}
+
+ )
+})
+
+export const SingleTokenTag = memo(function SingleTokenTag(props: TagProps) {
+ const { t } = useTranslation()
+ return (
+
+ {t('Single Token')}
+
+ )
+})
+
+export { V3FeeTag as FeeTag, CompoundingPoolTag as AutoCompoundTag }
diff --git a/apps/web/src/views/PositionManagers/components/Toggles.tsx b/apps/web/src/views/PositionManagers/components/Toggles.tsx
new file mode 100644
index 0000000000000..5ecae11ac0cac
--- /dev/null
+++ b/apps/web/src/views/PositionManagers/components/Toggles.tsx
@@ -0,0 +1,27 @@
+import { styled } from 'styled-components'
+import { Text, Flex, Toggle, useMatchBreakpoints } from '@pancakeswap/uikit'
+import { useTranslation } from '@pancakeswap/localization'
+
+import { useStakeOnly } from '../hooks'
+
+const ToggleWrapper = styled(Flex).attrs({
+ alignItems: 'center',
+ ml: '0.75em',
+})`
+ ${Text} {
+ margin-left: 0.5em;
+ }
+`
+
+export function StakeOnlyToggle() {
+ const [stakeOnly, toggle] = useStakeOnly()
+ const { t } = useTranslation()
+ const { isMobile } = useMatchBreakpoints()
+
+ return (
+
+
+ {isMobile ? t('Staked') : t('Staked only')}
+
+ )
+}
diff --git a/apps/web/src/views/PositionManagers/components/TokenPairLogos.tsx b/apps/web/src/views/PositionManagers/components/TokenPairLogos.tsx
new file mode 100644
index 0000000000000..36aa30ee7cdef
--- /dev/null
+++ b/apps/web/src/views/PositionManagers/components/TokenPairLogos.tsx
@@ -0,0 +1,53 @@
+import { memo } from 'react'
+import { Currency } from '@pancakeswap/sdk'
+import { AutoRenewIcon, Box } from '@pancakeswap/uikit'
+import { styled } from 'styled-components'
+
+import { TokenPairImage } from 'components/TokenImage'
+
+const TokenPairComp = styled(TokenPairImage)`
+ z-index: 1;
+`
+
+const Container = styled(Box)`
+ position: relative;
+ width: 64px;
+ height: 64px;
+ display: block;
+`
+
+const AutoMark = styled(AutoRenewIcon).attrs({
+ color: 'currenctColor',
+ width: '20px',
+ height: '20px',
+})`
+ padding: 0.15em;
+ position: absolute;
+ right: 0;
+ bottom: 0;
+ background: ${({ theme }) => theme.colors.success};
+ border: 1px solid rgba(0, 0, 0, 0.2);
+ border-radius: 50%;
+ z-index: 2;
+`
+
+interface Props {
+ currencyA: Currency
+ currencyB: Currency
+ autoMark?: boolean
+}
+
+export const TokenPairLogos = memo(function TokenPairLogos({ currencyA, currencyB, autoMark }: Props) {
+ return (
+
+
+ {autoMark && }
+
+ )
+})
diff --git a/apps/web/src/views/PositionManagers/components/VaultInfo.tsx b/apps/web/src/views/PositionManagers/components/VaultInfo.tsx
new file mode 100644
index 0000000000000..961b604ea72a1
--- /dev/null
+++ b/apps/web/src/views/PositionManagers/components/VaultInfo.tsx
@@ -0,0 +1,98 @@
+import { useTranslation } from '@pancakeswap/localization'
+import { BaseAssets } from '@pancakeswap/position-managers'
+import { Currency, Price, Percent } from '@pancakeswap/sdk'
+import { getBalanceAmount } from '@pancakeswap/utils/formatBalance'
+import { Box, RowBetween, Text } from '@pancakeswap/uikit'
+import { memo, useMemo } from 'react'
+import { styled } from 'styled-components'
+import { SpaceProps } from 'styled-system'
+import { useTotalStakedInUsd } from 'views/PositionManagers/hooks/useTotalStakedInUsd'
+import BigNumber from 'bignumber.js'
+
+const InfoText = styled(Text).attrs({
+ fontSize: '0.875em',
+})``
+
+export interface VaultInfoProps extends SpaceProps {
+ currencyA: Currency
+ currencyB: Currency
+ isSingleDepositToken: boolean
+ allowDepositToken0: boolean
+ allowDepositToken1: boolean
+
+ // Total assets of the vault
+ vaultAssets?: BaseAssets
+
+ // timestamp of the last time position is updated
+ lastUpdatedAt?: number
+
+ // price of the current pool
+ price?: Price
+
+ managerFee?: Percent
+ poolToken0Amount?: bigint
+ poolToken1Amount?: bigint
+ token0PriceUSD?: number
+ token1PriceUSD?: number
+ rewardPerSecond: string
+ earningToken: Currency
+ isInCakeRewardDateRange: boolean
+}
+
+export const VaultInfo = memo(function VaultInfo({
+ currencyA,
+ currencyB,
+ poolToken0Amount,
+ poolToken1Amount,
+ token0PriceUSD,
+ token1PriceUSD,
+ isSingleDepositToken,
+ allowDepositToken0,
+ allowDepositToken1,
+ rewardPerSecond,
+ earningToken,
+ managerFee,
+ isInCakeRewardDateRange,
+ ...props
+}: VaultInfoProps) {
+ const { t } = useTranslation()
+
+ const tokenPerSecond = useMemo(() => {
+ return getBalanceAmount(new BigNumber(rewardPerSecond), earningToken.decimals).toNumber()
+ }, [rewardPerSecond, earningToken])
+
+ const totalStakedInUsd = useTotalStakedInUsd({
+ currencyA,
+ currencyB,
+ poolToken0Amount,
+ poolToken1Amount,
+ token0PriceUSD,
+ token1PriceUSD,
+ })
+
+ return (
+
+ {isSingleDepositToken && (
+
+ {t('Depositing Token')}:
+ {allowDepositToken0 && {currencyA.symbol}}
+ {allowDepositToken1 && {currencyB.symbol}}
+
+ )}
+
+ {t('Total staked')}:
+ {`$${totalStakedInUsd.toFixed(2)}`}
+
+ {isInCakeRewardDateRange && (
+
+ {t('Farming Rewards')}:
+ {`~${tokenPerSecond} ${earningToken.symbol} / ${t('second')}`}
+
+ )}
+
+ {t('Manager Fee')}:
+ {`${managerFee?.numerator}`}%
+
+
+ )
+})
diff --git a/apps/web/src/views/PositionManagers/components/VaultLinks.tsx b/apps/web/src/views/PositionManagers/components/VaultLinks.tsx
new file mode 100644
index 0000000000000..4a477d48bf2ff
--- /dev/null
+++ b/apps/web/src/views/PositionManagers/components/VaultLinks.tsx
@@ -0,0 +1,83 @@
+import { styled } from 'styled-components'
+import { Address } from 'viem'
+import { SpaceProps } from 'styled-system'
+import { PropsWithChildren, memo, useMemo } from 'react'
+import { FlexProps, Flex, ScanLink } from '@pancakeswap/uikit'
+import { ChainId } from '@pancakeswap/chains'
+import { useActiveChainId } from 'hooks/useActiveChainId'
+import { useTranslation } from '@pancakeswap/localization'
+import { MANAGER } from '@pancakeswap/position-managers'
+
+import { getBlockExploreLink } from 'utils'
+
+const StyledScanLink = styled(ScanLink)``
+
+const LinkContainer = styled(Flex)`
+ ${StyledScanLink} {
+ margin-top: 0.25em;
+
+ &:first-child {
+ margin-top: 0;
+ }
+ }
+`
+
+interface Props extends SpaceProps, FlexProps {
+ layout?: 'row' | 'column'
+ vaultAddress: Address
+ managerInfoUrl: string
+ strategyInfoUrl: string
+ managerAddress: string
+ projectVaultUrl?: string
+ manager: {
+ id: MANAGER
+ name: string
+ }
+}
+
+const LinkSupportChains = [ChainId.BSC, ChainId.BSC_TESTNET]
+
+export const VaultLinks = memo(function VaultLinks({
+ layout = 'column',
+ managerInfoUrl,
+ strategyInfoUrl,
+ // projectVaultUrl,
+ managerAddress,
+ vaultAddress,
+ // manager,
+ children,
+ ...props
+}: PropsWithChildren) {
+ const { t } = useTranslation()
+ const { chainId } = useActiveChainId()
+
+ // const managerInfo: BaseManager = useMemo(() => baseManagers[manager.id], [manager])
+
+ const useBscCoinFallback = useMemo(() => (chainId ? LinkSupportChains.includes(chainId) : false), [chainId])
+
+ return (
+
+ {/* {t('Pair Info')} */}
+ {t('Manager Info')}
+ {t('Strategy Info')}
+
+ {t('View Manager Address')}
+
+
+ {t('View Vault Contract')}
+
+ {/* {projectVaultUrl && managerInfo && (
+ }>
+ {t('View Vault on %managerName%', { managerName: managerInfo.name })}
+
+ )} */}
+ {children}
+
+ )
+})
diff --git a/apps/web/src/views/PositionManagers/components/ViewSwitch.tsx b/apps/web/src/views/PositionManagers/components/ViewSwitch.tsx
new file mode 100644
index 0000000000000..d765c7041ada0
--- /dev/null
+++ b/apps/web/src/views/PositionManagers/components/ViewSwitch.tsx
@@ -0,0 +1,10 @@
+import { memo } from 'react'
+import { ToggleView } from '@pancakeswap/uikit'
+
+import { useViewMode } from '../hooks'
+
+export const ViewSwitch = memo(function ViewSwitch() {
+ const { mode, setViewMode } = useViewMode()
+
+ return
+})
diff --git a/apps/web/src/views/PositionManagers/components/YieldInfo.tsx b/apps/web/src/views/PositionManagers/components/YieldInfo.tsx
new file mode 100644
index 0000000000000..9b71a1d1accf4
--- /dev/null
+++ b/apps/web/src/views/PositionManagers/components/YieldInfo.tsx
@@ -0,0 +1,65 @@
+import { useTranslation } from '@pancakeswap/localization'
+import { Box, Flex, RowBetween, Text } from '@pancakeswap/uikit'
+import { memo, useMemo } from 'react'
+import { AprResult } from '../hooks'
+import { AprButton } from './AprButton'
+import { AutoCompoundTag } from './Tags'
+
+interface Props {
+ id: number | string
+ apr: AprResult
+ isAprLoading: boolean
+ withCakeReward?: boolean
+ lpSymbol: string
+ autoCompound?: boolean
+ totalAssetsInUsd: number
+ onAprClick?: () => void
+ userLpAmounts?: bigint
+ totalSupplyAmounts?: bigint
+ precision?: bigint
+}
+
+export const YieldInfo = memo(function YieldInfo({
+ id,
+ apr,
+ isAprLoading,
+ withCakeReward,
+ autoCompound,
+ totalAssetsInUsd,
+ lpSymbol,
+ userLpAmounts,
+ totalSupplyAmounts,
+ precision,
+}: Props) {
+ const { t } = useTranslation()
+
+ const earning = useMemo(
+ () => (withCakeReward && apr.isInCakeRewardDateRange ? ['CAKE', t('Fees')].join(' + ') : t('Fees')),
+ [withCakeReward, t, apr.isInCakeRewardDateRange],
+ )
+
+ return (
+
+
+ {t('APR')}:
+
+
+
+ {t('Earn')}:
+
+ {earning}
+ {autoCompound && }
+
+
+
+ )
+})
diff --git a/apps/web/src/views/PositionManagers/components/index.ts b/apps/web/src/views/PositionManagers/components/index.ts
new file mode 100644
index 0000000000000..16f6e45e505e4
--- /dev/null
+++ b/apps/web/src/views/PositionManagers/components/index.ts
@@ -0,0 +1,21 @@
+export * from './Header'
+export * from './ControlsContainer'
+export * from './LiveSwitch'
+export * from './ViewSwitch'
+export * from './Toggles'
+export * from './Filters'
+export * from './DuoTokenVaultCard'
+export * from './CardLayout'
+export * from './Tags'
+export * from './TokenPairLogos'
+export * from './CardTitle'
+export * from './ManagerInfo'
+export * from './CardSection'
+export * from './LiquidityManagement'
+export * from './AddLiquidity'
+export * from './InnerCard'
+export * from './StakedAssets'
+export * from './RemoveLiquidity'
+export * from './PercentSlider'
+export * from './ExpandableSection'
+export * from './VaultLinks'
diff --git a/apps/web/src/views/PositionManagers/containers/Controls.tsx b/apps/web/src/views/PositionManagers/containers/Controls.tsx
new file mode 100644
index 0000000000000..a3e9354268a8e
--- /dev/null
+++ b/apps/web/src/views/PositionManagers/containers/Controls.tsx
@@ -0,0 +1,45 @@
+import { useMatchBreakpoints } from '@pancakeswap/uikit'
+
+import {
+ ControlsContainer,
+ ControlGroup,
+ LiveSwitch,
+ // ViewSwitch,
+ StakeOnlyToggle,
+ SortFilter,
+ SearchFilter,
+} from '../components'
+
+export function Controls() {
+ const { isDesktop } = useMatchBreakpoints()
+
+ const controls = isDesktop ? (
+
+
+ {/* */}
+
+
+
+
+
+
+
+
+ ) : (
+
+
+ {/* */}
+
+
+
+
+
+
+
+
+
+
+ )
+
+ return controls
+}
diff --git a/apps/web/src/views/PositionManagers/containers/PCSVaultCard.tsx b/apps/web/src/views/PositionManagers/containers/PCSVaultCard.tsx
new file mode 100644
index 0000000000000..5bc6e3fc86e62
--- /dev/null
+++ b/apps/web/src/views/PositionManagers/containers/PCSVaultCard.tsx
@@ -0,0 +1,195 @@
+import { PCSDuoTokenVaultConfig } from '@pancakeswap/position-managers'
+import { usePositionManagerAdepterContract } from 'hooks/useContract'
+import { memo, useMemo, useEffect } from 'react'
+import { FarmV3DataWithPriceAndUserInfo } from '@pancakeswap/farms'
+import { useQuery } from '@tanstack/react-query'
+import { CurrencyAmount } from '@pancakeswap/sdk'
+import { DuoTokenVaultCard } from '../components'
+import {
+ usePCSVault,
+ AprData,
+ AprDataInfo,
+ PositionManagerDetailsData,
+ useEarningTokenPriceInfo,
+ useTotalStakedInUsd,
+ usePositionInfo,
+ useApr,
+ useTotalAssetInUsd,
+ useTokenPriceFromSubgraph,
+} from '../hooks'
+
+interface Props {
+ config: PCSDuoTokenVaultConfig
+ farmsV3: FarmV3DataWithPriceAndUserInfo[]
+ aprDataList: AprData
+ updatePositionMangerDetailsData: (id: number, newData: PositionManagerDetailsData) => void
+}
+
+export const PCSVaultCard = memo(function PCSVaultCard({
+ config,
+ farmsV3,
+ aprDataList,
+ updatePositionMangerDetailsData,
+}: Props) {
+ const { vault } = usePCSVault({ config })
+ const {
+ id,
+ currencyA,
+ currencyB,
+ earningToken,
+ name,
+ strategy,
+ feeTier,
+ autoFarm,
+ manager,
+ address,
+ adapterAddress,
+ isSingleDepositToken,
+ allowDepositToken0,
+ allowDepositToken1,
+ priceFromV3FarmPid,
+ managerInfoUrl,
+ strategyInfoUrl,
+ projectVaultUrl,
+ learnMoreAboutUrl,
+ } = vault
+
+ const adapterContract = usePositionManagerAdepterContract(adapterAddress ?? '0x')
+ const tokenRatio = useQuery(
+ ['adapterAddress', adapterAddress],
+ async () => {
+ const result = await adapterContract.read.tokenPerShare()
+ return Number((result[0] * 100n) / result[1]) / 100
+ },
+ {
+ enabled: !!adapterContract,
+ },
+ ).data
+ const priceFromSubgraph = useTokenPriceFromSubgraph(
+ priceFromV3FarmPid ? undefined : currencyA.isToken ? currencyA.address.toLowerCase() : undefined,
+ priceFromV3FarmPid ? undefined : currencyB.isToken ? currencyB.address.toLowerCase() : undefined,
+ )
+
+ const info = usePositionInfo(address, adapterAddress ?? '0x')
+
+ const tokensPriceUSD = useMemo(() => {
+ const farm = farmsV3.find((d) => d.pid === priceFromV3FarmPid)
+ if (!farm) return priceFromSubgraph
+ const isToken0And1Reversed =
+ farm.token.address.toLowerCase() === (currencyB.isToken ? currencyB.address.toLowerCase() : '')
+ return {
+ token0: Number(isToken0And1Reversed ? farm.quoteTokenPriceBusd : farm.tokenPriceBusd),
+ token1: Number(isToken0And1Reversed ? farm.tokenPriceBusd : farm.quoteTokenPriceBusd),
+ }
+ }, [farmsV3, priceFromV3FarmPid, priceFromSubgraph, currencyB])
+
+ const managerInfo = useMemo(
+ () => ({
+ id: manager.id,
+ name: manager.name,
+ }),
+ [manager],
+ )
+
+ const aprDataInfo = useMemo(() => {
+ const { isLoading, data } = aprDataList
+ return {
+ isLoading,
+ info: data?.length
+ ? data?.find((apr: AprDataInfo) => apr.lpAddress.toLowerCase() === info.vaultAddress?.toLowerCase())
+ : undefined,
+ }
+ }, [info.vaultAddress, aprDataList])
+
+ const { earningUsdValue } = useEarningTokenPriceInfo(earningToken, info?.pendingReward)
+
+ const totalStakedInUsd = useTotalStakedInUsd({
+ currencyA,
+ currencyB,
+ poolToken0Amount: info?.poolToken0Amounts,
+ poolToken1Amount: info?.poolToken1Amounts,
+ token0PriceUSD: tokensPriceUSD?.token0,
+ token1PriceUSD: tokensPriceUSD?.token1,
+ })
+
+ const apr = useApr({
+ currencyA,
+ currencyB,
+ poolToken0Amount: info?.poolToken0Amounts,
+ poolToken1Amount: info?.poolToken1Amounts,
+ token0PriceUSD: tokensPriceUSD?.token0,
+ token1PriceUSD: tokensPriceUSD?.token1,
+ rewardPerSecond: info.rewardPerSecond,
+ earningToken,
+ avgToken0Amount: aprDataInfo?.info?.token0 ?? 0,
+ avgToken1Amount: aprDataInfo?.info?.token1 ?? 0,
+ rewardEndTime: info.endTimestamp,
+ rewardStartTime: info.startTimestamp,
+ })
+
+ const staked0Amount = info?.userToken0Amounts
+ ? CurrencyAmount.fromRawAmount(currencyA, info.userToken0Amounts)
+ : undefined
+ const staked1Amount = info?.userToken1Amounts
+ ? CurrencyAmount.fromRawAmount(currencyB, info.userToken1Amounts)
+ : undefined
+ const totalAssetsInUsd = useTotalAssetInUsd(
+ staked0Amount,
+ staked1Amount,
+ tokensPriceUSD?.token0,
+ tokensPriceUSD?.token1,
+ )
+
+ useEffect(() => {
+ updatePositionMangerDetailsData(id, {
+ apr: apr ? Number(apr.combinedApr) : 0,
+ earned: earningUsdValue,
+ totalStaked: totalStakedInUsd,
+ isUserStaked: totalAssetsInUsd > 0,
+ })
+ }, [earningUsdValue, totalStakedInUsd, id, totalAssetsInUsd, apr, updatePositionMangerDetailsData])
+ return (
+
+ {id}
+
+ )
+})
diff --git a/apps/web/src/views/PositionManagers/containers/VaultCards.tsx b/apps/web/src/views/PositionManagers/containers/VaultCards.tsx
new file mode 100644
index 0000000000000..d852945cefd72
--- /dev/null
+++ b/apps/web/src/views/PositionManagers/containers/VaultCards.tsx
@@ -0,0 +1,82 @@
+import { memo } from 'react'
+import { isPCSVaultConfig } from '@pancakeswap/position-managers'
+import { useFarmsV3WithPositionsAndBooster } from 'state/farmsV3/hooks'
+
+import {
+ useVaultConfigs,
+ useFetchApr,
+ useSearch,
+ useSortBy,
+ usePositionManagerStatus,
+ PositionManagerStatus,
+ usePositionManagerDetailsData,
+ useStakeOnly,
+} from '../hooks'
+import { PCSVaultCard } from './PCSVaultCard'
+import { CardLayout } from '../components'
+
+export const VaultCards = memo(function VaultCards() {
+ const configs = useVaultConfigs()
+ const [search] = useSearch()
+ const { status } = usePositionManagerStatus()
+ const [sortBy] = useSortBy()
+ const [stakeOnly] = useStakeOnly()
+ const { data: positionMangerDetailsData, updateData: updatePositionMangerDetailsData } =
+ usePositionManagerDetailsData()
+ const aprDataList = useFetchApr()
+ const { farmsWithPositions: farmsV3 } = useFarmsV3WithPositionsAndBooster()
+ const cards = configs
+ .filter((d) => {
+ if (stakeOnly) {
+ return positionMangerDetailsData?.[d.id]?.isUserStaked
+ }
+ return true
+ })
+ .filter((d) => {
+ if (search) {
+ return `${d.currencyA.symbol}-${d.currencyB.symbol}${d.name}`.toLowerCase().includes(search.toLowerCase())
+ }
+ return true
+ })
+ .filter(() => {
+ if (status === PositionManagerStatus.FINISHED) {
+ return false
+ // return d.endTimestamp <= Date.now() / 1000
+ }
+ return true
+ // return d.endTimestamp > Date.now() / 1000
+ })
+ .sort((a, b) => {
+ if (sortBy === 'apr') {
+ return (positionMangerDetailsData?.[b.id]?.apr ?? 0) - (positionMangerDetailsData?.[a.id]?.apr ?? 0)
+ }
+ if (sortBy === 'earned') {
+ return (positionMangerDetailsData?.[b.id]?.earned ?? 0) - (positionMangerDetailsData?.[a.id]?.earned ?? 0)
+ }
+ if (sortBy === 'totalStaked') {
+ return (
+ (positionMangerDetailsData?.[b.id]?.totalStaked ?? 0) - (positionMangerDetailsData?.[a.id]?.totalStaked ?? 0)
+ )
+ }
+ if (sortBy === 'latest') {
+ return b.id - a.id
+ }
+ return a.id - b.id
+ })
+ .map((config) => {
+ if (isPCSVaultConfig(config)) {
+ return (
+
+ )
+ }
+ return null
+ })
+
+ return {cards}
+})
diff --git a/apps/web/src/views/PositionManagers/containers/index.ts b/apps/web/src/views/PositionManagers/containers/index.ts
new file mode 100644
index 0000000000000..0cd39b760c4b7
--- /dev/null
+++ b/apps/web/src/views/PositionManagers/containers/index.ts
@@ -0,0 +1,2 @@
+export * from './Controls'
+export * from './VaultCards'
diff --git a/apps/web/src/views/PositionManagers/hooks/index.ts b/apps/web/src/views/PositionManagers/hooks/index.ts
new file mode 100644
index 0000000000000..487c14b75ba7e
--- /dev/null
+++ b/apps/web/src/views/PositionManagers/hooks/index.ts
@@ -0,0 +1,14 @@
+export * from './usePositionManagerStatus'
+export * from './useViewMode'
+export * from './useToggles'
+export * from './useFilters'
+export * from './useVault'
+export * from './useVaultConfigs'
+export * from './useFetchApr'
+export * from './usePositionManagerDetailsData'
+export * from './useEarningTokenPriceInfo'
+export * from './useTotalStakedInUsd'
+export * from './useAdapterInfo'
+export * from './useApr'
+export * from './useTotalAssetInUsd'
+export * from './useTokenPriceFromSubgraph'
diff --git a/apps/web/src/views/PositionManagers/hooks/useAdapterInfo.ts b/apps/web/src/views/PositionManagers/hooks/useAdapterInfo.ts
new file mode 100644
index 0000000000000..4042a2c19a20c
--- /dev/null
+++ b/apps/web/src/views/PositionManagers/hooks/useAdapterInfo.ts
@@ -0,0 +1,245 @@
+import { useQuery } from '@tanstack/react-query'
+import { positionManagerAdapterABI, positionManagerWrapperABI } from '@pancakeswap/position-managers'
+import { usePositionManagerWrapperContract } from 'hooks/useContract'
+import { useActiveChainId } from 'hooks/useActiveChainId'
+import { publicClient } from 'utils/wagmi'
+import { Address } from 'viem'
+import useActiveWeb3React from 'hooks/useActiveWeb3React'
+import { Percent } from '@pancakeswap/sdk'
+
+export async function getAdapterTokensAmounts({ address, chainId }): Promise<{
+ token0Amounts: bigint
+ token1Amounts: bigint
+ token0PerShare: bigint
+ token1PerShare: bigint
+ precision: bigint
+ totalSupply: bigint
+ managerFeePercentage: Percent
+ vaultAddress: Address
+ managerAddress: Address
+} | null> {
+ const [totalSupplyData, tokenPerShareData, PRECISIONData, managerFeeData, vaultAddressData, managerAddressData] =
+ await publicClient({
+ chainId,
+ }).multicall({
+ contracts: [
+ {
+ address,
+ functionName: 'totalSupply',
+ abi: positionManagerAdapterABI,
+ },
+ {
+ address,
+ functionName: 'tokenPerShare',
+ abi: positionManagerAdapterABI,
+ },
+ {
+ address,
+ functionName: 'PRECISION',
+ abi: positionManagerAdapterABI,
+ },
+ {
+ address,
+ functionName: 'managerFee',
+ abi: positionManagerAdapterABI,
+ },
+ {
+ address,
+ functionName: 'vault',
+ abi: positionManagerAdapterABI,
+ },
+ {
+ address,
+ functionName: 'manager',
+ abi: positionManagerAdapterABI,
+ },
+ ],
+ })
+
+ if (
+ !totalSupplyData?.result ||
+ !tokenPerShareData?.result ||
+ !PRECISIONData?.result ||
+ (!managerFeeData?.result && managerFeeData?.result !== 0n) ||
+ !vaultAddressData?.result ||
+ !managerAddressData?.result
+ )
+ return null
+
+ const [totalSupply, tokenPerShare, PRECISION, managerFee, vaultAddress, managerAddress] = [
+ totalSupplyData.result,
+ tokenPerShareData.result,
+ PRECISIONData.result,
+ managerFeeData.result,
+ vaultAddressData.result,
+ managerAddressData.result,
+ ]
+ const precision = PRECISION ?? BigInt(0)
+ const token0Amounts = (totalSupply * tokenPerShare[0]) / precision
+ const token1Amounts = (totalSupply * tokenPerShare[1]) / precision
+ const managerFeePercentage = new Percent(Number((managerFee * 100n) / precision), 10000)
+
+ return {
+ token0Amounts,
+ token1Amounts,
+ token0PerShare: tokenPerShare[0],
+ token1PerShare: tokenPerShare[1],
+ precision,
+ totalSupply,
+ managerFeePercentage,
+ vaultAddress,
+ managerAddress,
+ }
+}
+
+export const useAdapterTokensAmounts = (adapterAddress: Address) => {
+ const { chainId } = useActiveChainId()
+ const { data, refetch } = useQuery(
+ ['AdapterTokensAmounts', adapterAddress, chainId],
+ () => getAdapterTokensAmounts({ address: adapterAddress, chainId }),
+ {
+ enabled: !!adapterAddress && !!chainId,
+ refetchInterval: 3000,
+ staleTime: 3000,
+ cacheTime: 3000,
+ },
+ )
+ return { data, refetch }
+}
+
+export const useUserAmounts = (wrapperAddress: Address) => {
+ const { account } = useActiveWeb3React()
+ const contract = usePositionManagerWrapperContract(wrapperAddress)
+ const { data, refetch } = useQuery(
+ ['useUserAmounts', wrapperAddress, account],
+ () => contract.read.userInfo([account ?? '0x']),
+ {
+ enabled: !!wrapperAddress && !!account,
+ refetchInterval: 3000,
+ staleTime: 3000,
+ cacheTime: 3000,
+ },
+ )
+ return { data, refetch }
+}
+
+export const useWrapperStaticData = (wrapperAddress: Address) => {
+ const { chainId } = useActiveChainId()
+ const { data, refetch } = useQuery(
+ ['useWrapperStaticData', wrapperAddress, chainId],
+ () => getWrapperStaticData({ address: wrapperAddress, chainId }),
+ {
+ enabled: !!wrapperAddress && !!chainId,
+ refetchInterval: 30000,
+ staleTime: 30000,
+ cacheTime: 30000,
+ },
+ )
+ return { data, refetch }
+}
+
+export const usePositionInfo = (wrapperAddress: Address, adapterAddress: Address) => {
+ const { data: userAmounts, refetch: refetchUserAmounts } = useUserAmounts(wrapperAddress)
+ const { data: poolAmounts, refetch: refetchPoolAmounts } = useAdapterTokensAmounts(adapterAddress)
+ const { data: pendingReward, refetch: refetchPendingReward } = useUserPendingRewardAmounts(wrapperAddress)
+ const { data: staticData } = useWrapperStaticData(wrapperAddress)
+
+ const poolAndUserAmountsReady = userAmounts && poolAmounts
+
+ return {
+ pendingReward,
+ poolToken0Amounts: poolAmounts?.token0Amounts ?? BigInt(0),
+ poolToken1Amounts: poolAmounts?.token1Amounts ?? BigInt(0),
+ userToken0Amounts: poolAndUserAmountsReady
+ ? (userAmounts[0] * poolAmounts?.token0PerShare) / poolAmounts.precision
+ : BigInt(0),
+ userToken1Amounts: poolAndUserAmountsReady
+ ? (userAmounts[0] * poolAmounts?.token1PerShare) / poolAmounts.precision
+ : BigInt(0),
+ userVaultPercentage: poolAndUserAmountsReady
+ ? new Percent(userAmounts[0], poolAmounts.totalSupply)
+ : new Percent(0, 100),
+ refetchPositionInfo: () => {
+ refetchUserAmounts()
+ refetchPoolAmounts()
+ refetchPendingReward()
+ },
+ startTimestamp: staticData?.startTimestamp ? Number(staticData.startTimestamp) : 0,
+ endTimestamp: staticData?.endTimestamp ? Number(staticData.endTimestamp) : 0,
+ rewardPerSecond: staticData?.rewardPerSecond ?? '',
+ totalSupplyAmounts: poolAmounts?.totalSupply,
+ userLpAmounts: userAmounts?.[0],
+ precision: poolAmounts?.precision,
+ adapterAddress: staticData?.adapterAddress,
+ vaultAddress: poolAmounts?.vaultAddress,
+ managerFeePercentage: poolAmounts?.managerFeePercentage,
+ managerAddress: poolAmounts?.managerAddress,
+ }
+}
+
+export const useUserPendingRewardAmounts = (wrapperAddress: Address) => {
+ const { account } = useActiveWeb3React()
+ const contract = usePositionManagerWrapperContract(wrapperAddress)
+ const { data, refetch } = useQuery(
+ ['useUserPendingRewardAmounts', account, wrapperAddress],
+ () => contract.read.pendingReward([account ?? '0x']),
+ {
+ enabled: !!account,
+ refetchInterval: 3000,
+ staleTime: 3000,
+ cacheTime: 3000,
+ },
+ )
+ return { data, refetch }
+}
+
+export async function getWrapperStaticData({ address, chainId }): Promise<{
+ startTimestamp: string
+ endTimestamp: string
+ rewardPerSecond: string
+ adapterAddress: Address
+} | null> {
+ const [startTimestampData, endTimestampData, rewardPerSecondData, adapterAddrData] = await publicClient({
+ chainId,
+ }).multicall({
+ contracts: [
+ {
+ address,
+ functionName: 'startTimestamp',
+ abi: positionManagerWrapperABI,
+ },
+ {
+ address,
+ functionName: 'endTimestamp',
+ abi: positionManagerWrapperABI,
+ },
+ {
+ address,
+ functionName: 'rewardPerSecond',
+ abi: positionManagerWrapperABI,
+ },
+ {
+ address,
+ functionName: 'adapterAddr',
+ abi: positionManagerWrapperABI,
+ },
+ ],
+ })
+
+ if (!startTimestampData.result || !endTimestampData.result || !rewardPerSecondData.result || !adapterAddrData.result)
+ return null
+
+ const [startTimestamp, endTimestamp, rewardPerSecond, adapterAddress] = [
+ startTimestampData.result,
+ endTimestampData.result,
+ rewardPerSecondData.result,
+ adapterAddrData.result,
+ ]
+
+ return {
+ startTimestamp: startTimestamp.toString(),
+ endTimestamp: endTimestamp.toString(),
+ rewardPerSecond: rewardPerSecond?.toString() ?? '',
+ adapterAddress,
+ }
+}
diff --git a/apps/web/src/views/PositionManagers/hooks/useApr.ts b/apps/web/src/views/PositionManagers/hooks/useApr.ts
new file mode 100644
index 0000000000000..4b3bea9413925
--- /dev/null
+++ b/apps/web/src/views/PositionManagers/hooks/useApr.ts
@@ -0,0 +1,101 @@
+import { useMemo } from 'react'
+import { Currency } from '@pancakeswap/sdk'
+import BigNumber from 'bignumber.js'
+import { getBalanceAmount } from '@pancakeswap/utils/formatBalance'
+import { useTotalStakedInUsd } from 'views/PositionManagers/hooks/useTotalStakedInUsd'
+import { YEAR_IN_SECONDS } from '@pancakeswap/utils/getTimePeriods'
+import { useCakePrice } from 'hooks/useCakePrice'
+import { BIG_ZERO } from '@pancakeswap/utils/bigNumber'
+
+interface AprProps {
+ currencyA: Currency
+ currencyB: Currency
+ rewardPerSecond: string
+ avgToken0Amount: number
+ avgToken1Amount: number
+ poolToken0Amount?: bigint
+ poolToken1Amount?: bigint
+ token0PriceUSD?: number
+ token1PriceUSD?: number
+ earningToken: Currency
+ rewardEndTime: number
+ rewardStartTime: number
+}
+
+export interface AprResult {
+ combinedApr: string
+ lpApr: string
+ cakeYieldApr: string
+ isInCakeRewardDateRange: boolean
+}
+
+const ONE_YEAR = 365
+
+export const useApr = ({
+ currencyA,
+ currencyB,
+ rewardPerSecond,
+ poolToken0Amount,
+ poolToken1Amount,
+ token0PriceUSD,
+ token1PriceUSD,
+ avgToken0Amount,
+ avgToken1Amount,
+ earningToken,
+ rewardEndTime,
+ rewardStartTime,
+}: AprProps): AprResult => {
+ const cakePriceBusd = useCakePrice()
+
+ const isInCakeRewardDateRange = useMemo(
+ () => Date.now() / 1000 < rewardEndTime && Date.now() / 1000 >= rewardStartTime,
+ [rewardEndTime, rewardStartTime],
+ )
+
+ const totalStakedInUsd = useTotalStakedInUsd({
+ currencyA,
+ currencyB,
+ poolToken0Amount,
+ poolToken1Amount,
+ token0PriceUSD,
+ token1PriceUSD,
+ })
+
+ const totalLpApr = useMemo(() => {
+ const totalToken0Usd = getBalanceAmount(new BigNumber(avgToken0Amount), currencyA.decimals).times(
+ token0PriceUSD ?? 0,
+ )
+ const totalToken1Usd = getBalanceAmount(new BigNumber(avgToken1Amount), currencyB.decimals).times(
+ token1PriceUSD ?? 0,
+ )
+
+ const totalAvgStakedInUsd = totalToken0Usd.plus(totalToken1Usd)
+
+ return totalAvgStakedInUsd.times(ONE_YEAR).div(totalStakedInUsd).times(100)
+ }, [avgToken0Amount, avgToken1Amount, currencyA, currencyB, token0PriceUSD, token1PriceUSD, totalStakedInUsd])
+
+ const cakeYieldApr = useMemo(() => {
+ if (!isInCakeRewardDateRange) {
+ return BIG_ZERO
+ }
+
+ return getBalanceAmount(new BigNumber(rewardPerSecond), earningToken.decimals)
+ .times(YEAR_IN_SECONDS)
+ .times(cakePriceBusd)
+ .div(totalStakedInUsd)
+ .times(100)
+ }, [isInCakeRewardDateRange, earningToken, rewardPerSecond, cakePriceBusd, totalStakedInUsd])
+
+ const totalApr = useMemo(() => cakeYieldApr.plus(totalLpApr), [cakeYieldApr, totalLpApr])
+
+ const aprData = useMemo(() => {
+ return {
+ combinedApr: !totalApr.isNaN() ? totalApr.toFixed(2) ?? '-' : '',
+ lpApr: !totalLpApr.isNaN() ? totalLpApr.toFixed(2) ?? '-' : '',
+ cakeYieldApr: !cakeYieldApr.isNaN() ? cakeYieldApr.toFixed(2) ?? '-' : '',
+ isInCakeRewardDateRange,
+ }
+ }, [totalApr, totalLpApr, cakeYieldApr, isInCakeRewardDateRange])
+
+ return aprData
+}
diff --git a/apps/web/src/views/PositionManagers/hooks/useEarningTokenPriceInfo.ts b/apps/web/src/views/PositionManagers/hooks/useEarningTokenPriceInfo.ts
new file mode 100644
index 0000000000000..a40b269ed4184
--- /dev/null
+++ b/apps/web/src/views/PositionManagers/hooks/useEarningTokenPriceInfo.ts
@@ -0,0 +1,19 @@
+import { useStablecoinPrice } from 'hooks/useBUSDPrice'
+import { getBalanceAmount } from '@pancakeswap/utils/formatBalance'
+import BigNumber from 'bignumber.js'
+import { useMemo } from 'react'
+import { Currency } from '@pancakeswap/sdk'
+
+export const useEarningTokenPriceInfo = (earningToken: Currency, pendingReward: bigint | undefined) => {
+ const earningTokenPrice = useStablecoinPrice(earningToken ?? undefined, { enabled: !!earningToken })
+ const earningsBalance = useMemo(
+ () => getBalanceAmount(new BigNumber(pendingReward?.toString() ?? 0), earningToken.decimals).toNumber(),
+ [pendingReward, earningToken],
+ )
+
+ const earningUsdValue = useMemo(
+ () => new BigNumber(earningsBalance).times(earningTokenPrice?.toSignificant() ?? 0).toNumber(),
+ [earningsBalance, earningTokenPrice],
+ )
+ return { earningUsdValue, earningTokenPrice, earningsBalance }
+}
diff --git a/apps/web/src/views/PositionManagers/hooks/useFetchApr.ts b/apps/web/src/views/PositionManagers/hooks/useFetchApr.ts
new file mode 100644
index 0000000000000..0811a83bd67b3
--- /dev/null
+++ b/apps/web/src/views/PositionManagers/hooks/useFetchApr.ts
@@ -0,0 +1,62 @@
+import { useMemo } from 'react'
+import { ChainId } from '@pancakeswap/chains'
+import { Address } from 'viem'
+import { useQuery } from '@tanstack/react-query'
+import { useActiveChainId } from 'hooks/useActiveChainId'
+import {
+ POSITION_MANAGER_API,
+ SUPPORTED_CHAIN_IDS as POSITION_MANAGERS_SUPPORTED_CHAINS,
+} from '@pancakeswap/position-managers'
+
+export interface AprDataInfo {
+ token0: number
+ token1: number
+ chainId: ChainId
+ lpAddress: Address
+ calculationDays: number
+}
+
+export interface AprData {
+ data: AprDataInfo[]
+ isLoading: boolean
+ refetch: () => void
+}
+
+export const useFetchApr = (): AprData => {
+ const { chainId } = useActiveChainId()
+
+ const supportedChain = useMemo((): boolean => {
+ const chainIds = POSITION_MANAGERS_SUPPORTED_CHAINS
+ return Boolean(chainId && chainIds.includes(chainId))
+ }, [chainId])
+
+ const { data, isLoading, refetch } = useQuery(
+ ['/fetch-position-manager-apr', chainId],
+ async () => {
+ try {
+ const response = await fetch(`${POSITION_MANAGER_API}/${chainId}/vault/feeAvg`, {
+ method: 'POST',
+ headers: {
+ Accept: 'application/json',
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ avgFeeCalculationDays: '7',
+ }),
+ })
+
+ const result: AprDataInfo[] = await response.json()
+ return result
+ } catch (error) {
+ console.error(`Fetch fetch APR API Error: ${error}`)
+ return []
+ }
+ },
+ {
+ enabled: supportedChain,
+ refetchOnWindowFocus: false,
+ },
+ )
+
+ return { data: data ?? [], isLoading, refetch }
+}
diff --git a/apps/web/src/views/PositionManagers/hooks/useFilters.ts b/apps/web/src/views/PositionManagers/hooks/useFilters.ts
new file mode 100644
index 0000000000000..71647473c1489
--- /dev/null
+++ b/apps/web/src/views/PositionManagers/hooks/useFilters.ts
@@ -0,0 +1,35 @@
+import { useRouter } from 'next/router'
+import { useCallback, useMemo } from 'react'
+import { updateQuery } from '@pancakeswap/utils/clientRouter'
+
+function filterHookFactory(filterName: string) {
+ return function useFilter(): [string | undefined, (filter: string | undefined) => void] {
+ const router = useRouter()
+ const filter = useMemo(() => {
+ const value = router.query[filterName]
+ if (Array.isArray(value)) {
+ return value[0]
+ }
+ return value
+ }, [router.query])
+
+ const setFilter = useCallback(
+ (value?: string) => {
+ router.push(
+ updateQuery(router.asPath, {
+ [filterName]: value,
+ }),
+ '',
+ { scroll: false },
+ )
+ },
+ [router],
+ )
+
+ return [filter, setFilter]
+ }
+}
+
+export const useSortBy = filterHookFactory('sortBy')
+
+export const useSearch = filterHookFactory('search')
diff --git a/apps/web/src/views/PositionManagers/hooks/usePositionManagerDetailsData.ts b/apps/web/src/views/PositionManagers/hooks/usePositionManagerDetailsData.ts
new file mode 100644
index 0000000000000..3541f726dfa7a
--- /dev/null
+++ b/apps/web/src/views/PositionManagers/hooks/usePositionManagerDetailsData.ts
@@ -0,0 +1,24 @@
+import { useState, useCallback } from 'react'
+
+export interface PositionManagerDetailsData {
+ apr: number
+ earned: number
+ totalStaked: number
+ isUserStaked: boolean
+}
+
+export const usePositionManagerDetailsData = () => {
+ const [data, setData] = useState>({})
+ const updateData = useCallback((id: number, newData: PositionManagerDetailsData) => {
+ setData((prevData) => {
+ return {
+ ...prevData,
+ [id]: newData,
+ }
+ })
+ }, [])
+ return {
+ data,
+ updateData,
+ }
+}
diff --git a/apps/web/src/views/PositionManagers/hooks/usePositionManagerStatus.ts b/apps/web/src/views/PositionManagers/hooks/usePositionManagerStatus.ts
new file mode 100644
index 0000000000000..e1aa6399cc9c0
--- /dev/null
+++ b/apps/web/src/views/PositionManagers/hooks/usePositionManagerStatus.ts
@@ -0,0 +1,42 @@
+import { useRouter } from 'next/router'
+import { useCallback, useMemo } from 'react'
+import { getPathWithQueryPreserved } from '@pancakeswap/utils/clientRouter'
+
+export enum PositionManagerStatus {
+ LIVE,
+ FINISHED,
+}
+
+const LIVE_ROUTE = '/position-managers'
+const FINISHED_ROUTE = `${LIVE_ROUTE}/history`
+
+export function usePositionManagerStatus() {
+ const router = useRouter()
+
+ const pmStatus = useMemo(
+ () =>
+ router.query.slug?.length === 1 && router.query.slug[0] === 'history'
+ ? PositionManagerStatus.FINISHED
+ : PositionManagerStatus.LIVE,
+ [router.query],
+ )
+ const setStatus = useCallback(
+ (nextStatus: PositionManagerStatus) => {
+ if (pmStatus === nextStatus) {
+ return
+ }
+ router.push(
+ getPathWithQueryPreserved(
+ router.asPath,
+ nextStatus === PositionManagerStatus.LIVE ? LIVE_ROUTE : FINISHED_ROUTE,
+ ),
+ )
+ },
+ [pmStatus, router],
+ )
+
+ return {
+ status: pmStatus,
+ setStatus,
+ }
+}
diff --git a/apps/web/src/views/PositionManagers/hooks/useToggles.ts b/apps/web/src/views/PositionManagers/hooks/useToggles.ts
new file mode 100644
index 0000000000000..7a7d6393e77af
--- /dev/null
+++ b/apps/web/src/views/PositionManagers/hooks/useToggles.ts
@@ -0,0 +1,23 @@
+import { useRouter } from 'next/router'
+import { useCallback, useMemo } from 'react'
+import { updateQuery } from '@pancakeswap/utils/clientRouter'
+
+function toggleHookFactory(queryName: string) {
+ return function useToggle(): [boolean, () => void] {
+ const router = useRouter()
+ const on = useMemo(() => router.query[queryName] === '1', [router.query])
+ const toggle = useCallback(() => {
+ router.push(
+ updateQuery(router.asPath, {
+ [queryName]: !on ? '1' : undefined,
+ }),
+ '',
+ { scroll: false },
+ )
+ }, [router, on])
+
+ return [on, toggle]
+ }
+}
+
+export const useStakeOnly = toggleHookFactory('stakeOnly')
diff --git a/apps/web/src/views/PositionManagers/hooks/useTokenPriceFromSubgraph.ts b/apps/web/src/views/PositionManagers/hooks/useTokenPriceFromSubgraph.ts
new file mode 100644
index 0000000000000..f597a9ae2c13a
--- /dev/null
+++ b/apps/web/src/views/PositionManagers/hooks/useTokenPriceFromSubgraph.ts
@@ -0,0 +1,42 @@
+import { v3Clients } from 'utils/graphql'
+import { useActiveChainId } from 'hooks/useActiveChainId'
+import { gql, GraphQLClient } from 'graphql-request'
+import useSWRImmutable from 'swr/immutable'
+
+interface TokenPrice {
+ id: string
+ derivedUSD: string
+}
+
+const GET_TOKEN_PRICE_QUERY = (token0Address: string, token1Address: string) => gql`
+ {
+ tokens(
+ where: { id_in: ["${token0Address}", "${token1Address}"] }
+ ) {
+ id
+ derivedUSD
+ }
+ }
+`
+const fetchTokenPrice = async (dataClient: GraphQLClient, token0Address: string, token1Address: string) => {
+ try {
+ const data = await dataClient.request<{ tokens: TokenPrice[] }>(GET_TOKEN_PRICE_QUERY(token0Address, token1Address))
+ return data
+ } catch (e) {
+ console.error(e)
+ }
+ return null
+}
+
+export const useTokenPriceFromSubgraph = (token0Address: string | undefined, token1Address: string | undefined) => {
+ const { chainId } = useActiveChainId()
+ const { data } = useSWRImmutable(
+ !!chainId && token0Address && token1Address && [`positonManager${token0Address ?? ''}-${token1Address}`, chainId],
+ () => fetchTokenPrice(v3Clients[chainId ?? -1], token0Address ?? '', token1Address ?? ''),
+ { errorRetryCount: 3, errorRetryInterval: 3000 },
+ )
+ return {
+ token0: data?.tokens?.[0]?.derivedUSD ? Number(data.tokens[0].derivedUSD) : 0,
+ token1: data?.tokens?.[1]?.derivedUSD ? Number(data.tokens[1].derivedUSD) : 0,
+ }
+}
diff --git a/apps/web/src/views/PositionManagers/hooks/useTotalAssetInUsd.ts b/apps/web/src/views/PositionManagers/hooks/useTotalAssetInUsd.ts
new file mode 100644
index 0000000000000..702e635092830
--- /dev/null
+++ b/apps/web/src/views/PositionManagers/hooks/useTotalAssetInUsd.ts
@@ -0,0 +1,18 @@
+import { useMemo } from 'react'
+import { formatAmount } from '@pancakeswap/utils/formatFractions'
+import { Currency, CurrencyAmount } from '@pancakeswap/sdk'
+
+export const useTotalAssetInUsd = (
+ staked0Amount?: CurrencyAmount,
+ staked1Amount?: CurrencyAmount,
+ token0PriceUSD?: number,
+ token1PriceUSD?: number,
+) => {
+ const totalAssetsInUsd = useMemo(() => {
+ return (
+ Number(formatAmount(staked0Amount) ?? 0) * (token0PriceUSD ?? 0) +
+ Number(formatAmount(staked1Amount) ?? 0) * (token1PriceUSD ?? 0)
+ )
+ }, [staked0Amount, staked1Amount, token0PriceUSD, token1PriceUSD])
+ return totalAssetsInUsd
+}
diff --git a/apps/web/src/views/PositionManagers/hooks/useTotalStakedInUsd.ts b/apps/web/src/views/PositionManagers/hooks/useTotalStakedInUsd.ts
new file mode 100644
index 0000000000000..036356fe7d37b
--- /dev/null
+++ b/apps/web/src/views/PositionManagers/hooks/useTotalStakedInUsd.ts
@@ -0,0 +1,32 @@
+import { useMemo } from 'react'
+import BigNumber from 'bignumber.js'
+import { Currency, CurrencyAmount } from '@pancakeswap/sdk'
+
+interface TotalStakedInUsdProps {
+ currencyA: Currency
+ currencyB: Currency
+ poolToken0Amount?: bigint
+ poolToken1Amount?: bigint
+ token0PriceUSD?: number
+ token1PriceUSD?: number
+}
+
+export const useTotalStakedInUsd = ({
+ currencyA,
+ currencyB,
+ poolToken0Amount,
+ poolToken1Amount,
+ token0PriceUSD,
+ token1PriceUSD,
+}: TotalStakedInUsdProps): number => {
+ const pool0Amount = poolToken0Amount ? CurrencyAmount.fromRawAmount(currencyA, poolToken0Amount) : undefined
+ const pool1Amount = poolToken1Amount ? CurrencyAmount.fromRawAmount(currencyB, poolToken1Amount) : undefined
+
+ const totalStakedInUsd = useMemo(() => {
+ const totalPoolToken0Usd = new BigNumber(pool0Amount?.toSignificant() ?? 0).times(token0PriceUSD ?? 0)
+ const totalPoolToken1Usd = new BigNumber(pool1Amount?.toSignificant() ?? 0).times(token1PriceUSD ?? 0)
+ return totalPoolToken0Usd.plus(totalPoolToken1Usd).toNumber()
+ }, [pool0Amount, pool1Amount, token0PriceUSD, token1PriceUSD])
+
+ return totalStakedInUsd ?? 0
+}
diff --git a/apps/web/src/views/PositionManagers/hooks/useVault.ts b/apps/web/src/views/PositionManagers/hooks/useVault.ts
new file mode 100644
index 0000000000000..e4059c8244cd5
--- /dev/null
+++ b/apps/web/src/views/PositionManagers/hooks/useVault.ts
@@ -0,0 +1,29 @@
+import { PCSDuoTokenVault, PCSDuoTokenVaultConfig, createPCSVaultManager } from '@pancakeswap/position-managers'
+import { useMemo } from 'react'
+
+interface Params {
+ config: PCSDuoTokenVaultConfig
+}
+
+interface VaultDetail {
+ vault: PCSDuoTokenVault
+ loading: boolean
+ error?: Error | null
+}
+
+export function usePCSVault({ config }: Params): VaultDetail {
+ const manager = useMemo(() => createPCSVaultManager({ vaultConfig: config }), [config])
+
+ const vault = useMemo(() => {
+ return {
+ ...config,
+ manager,
+ }
+ }, [config, manager])
+
+ return {
+ vault,
+ loading: false,
+ error: null,
+ }
+}
diff --git a/apps/web/src/views/PositionManagers/hooks/useVaultConfigs.ts b/apps/web/src/views/PositionManagers/hooks/useVaultConfigs.ts
new file mode 100644
index 0000000000000..c4672afd06b27
--- /dev/null
+++ b/apps/web/src/views/PositionManagers/hooks/useVaultConfigs.ts
@@ -0,0 +1,10 @@
+import { VAULTS_CONFIG_BY_CHAIN, VaultConfig } from '@pancakeswap/position-managers'
+import { useMemo } from 'react'
+
+import { useActiveChainId } from 'hooks/useActiveChainId'
+
+export function useVaultConfigs(): VaultConfig[] {
+ const { chainId } = useActiveChainId()
+
+ return useMemo(() => (chainId && VAULTS_CONFIG_BY_CHAIN[chainId]) || [], [chainId])
+}
diff --git a/apps/web/src/views/PositionManagers/hooks/useViewMode.ts b/apps/web/src/views/PositionManagers/hooks/useViewMode.ts
new file mode 100644
index 0000000000000..4f9ee284072d8
--- /dev/null
+++ b/apps/web/src/views/PositionManagers/hooks/useViewMode.ts
@@ -0,0 +1,32 @@
+import { ViewMode } from '@pancakeswap/uikit'
+import { useRouter } from 'next/router'
+import { useCallback, useMemo } from 'react'
+import { updateQuery } from '@pancakeswap/utils/clientRouter'
+
+export function useViewMode() {
+ const router = useRouter()
+ const mode = useMemo(
+ () => (router.query.view === String(ViewMode.CARD).toLocaleLowerCase() ? ViewMode.CARD : ViewMode.TABLE),
+ [router.query],
+ )
+ const setViewMode = useCallback(
+ (viewMode: ViewMode) => {
+ if (mode === viewMode) {
+ return
+ }
+ router.push(
+ updateQuery(router.asPath, {
+ view: viewMode.toLocaleLowerCase(),
+ }),
+ '',
+ { scroll: false },
+ )
+ },
+ [router, mode],
+ )
+
+ return {
+ mode,
+ setViewMode,
+ }
+}
diff --git a/apps/web/src/views/PositionManagers/index.tsx b/apps/web/src/views/PositionManagers/index.tsx
new file mode 100644
index 0000000000000..80615076c8094
--- /dev/null
+++ b/apps/web/src/views/PositionManagers/index.tsx
@@ -0,0 +1,16 @@
+import Page from 'components/Layout/Page'
+
+import { Header } from './components'
+import { VaultCards, Controls } from './containers'
+
+export function PositionManagers() {
+ return (
+ <>
+
+
+
+
+
+ >
+ )
+}
diff --git a/apps/web/src/views/PositionManagers/utils/getReadableManagerFeeType.ts b/apps/web/src/views/PositionManagers/utils/getReadableManagerFeeType.ts
new file mode 100644
index 0000000000000..51881dcf9a437
--- /dev/null
+++ b/apps/web/src/views/PositionManagers/utils/getReadableManagerFeeType.ts
@@ -0,0 +1,11 @@
+import { TranslateFunction } from '@pancakeswap/localization'
+import { ManagerFeeType } from '@pancakeswap/position-managers'
+
+export function getReadableManagerFeeType(t: TranslateFunction, feeType: ManagerFeeType) {
+ switch (feeType) {
+ case ManagerFeeType.LP_REWARDS:
+ return t('% of LP rewards')
+ default:
+ return ''
+ }
+}
diff --git a/apps/web/src/views/PositionManagers/utils/getVaultName.ts b/apps/web/src/views/PositionManagers/utils/getVaultName.ts
new file mode 100644
index 0000000000000..be53ebeb02ce7
--- /dev/null
+++ b/apps/web/src/views/PositionManagers/utils/getVaultName.ts
@@ -0,0 +1,3 @@
+export function getVaultName(id: string | number, managerName: string) {
+ return `(${managerName}#${id})`
+}
diff --git a/apps/web/src/views/PositionManagers/utils/index.ts b/apps/web/src/views/PositionManagers/utils/index.ts
new file mode 100644
index 0000000000000..182aa9281d1cc
--- /dev/null
+++ b/apps/web/src/views/PositionManagers/utils/index.ts
@@ -0,0 +1,3 @@
+export * from './strategy'
+export * from './getVaultName'
+export * from './getReadableManagerFeeType'
diff --git a/apps/web/src/views/PositionManagers/utils/strategy.ts b/apps/web/src/views/PositionManagers/utils/strategy.ts
new file mode 100644
index 0000000000000..dedb395459fe6
--- /dev/null
+++ b/apps/web/src/views/PositionManagers/utils/strategy.ts
@@ -0,0 +1,13 @@
+import { TranslateFunction } from '@pancakeswap/localization'
+import { Strategy } from '@pancakeswap/position-managers'
+
+export function getStrategyName(t: TranslateFunction, strategy: Strategy) {
+ switch (strategy) {
+ case Strategy.TYPICAL_WIDE:
+ return t('Typical Wide')
+ case Strategy.YIELD_IQ:
+ return t('Yield IQ')
+ default:
+ return ''
+ }
+}
diff --git a/package.json b/package.json
index 685adc7278c11..6e0b9ea317e74 100644
--- a/package.json
+++ b/package.json
@@ -11,7 +11,7 @@
"packageManager": "pnpm@8.6.0",
"scripts": {
"preinstall": "npx only-allow pnpm",
- "dev": "turbo run dev --filter=web... --concurrency=21",
+ "dev": "turbo run dev --filter=web... --concurrency=22",
"dev:aptos": "pnpm turbo run dev --filter=aptos-web... --concurrency=20",
"dev:blog": "pnpm turbo run dev --filter=blog...",
"dev:bridge": "pnpm turbo run dev --filter=bridge... --concurrency=16",
diff --git a/packages/hooks/src/useDebouncedChangeHandler.ts b/packages/hooks/src/useDebouncedChangeHandler.ts
index 411c5898518a4..ba61aecf919ef 100644
--- a/packages/hooks/src/useDebouncedChangeHandler.ts
+++ b/packages/hooks/src/useDebouncedChangeHandler.ts
@@ -8,7 +8,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'
*/
export default function useDebouncedChangeHandler(
value: T,
- onChange: (newValue: T) => void,
+ onChange: ((newValue: T) => void) | undefined,
debouncedMs = 100,
): [T, (value: T) => void] {
const [inner, setInner] = useState(() => value)
@@ -21,7 +21,7 @@ export default function useDebouncedChangeHandler(
clearTimeout(timer.current)
}
timer.current = setTimeout(() => {
- onChange(newValue)
+ onChange?.(newValue)
timer.current = undefined
}, debouncedMs)
},
diff --git a/packages/localization/src/config/translations.json b/packages/localization/src/config/translations.json
index 0d8c707ce276f..7d662978e005d 100644
--- a/packages/localization/src/config/translations.json
+++ b/packages/localization/src/config/translations.json
@@ -2788,12 +2788,48 @@
"Trade, earn, and own crypto on the all-in-one multichain DEX": "Trade, earn, and own crypto on the all-in-one multichain DEX",
"opBNB is LIVE!": "opBNB is LIVE!",
"PancakeSwap Now Live on opBNB!": "PancakeSwap Now Live on opBNB!",
- "Earn rewards hassle-free with single-sided staking": "Earn rewards hassle-free with single-sided staking",
"Unstaking from v2 farm?": "Unstaking from v2 farm?",
"Migrate to v3 here": "Migrate to v3 here",
"Migrate to V3": "Migrate to V3",
"Incentives have moved to": "Incentives have moved to",
"Merkl": "Merkl",
"and are now claimable without staking your LP token.": "and are now claimable without staking your LP token.",
- "Continue seeding your liquidity on PancakeSwap to accrue the rewards!": "Continue seeding your liquidity on PancakeSwap to accrue the rewards!"
+ "Continue seeding your liquidity on PancakeSwap to accrue the rewards!": "Continue seeding your liquidity on PancakeSwap to accrue the rewards!",
+ "Learn more about the strategy": "Learn more about the strategy",
+ "Earn rewards hassle-free with single-sided staking": "Earn rewards hassle-free with single-sided staking",
+ "You are providing liquidity via a 3rd party liquidity manager": "You are providing liquidity via a 3rd party liquidity manager",
+ "which is responsible for managing the underlying assets.": "which is responsible for managing the underlying assets.",
+ "Please always DYOR before depositing your assets.": "Please always DYOR before depositing your assets.",
+ "Single token deposits only. The final position may consist with both tokens. Learn more about the strategy": "Single token deposits only. The final position may consist with both tokens. Learn more about the strategy",
+ "Single Token": "Single Token",
+ "Your funds have been staked in position manager.": "Your funds have been staked in position manager.",
+ "Your funds have been unstaked in position manager.": "Your funds have been unstaked in position manager.",
+ "Automated your PancakeSwap V3 liquidity": "Automated your PancakeSwap V3 liquidity",
+ "Position Manager": "Position Manager",
+ "Adding": "Adding",
+ "Your share in the vault": "Your share in the vault",
+ "Search Managers": "Search Managers",
+ "Managed by": "Managed by",
+ "With": "With",
+ "strategy": "strategy",
+ "Removing": "Removing",
+ "Depositing Token": "Depositing Token",
+ "Pair Info": "Pair Info",
+ "Manager Info": "Manager Info",
+ "Strategy Info": "Strategy Info",
+ "View Manager Address": "View Manager Address",
+ "View Vault Contract": "View Vault Contract",
+ "View Vault on %managerName%": "View Vault on %managerName%",
+ "% of LP rewards": "% of LP rewards",
+ "Typical Wide": "Typical Wide",
+ "Yield IQ": "Yield IQ",
+ "Token amounts displayed above are estimations. The final amount of tokens received may vary.": "Token amounts displayed above are estimations. The final amount of tokens received may vary.",
+ "Pending Confirm": "Pending Confirm",
+ "CAKE APR": "CAKE APR",
+ "LP APR": "LP APR",
+ "Calculated based on previous 7 days average data.": "Calculated based on previous 7 days average data.",
+ "Farming Rewards": "Farming Rewards",
+ "second": "second",
+ "Manager Fee": "Manager Fee",
+ "Removing Liquidity": "Removing Liquidity"
}
diff --git a/packages/position-managers/index.ts b/packages/position-managers/index.ts
new file mode 100644
index 0000000000000..6f39cd49b29ea
--- /dev/null
+++ b/packages/position-managers/index.ts
@@ -0,0 +1 @@
+export * from './src'
diff --git a/packages/position-managers/package.json b/packages/position-managers/package.json
new file mode 100644
index 0000000000000..8cc7594dd73a8
--- /dev/null
+++ b/packages/position-managers/package.json
@@ -0,0 +1,28 @@
+{
+ "name": "@pancakeswap/position-managers",
+ "version": "0.0.1",
+ "sideEffects": false,
+ "private": true,
+ "scripts": {
+ "test": "vitest --run"
+ },
+ "type": "module",
+ "dependencies": {
+ "@pancakeswap/sdk": "workspace:*",
+ "@pancakeswap/v3-sdk": "workspace:*",
+ "@pancakeswap/token-lists": "workspace:*",
+ "@pancakeswap/tokens": "workspace:*",
+ "@pancakeswap/chains": "workspace:*",
+ "bignumber.js": "^9.0.0",
+ "lodash": "^4.17.21",
+ "viem": "^1.15.1",
+ "wagmi": "^1.4.3"
+ },
+ "devDependencies": {
+ "@types/lodash": "^4.14.168",
+ "@pancakeswap/tsconfig": "workspace:*",
+ "@pancakeswap/utils": "workspace:*",
+ "typescript": "^5.1.3",
+ "vitest": "^0.27.2"
+ }
+}
diff --git a/packages/position-managers/src/abi/index.ts b/packages/position-managers/src/abi/index.ts
new file mode 100644
index 0000000000000..9c61296abbda3
--- /dev/null
+++ b/packages/position-managers/src/abi/index.ts
@@ -0,0 +1,2 @@
+export * from './positionManagerAdapter'
+export * from './positionManagerWrapper'
diff --git a/packages/position-managers/src/abi/positionManagerAdapter.ts b/packages/position-managers/src/abi/positionManagerAdapter.ts
new file mode 100644
index 0000000000000..a316d7c629a1d
--- /dev/null
+++ b/packages/position-managers/src/abi/positionManagerAdapter.ts
@@ -0,0 +1,243 @@
+export const positionManagerAdapterABI = [
+ { inputs: [], stateMutability: 'nonpayable', type: 'constructor' },
+ {
+ anonymous: false,
+ inputs: [
+ { indexed: true, internalType: 'contract IERC20', name: '_token', type: 'address' },
+ { indexed: true, internalType: 'address', name: '_spender', type: 'address' },
+ { indexed: false, internalType: 'uint256', name: '_amount', type: 'uint256' },
+ ],
+ name: 'ApproveToken',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [{ indexed: false, internalType: 'uint8', name: 'version', type: 'uint8' }],
+ name: 'Initialized',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ { indexed: true, internalType: 'address', name: 'previousOwner', type: 'address' },
+ { indexed: true, internalType: 'address', name: 'newOwner', type: 'address' },
+ ],
+ name: 'OwnershipTransferred',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ { indexed: true, internalType: 'address', name: 'oldVault', type: 'address' },
+ { indexed: true, internalType: 'address', name: 'newVault', type: 'address' },
+ { indexed: false, internalType: 'address', name: 'token0', type: 'address' },
+ { indexed: false, internalType: 'address', name: 'token1', type: 'address' },
+ { indexed: false, internalType: 'address', name: 'lpToken', type: 'address' },
+ ],
+ name: 'SetVault',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ { indexed: true, internalType: 'address', name: 'oldWrapper', type: 'address' },
+ { indexed: true, internalType: 'address', name: 'newWrapper', type: 'address' },
+ ],
+ name: 'SetWrapper',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ { indexed: true, internalType: 'contract IERC20', name: '_token', type: 'address' },
+ { indexed: true, internalType: 'address', name: '_recipient', type: 'address' },
+ { indexed: false, internalType: 'uint256', name: '_amount', type: 'uint256' },
+ ],
+ name: 'TransferToken',
+ type: 'event',
+ },
+ { stateMutability: 'nonpayable', type: 'fallback' },
+ {
+ inputs: [],
+ name: 'PRECISION',
+ outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'PROTOCOL',
+ outputs: [{ internalType: 'string', name: '', type: 'string' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'VERSION',
+ outputs: [{ internalType: 'uint8', name: '', type: 'uint8' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [
+ { internalType: 'contract IERC20', name: '_token', type: 'address' },
+ { internalType: 'address', name: '_spender', type: 'address' },
+ { internalType: 'uint256', name: '_amount', type: 'uint256' },
+ ],
+ name: 'approveToken',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [
+ { internalType: 'uint256', name: '_amount0', type: 'uint256' },
+ { internalType: 'uint256', name: '_amount1', type: 'uint256' },
+ { internalType: 'address', name: '_user', type: 'address' },
+ { internalType: 'bytes', name: '', type: 'bytes' },
+ ],
+ name: 'deposit',
+ outputs: [{ internalType: 'uint256', name: '_share', type: 'uint256' }],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [
+ { internalType: 'address', name: '_wrapper', type: 'address' },
+ { internalType: 'address', name: '_vault', type: 'address' },
+ { internalType: 'address', name: '_admin', type: 'address' },
+ ],
+ name: 'initialize',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'lpToken',
+ outputs: [{ internalType: 'address', name: '', type: 'address' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'manager',
+ outputs: [{ internalType: 'address', name: '', type: 'address' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'managerFee',
+ outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'owner',
+ outputs: [{ internalType: 'address', name: '', type: 'address' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'pool',
+ outputs: [{ internalType: 'address', name: '', type: 'address' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ { inputs: [], name: 'renounceOwnership', outputs: [], stateMutability: 'nonpayable', type: 'function' },
+ {
+ inputs: [{ internalType: 'address', name: '_newVault', type: 'address' }],
+ name: 'setVault',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'address', name: '_newWrapper', type: 'address' }],
+ name: 'setWrapper',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'token0',
+ outputs: [{ internalType: 'address', name: '', type: 'address' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'token1',
+ outputs: [{ internalType: 'address', name: '', type: 'address' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'tokenPerShare',
+ outputs: [
+ { internalType: 'uint256', name: '_token0PerShare', type: 'uint256' },
+ { internalType: 'uint256', name: '_token1PerShare', type: 'uint256' },
+ ],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'totalSupply',
+ outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'address', name: 'newOwner', type: 'address' }],
+ name: 'transferOwnership',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [
+ { internalType: 'contract IERC20', name: '_token', type: 'address' },
+ { internalType: 'address', name: '_recipient', type: 'address' },
+ { internalType: 'uint256', name: '_amount', type: 'uint256' },
+ ],
+ name: 'transferToken',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'vault',
+ outputs: [{ internalType: 'address', name: '', type: 'address' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [
+ { internalType: 'uint256', name: '_share', type: 'uint256' },
+ { internalType: 'address', name: '_user', type: 'address' },
+ { internalType: 'bytes', name: '', type: 'bytes' },
+ ],
+ name: 'withdraw',
+ outputs: [
+ { internalType: 'uint256', name: '_amount0', type: 'uint256' },
+ { internalType: 'uint256', name: '_amount1', type: 'uint256' },
+ ],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'wrapper',
+ outputs: [{ internalType: 'address', name: '', type: 'address' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ { stateMutability: 'payable', type: 'receive' },
+] as const
diff --git a/packages/position-managers/src/abi/positionManagerWrapper.ts b/packages/position-managers/src/abi/positionManagerWrapper.ts
new file mode 100644
index 0000000000000..0cbce06fabd20
--- /dev/null
+++ b/packages/position-managers/src/abi/positionManagerWrapper.ts
@@ -0,0 +1,350 @@
+export const positionManagerWrapperABI = [
+ { inputs: [], stateMutability: 'nonpayable', type: 'constructor' },
+ {
+ anonymous: false,
+ inputs: [
+ { indexed: true, internalType: 'address', name: 'user', type: 'address' },
+ { indexed: true, internalType: 'address', name: 'newAdapterAddr', type: 'address' },
+ ],
+ name: 'AdapterUpdated',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ { indexed: true, internalType: 'address', name: 'user', type: 'address' },
+ { indexed: false, internalType: 'uint256', name: 'amount', type: 'uint256' },
+ ],
+ name: 'Deposit',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ { indexed: true, internalType: 'address', name: 'user', type: 'address' },
+ { indexed: false, internalType: 'uint256', name: 'amount', type: 'uint256' },
+ { indexed: false, internalType: 'uint256', name: 'endTimestamp', type: 'uint256' },
+ ],
+ name: 'DepositAndExpend',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ { indexed: true, internalType: 'address', name: 'user', type: 'address' },
+ { indexed: false, internalType: 'uint256', name: 'amount', type: 'uint256' },
+ ],
+ name: 'EmergencyWithdraw',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ { indexed: true, internalType: 'address', name: 'user', type: 'address' },
+ { indexed: false, internalType: 'uint256', name: 'amount', type: 'uint256' },
+ { indexed: false, internalType: 'uint256', name: 'amount0', type: 'uint256' },
+ { indexed: false, internalType: 'uint256', name: 'amount1', type: 'uint256' },
+ ],
+ name: 'MintThenDeposit',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [{ indexed: false, internalType: 'uint256', name: 'poolLimitPerUser', type: 'uint256' }],
+ name: 'NewPoolLimit',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ { indexed: false, internalType: 'uint256', name: 'oldRewardPerSecond', type: 'uint256' },
+ { indexed: false, internalType: 'uint256', name: 'newRewardPerSecond', type: 'uint256' },
+ { indexed: false, internalType: 'uint256', name: 'startTimestamp', type: 'uint256' },
+ { indexed: false, internalType: 'uint256', name: 'endTimestamp', type: 'uint256' },
+ ],
+ name: 'NewRewardPerSecond',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ { indexed: false, internalType: 'uint256', name: 'oldStartTimestamp', type: 'uint256' },
+ { indexed: false, internalType: 'uint256', name: 'newStartTimestamp', type: 'uint256' },
+ { indexed: false, internalType: 'uint256', name: 'oldEndTimestamp', type: 'uint256' },
+ { indexed: false, internalType: 'uint256', name: 'newEndTimestamp', type: 'uint256' },
+ { indexed: false, internalType: 'uint256', name: 'rewardPerSecond', type: 'uint256' },
+ ],
+ name: 'NewStartAndEndTimestamp',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ { indexed: true, internalType: 'address', name: 'previousOwner', type: 'address' },
+ { indexed: true, internalType: 'address', name: 'newOwner', type: 'address' },
+ ],
+ name: 'OwnershipTransferred',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ { indexed: false, internalType: 'uint256', name: 'startTimestamp', type: 'uint256' },
+ { indexed: false, internalType: 'uint256', name: 'endTimestamp', type: 'uint256' },
+ { indexed: false, internalType: 'uint256', name: 'rewardPerSecond', type: 'uint256' },
+ ],
+ name: 'Restart',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [{ indexed: false, internalType: 'uint256', name: 'blockNumber', type: 'uint256' }],
+ name: 'RewardsStop',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ { indexed: true, internalType: 'address', name: 'token', type: 'address' },
+ { indexed: false, internalType: 'uint256', name: 'amount', type: 'uint256' },
+ ],
+ name: 'TokenRecovery',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ { indexed: true, internalType: 'address', name: 'user', type: 'address' },
+ { indexed: false, internalType: 'uint256', name: 'amount', type: 'uint256' },
+ ],
+ name: 'Withdraw',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ { indexed: true, internalType: 'address', name: 'user', type: 'address' },
+ { indexed: false, internalType: 'uint256', name: 'amount', type: 'uint256' },
+ { indexed: false, internalType: 'uint256', name: 'amount0', type: 'uint256' },
+ { indexed: false, internalType: 'uint256', name: 'amount1', type: 'uint256' },
+ ],
+ name: 'WithdrawThenBurn',
+ type: 'event',
+ },
+ {
+ inputs: [],
+ name: 'PRECISION_FACTOR',
+ outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'WRAPPER_FACTORY',
+ outputs: [{ internalType: 'address', name: '', type: 'address' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'accTokenPerShare',
+ outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'adapterAddr',
+ outputs: [{ internalType: 'address payable', name: '', type: 'address' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'uint256', name: '_amount', type: 'uint256' }],
+ name: 'deposit',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'uint256', name: '_amount', type: 'uint256' }],
+ name: 'depositRewardAndExpend',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'uint256', name: '_amount', type: 'uint256' }],
+ name: 'emergencyRewardWithdraw',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ { inputs: [], name: 'emergencyWithdraw', outputs: [], stateMutability: 'nonpayable', type: 'function' },
+ {
+ inputs: [],
+ name: 'endTimestamp',
+ outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [
+ { internalType: 'contract IERC20Metadata', name: '_stakedToken', type: 'address' },
+ { internalType: 'contract IERC20Metadata', name: '_rewardToken', type: 'address' },
+ { internalType: 'uint256', name: '_rewardPerSecond', type: 'uint256' },
+ { internalType: 'uint256', name: '_startTimestamp', type: 'uint256' },
+ { internalType: 'uint256', name: '_endTimestamp', type: 'uint256' },
+ { internalType: 'address', name: '_admin', type: 'address' },
+ ],
+ name: 'initialize',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'isInitialized',
+ outputs: [{ internalType: 'bool', name: '', type: 'bool' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'lastRewardTimestamp',
+ outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [
+ { internalType: 'uint256', name: '_amount0', type: 'uint256' },
+ { internalType: 'uint256', name: '_amount1', type: 'uint256' },
+ { internalType: 'bytes', name: '_data', type: 'bytes' },
+ ],
+ name: 'mintThenDeposit',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'owner',
+ outputs: [{ internalType: 'address', name: '', type: 'address' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'address', name: '_user', type: 'address' }],
+ name: 'pendingReward',
+ outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'address', name: '_token', type: 'address' }],
+ name: 'recoverToken',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ { inputs: [], name: 'renounceOwnership', outputs: [], stateMutability: 'nonpayable', type: 'function' },
+ {
+ inputs: [
+ { internalType: 'uint256', name: '_startTimestamp', type: 'uint256' },
+ { internalType: 'uint256', name: '_endTimestamp', type: 'uint256' },
+ { internalType: 'uint256', name: '_rewardPerSecond', type: 'uint256' },
+ ],
+ name: 'restart',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'rewardPerSecond',
+ outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'rewardToken',
+ outputs: [{ internalType: 'contract IERC20Metadata', name: '', type: 'address' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'stakedToken',
+ outputs: [{ internalType: 'contract IERC20Metadata', name: '', type: 'address' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'startTimestamp',
+ outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ { inputs: [], name: 'stopReward', outputs: [], stateMutability: 'nonpayable', type: 'function' },
+ {
+ inputs: [{ internalType: 'address', name: 'newOwner', type: 'address' }],
+ name: 'transferOwnership',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'address', name: '_adapterAddr', type: 'address' }],
+ name: 'updateAdapterAddress',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'uint256', name: '_rewardPerSecond', type: 'uint256' }],
+ name: 'updateRewardPerSecond',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [
+ { internalType: 'uint256', name: '_startTimestamp', type: 'uint256' },
+ { internalType: 'uint256', name: '_endTimestamp', type: 'uint256' },
+ ],
+ name: 'updateStartAndEndTimestamp',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'address', name: '', type: 'address' }],
+ name: 'userInfo',
+ outputs: [
+ { internalType: 'uint256', name: 'amount', type: 'uint256' },
+ { internalType: 'uint256', name: 'rewardDebt', type: 'uint256' },
+ ],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'uint256', name: '_amount', type: 'uint256' }],
+ name: 'withdraw',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [
+ { internalType: 'uint256', name: '_amount', type: 'uint256' },
+ { internalType: 'bytes', name: '_data', type: 'bytes' },
+ ],
+ name: 'withdrawThenBurn',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+] as const
diff --git a/packages/position-managers/src/constants/endpoints.ts b/packages/position-managers/src/constants/endpoints.ts
new file mode 100644
index 0000000000000..3e045e4852e46
--- /dev/null
+++ b/packages/position-managers/src/constants/endpoints.ts
@@ -0,0 +1 @@
+export const POSITION_MANAGER_API = 'https://vault-api.pancake.run/api/v1'
diff --git a/packages/position-managers/src/constants/index.ts b/packages/position-managers/src/constants/index.ts
new file mode 100644
index 0000000000000..827d4be824563
--- /dev/null
+++ b/packages/position-managers/src/constants/index.ts
@@ -0,0 +1,4 @@
+export * from './supportedChains'
+export * from './vaults'
+export * from './managers'
+export * from './endpoints'
diff --git a/packages/position-managers/src/constants/managers.ts b/packages/position-managers/src/constants/managers.ts
new file mode 100644
index 0000000000000..c5dd65a548833
--- /dev/null
+++ b/packages/position-managers/src/constants/managers.ts
@@ -0,0 +1,24 @@
+export enum MANAGER {
+ PCS = 'pcs-position-manager',
+ BRIL = 'bril-position-manager',
+}
+
+export interface BaseManager {
+ id: MANAGER
+ name: string
+ introLink?: string
+}
+
+export const baseManagers: { [manager in MANAGER]: BaseManager } = {
+ [MANAGER.PCS]: {
+ id: MANAGER.PCS,
+ name: 'PCS',
+ },
+ [MANAGER.BRIL]: {
+ id: MANAGER.BRIL,
+ name: 'Bril Finance',
+ introLink: 'https://www.bril.finance/',
+ },
+}
+
+export const VERIFIED_MANAGERS = [MANAGER.PCS]
diff --git a/packages/position-managers/src/constants/supportedChains.ts b/packages/position-managers/src/constants/supportedChains.ts
new file mode 100644
index 0000000000000..5adf07b54b9c4
--- /dev/null
+++ b/packages/position-managers/src/constants/supportedChains.ts
@@ -0,0 +1,5 @@
+import { ChainId } from '@pancakeswap/chains'
+
+export const SUPPORTED_CHAIN_IDS = [ChainId.BSC, ChainId.BSC_TESTNET] as const
+
+export type SupportedChainId = (typeof SUPPORTED_CHAIN_IDS)[number]
diff --git a/packages/position-managers/src/constants/vaults/1.ts b/packages/position-managers/src/constants/vaults/1.ts
new file mode 100644
index 0000000000000..d15d488ea6f24
--- /dev/null
+++ b/packages/position-managers/src/constants/vaults/1.ts
@@ -0,0 +1,3 @@
+import { VaultConfig } from '../../types'
+
+export const vaults: VaultConfig[] = []
diff --git a/packages/position-managers/src/constants/vaults/56.ts b/packages/position-managers/src/constants/vaults/56.ts
new file mode 100644
index 0000000000000..d4ad8ca80d1e7
--- /dev/null
+++ b/packages/position-managers/src/constants/vaults/56.ts
@@ -0,0 +1,120 @@
+import { bscTokens } from '@pancakeswap/tokens'
+import { FeeAmount } from '@pancakeswap/v3-sdk'
+
+import { Strategy, VaultConfig } from '../../types'
+import { MANAGER } from '../managers'
+
+export const vaults: VaultConfig[] = [
+ {
+ id: 1,
+ name: 'BRIL',
+ address: '0xF8C4d24Af47cBD87E3C8Cc368fcd7e3cd2a13083',
+ adapterAddress: '0x6F34909c663e6E6dA32b73f0aa5aD7bdABf21a63',
+ currencyA: bscTokens.cake,
+ currencyB: bscTokens.usdt,
+ earningToken: bscTokens.cake,
+ feeTier: FeeAmount.MEDIUM,
+ strategy: Strategy.YIELD_IQ,
+ manager: MANAGER.BRIL,
+ isSingleDepositToken: true,
+ allowDepositToken0: false,
+ allowDepositToken1: true,
+ priceFromV3FarmPid: 3,
+ managerInfoUrl: 'https://www.bril.finance/',
+ strategyInfoUrl: 'https://docs.bril.finance/yield-iq/overview',
+ learnMoreAboutUrl: 'https://docs.bril.finance/bril-finance/introduction',
+ },
+ {
+ id: 2,
+ name: 'BRIL',
+ address: '0x799Ea58D15429fa7C42cc211e2181FD4EF54ec37',
+ adapterAddress: '0x443454bd4916E84EB3de7b50F4D7f6B384E72457',
+ currencyA: bscTokens.usdt,
+ currencyB: bscTokens.wbnb,
+ earningToken: bscTokens.cake,
+ feeTier: FeeAmount.LOW,
+ strategy: Strategy.YIELD_IQ,
+ manager: MANAGER.BRIL,
+ isSingleDepositToken: true,
+ allowDepositToken0: true,
+ allowDepositToken1: false,
+ priceFromV3FarmPid: 5,
+ managerInfoUrl: 'https://www.bril.finance/',
+ strategyInfoUrl: 'https://docs.bril.finance/yield-iq/overview',
+ learnMoreAboutUrl: 'https://docs.bril.finance/bril-finance/introduction',
+ },
+ {
+ id: 3,
+ name: 'BRIL',
+ address: '0xCd03B3757BC956e312F639dA1661d18DB7e72ED7',
+ adapterAddress: '0x2cFE4c59286D06630eA9f6Da8b2887BaC1AD9c4C',
+ currencyA: bscTokens.cake,
+ currencyB: bscTokens.wbnb,
+ earningToken: bscTokens.cake,
+ feeTier: FeeAmount.MEDIUM,
+ strategy: Strategy.YIELD_IQ,
+ manager: MANAGER.BRIL,
+ isSingleDepositToken: true,
+ allowDepositToken0: false,
+ allowDepositToken1: true,
+ priceFromV3FarmPid: 1,
+ managerInfoUrl: 'https://www.bril.finance/',
+ strategyInfoUrl: 'https://docs.bril.finance/yield-iq/overview',
+ learnMoreAboutUrl: 'https://docs.bril.finance/bril-finance/introduction',
+ },
+ {
+ id: 4,
+ name: 'BRIL',
+ address: '0x2044bCaaDa8370b4ee8Ad47DaAD290B80878D068',
+ adapterAddress: '0x259C5a1f3482C3988c546745A876E3f1017533df',
+ currencyA: bscTokens.usdt,
+ currencyB: bscTokens.btcb,
+ earningToken: bscTokens.cake,
+ feeTier: FeeAmount.LOW,
+ strategy: Strategy.YIELD_IQ,
+ manager: MANAGER.BRIL,
+ isSingleDepositToken: true,
+ allowDepositToken0: false,
+ allowDepositToken1: true,
+ priceFromV3FarmPid: 7,
+ managerInfoUrl: 'https://www.bril.finance/',
+ strategyInfoUrl: 'https://docs.bril.finance/yield-iq/overview',
+ learnMoreAboutUrl: 'https://docs.bril.finance/bril-finance/introduction',
+ },
+ {
+ id: 5,
+ name: 'BRIL',
+ address: '0x819c1C2FeF70Eb45919Ce7c7936cC0da95E30A33',
+ adapterAddress: '0x0CD23a6DcDF86535dF5b160E0adc0C7C46f80BaC',
+ currencyA: bscTokens.cake,
+ currencyB: bscTokens.btcb,
+ earningToken: bscTokens.cake,
+ feeTier: FeeAmount.LOW,
+ strategy: Strategy.YIELD_IQ,
+ manager: MANAGER.BRIL,
+ isSingleDepositToken: true,
+ allowDepositToken0: false,
+ allowDepositToken1: true,
+ managerInfoUrl: 'https://www.bril.finance/',
+ strategyInfoUrl: 'https://docs.bril.finance/yield-iq/overview',
+ learnMoreAboutUrl: 'https://docs.bril.finance/bril-finance/introduction',
+ },
+ {
+ id: 6,
+ name: 'BRIL',
+ address: '0x7216B5ae51a459Add75Dc3d0f1B030996da82aE0',
+ adapterAddress: '0xdE4810cF706F2df6a4Ab063D7008a575Fb2B6c4C',
+ currencyA: bscTokens.cake,
+ currencyB: bscTokens.eth,
+ earningToken: bscTokens.cake,
+ feeTier: FeeAmount.LOW,
+ strategy: Strategy.YIELD_IQ,
+ manager: MANAGER.BRIL,
+ isSingleDepositToken: true,
+ allowDepositToken0: false,
+ allowDepositToken1: true,
+ managerInfoUrl: 'https://www.bril.finance/',
+ strategyInfoUrl: 'https://docs.bril.finance/yield-iq/overview',
+ learnMoreAboutUrl: 'https://docs.bril.finance/bril-finance/introduction',
+ },
+]
diff --git a/packages/position-managers/src/constants/vaults/index.ts b/packages/position-managers/src/constants/vaults/index.ts
new file mode 100644
index 0000000000000..f202ad48335c1
--- /dev/null
+++ b/packages/position-managers/src/constants/vaults/index.ts
@@ -0,0 +1,20 @@
+import { ChainId } from '@pancakeswap/chains'
+
+import { PCSDuoTokenVaultConfig, VaultConfig } from '../../types'
+import { SupportedChainId } from '../supportedChains'
+import { vaults as ethVaults } from './1'
+import { vaults as bscVaults } from './56'
+import { MANAGER } from '../managers'
+
+export type VaultsConfigByChain = {
+ [chainId in SupportedChainId]: VaultConfig[]
+}
+
+export const VAULTS_CONFIG_BY_CHAIN = {
+ [ChainId.ETHEREUM]: ethVaults,
+ [ChainId.BSC]: bscVaults,
+}
+
+export function isPCSVaultConfig(config: VaultConfig): config is PCSDuoTokenVaultConfig {
+ return config.manager === MANAGER.PCS || config.manager === MANAGER.BRIL
+}
diff --git a/packages/position-managers/src/index.ts b/packages/position-managers/src/index.ts
new file mode 100644
index 0000000000000..70313691840bf
--- /dev/null
+++ b/packages/position-managers/src/index.ts
@@ -0,0 +1,4 @@
+export * from './constants'
+export * from './types'
+export * from './managers'
+export * from './abi'
diff --git a/packages/position-managers/src/managers/index.ts b/packages/position-managers/src/managers/index.ts
new file mode 100644
index 0000000000000..db15d97a9125d
--- /dev/null
+++ b/packages/position-managers/src/managers/index.ts
@@ -0,0 +1,7 @@
+import { MANAGER, VERIFIED_MANAGERS } from '../constants/managers'
+
+export function isManagerVerified(manager: MANAGER) {
+ return VERIFIED_MANAGERS.includes(manager)
+}
+
+export * from './pcs'
diff --git a/packages/position-managers/src/managers/pcs.ts b/packages/position-managers/src/managers/pcs.ts
new file mode 100644
index 0000000000000..134109471b0f0
--- /dev/null
+++ b/packages/position-managers/src/managers/pcs.ts
@@ -0,0 +1,41 @@
+import { baseManagers } from '../constants/managers'
+import { OnChainActionResponse, PCSDuoTokenVaultConfig, PCSPositionManager } from '../types'
+
+interface Params {
+ vaultConfig: PCSDuoTokenVaultConfig
+}
+
+export function createPCSVaultManager({ vaultConfig }: Params): PCSPositionManager {
+ async function addLiquidity(): Promise {
+ return { txHash: '0x' }
+ }
+
+ async function removeLiquidity(): Promise {
+ return { txHash: '0x' }
+ }
+
+ return {
+ ...baseManagers[vaultConfig.manager],
+ addLiquidity,
+ removeLiquidity,
+
+ getTotalAssets: async () => {
+ return {
+ position: null,
+ amounts: [],
+ }
+ },
+
+ getAccountShare: async () => {
+ return null
+ },
+
+ getRebalanceHistory: async () => {
+ return []
+ },
+
+ getOnChainActionAgent: () => {
+ return '0x'
+ },
+ }
+}
diff --git a/packages/position-managers/src/types.ts b/packages/position-managers/src/types.ts
new file mode 100644
index 0000000000000..2b891b98af24c
--- /dev/null
+++ b/packages/position-managers/src/types.ts
@@ -0,0 +1,134 @@
+import { Currency, CurrencyAmount, Percent, Price } from '@pancakeswap/sdk'
+import { FeeAmount } from '@pancakeswap/v3-sdk'
+import { Address, Hash } from 'viem'
+
+import { MANAGER, BaseManager } from './constants/managers'
+
+export enum OnChainActionType {
+ ADD_LIQUIDITY,
+ REMOVE_LIQUIDITY,
+ HARVEST,
+}
+
+export enum Strategy {
+ TYPICAL_WIDE,
+ YIELD_IQ,
+}
+
+export interface OnChainActionResponse {
+ txHash: Hash
+}
+
+export enum ManagerFeeType {
+ LP_REWARDS,
+}
+
+export interface ManagerFee {
+ type: ManagerFeeType
+ rate: Percent
+}
+
+export interface Position {
+ positionId: string
+ liquidity: bigint
+ tickUpper: number
+ tickLower: number
+}
+
+export interface RebalanceHistory {
+ // Timestamp
+ at: number
+
+ price: Price
+
+ // For 3rd party vaults, they might hold 100% of the assets
+ // instead of providing liquidity
+ position: Position | null
+}
+
+export interface BaseAssets {
+ position: Position | null
+
+ // Remaining token amounts hold by the position manager
+ // Happens with 3rd party position managers when their strategy is
+ // partially staking assets while utilizing remaining assets via other channel
+ amounts?: CurrencyAmount[]
+}
+
+// For position manager instance, the required context should contain both
+// manager config and the user wallet client (signer)
+export interface BasePositionManager extends BaseManager {
+ addLiquidity: (params: { amounts: CurrencyAmount[]; slippage?: Percent }) => Promise
+ removeLiquidity: (params: {
+ // The total assets that user currently owns
+ assets: A
+ percentage: Percent
+ slippage?: Percent
+ }) => Promise
+
+ // Get the contract address of specific on chain fund action
+ // Mainly used for token approval
+ getOnChainActionAgent: (actionType: OnChainActionType) => Address
+
+ // Get the total assets currently being managed.
+ // Position will change from time to time
+ getTotalAssets: () => Promise
+
+ // Get the assets share of the current user account in this vault
+ getAccountShare: () => Promise
+
+ getRebalanceHistory: () => Promise
+}
+
+export interface FarmReward {
+ amount: CurrencyAmount
+}
+
+export interface PCSPositionManager extends BasePositionManager {
+ // Only available on vault with farm
+ harvest?: () => Promise
+
+ getFarmRewards?: () => Promise
+}
+
+export type PositionManager = PCSPositionManager
+
+// Duo token position
+export interface DuoTokenVault {
+ // The unique id of the vault
+ // It can be used to sort the managed positions on fe
+ id: number
+ name: string
+ adapterAddress: Address
+ currencyA: Currency
+ currencyB: Currency
+ earningToken: Currency
+ feeTier: FeeAmount
+ manager: PositionManager
+ strategy: Strategy
+ isSingleDepositToken: boolean
+ allowDepositToken0?: boolean
+ allowDepositToken1?: boolean
+ priceFromV3FarmPid?: number
+ managerInfoUrl: string
+ strategyInfoUrl: string
+ projectVaultUrl?: string
+ learnMoreUrl?: string
+ learnMoreAboutUrl?: string
+}
+
+export interface PCSDuoTokenVault extends DuoTokenVault {
+ address: Address
+ autoCompound?: boolean
+
+ // Auto farm with lp
+ autoFarm?: boolean
+}
+
+export type Vault = PCSDuoTokenVault
+
+export interface PCSDuoTokenVaultConfig extends Omit {
+ manager: MANAGER
+}
+
+export type VaultConfig = PCSDuoTokenVaultConfig
diff --git a/packages/position-managers/tsconfig.json b/packages/position-managers/tsconfig.json
new file mode 100644
index 0000000000000..e1fe2de72bf2b
--- /dev/null
+++ b/packages/position-managers/tsconfig.json
@@ -0,0 +1,12 @@
+{
+ "extends": "@pancakeswap/tsconfig/base",
+ "include": ["./src"],
+ "exclude": ["**/*.test.ts"],
+ "compilerOptions": {
+ "target": "es2020",
+ "esModuleInterop": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "skipLibCheck": true
+ }
+}
diff --git a/packages/position-managers/vitest.config.ts b/packages/position-managers/vitest.config.ts
new file mode 100644
index 0000000000000..5e53b1c5e6dfb
--- /dev/null
+++ b/packages/position-managers/vitest.config.ts
@@ -0,0 +1,9 @@
+import { defineConfig } from 'vitest/config'
+
+export default defineConfig({
+ test: {
+ environment: 'node',
+ globals: true,
+ passWithNoTests: true,
+ },
+})
diff --git a/packages/uikit/src/components/Button/ExpandableButton.tsx b/packages/uikit/src/components/Button/ExpandableButton.tsx
index 8a26f8d812a12..57ff90d23b73d 100644
--- a/packages/uikit/src/components/Button/ExpandableButton.tsx
+++ b/packages/uikit/src/components/Button/ExpandableButton.tsx
@@ -1,16 +1,23 @@
import React from "react";
+import { SpaceProps } from "styled-system";
+
import { ChevronDownIcon, ChevronUpIcon } from "../Svg";
import Button from "./Button";
import IconButton from "./IconButton";
-interface Props {
+interface Props extends SpaceProps {
onClick?: () => void;
expanded?: boolean;
}
-export const ExpandableButton: React.FC> = ({ onClick, expanded, children }) => {
+export const ExpandableButton: React.FC> = ({
+ onClick,
+ expanded,
+ children,
+ ...rest
+}) => {
return (
-
+
{children}
{expanded ? : }
@@ -20,7 +27,7 @@ ExpandableButton.defaultProps = {
expanded: false,
};
-export const ExpandableLabel: React.FC> = ({ onClick, expanded, children }) => {
+export const ExpandableLabel: React.FC> = ({ onClick, expanded, children, ...rest }) => {
return (
diff --git a/packages/utils/clientRouter.ts b/packages/utils/clientRouter.ts
new file mode 100644
index 0000000000000..9b20d4cd58596
--- /dev/null
+++ b/packages/utils/clientRouter.ts
@@ -0,0 +1,23 @@
+import parse from 'url-parse'
+
+export function updateQuery(url: string, query: object) {
+ const parsed = parse(url, true)
+ const queries = {
+ ...parsed.query,
+ ...query,
+ }
+ for (const key of Object.keys(queries)) {
+ if (queries[key] === undefined) {
+ delete queries[key]
+ }
+ }
+
+ parsed.set('query', queries)
+ return parsed.toString()
+}
+
+export function getPathWithQueryPreserved(currentUrl: string, newPath: string) {
+ const parsed = parse(currentUrl, true)
+ parsed.set('pathname', newPath)
+ return parsed.toString()
+}
diff --git a/packages/utils/formatTimestamp.ts b/packages/utils/formatTimestamp.ts
new file mode 100644
index 0000000000000..0d25960f988ef
--- /dev/null
+++ b/packages/utils/formatTimestamp.ts
@@ -0,0 +1,53 @@
+export enum Precision {
+ DATE,
+ MINUTE,
+}
+
+interface Options {
+ locale?: string
+ timeZone?: string
+
+ showTimeZone?: boolean
+ precision?: Precision
+}
+
+export function formatTimestamp(
+ timestamp: number,
+ {
+ locale,
+ timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone,
+ showTimeZone = false,
+ precision = Precision.MINUTE,
+ }: Options = {},
+) {
+ const now = new Date(timestamp)
+ const dateFormat: Intl.DateTimeFormatOptions = {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ }
+ const minuteFormat: Intl.DateTimeFormatOptions = {
+ hour: 'numeric',
+ minute: '2-digit',
+ }
+ const format =
+ precision === Precision.MINUTE
+ ? {
+ ...dateFormat,
+ ...minuteFormat,
+ }
+ : dateFormat
+ const formatted = now.toLocaleString(locale, {
+ ...format,
+ timeZone,
+ })
+
+ if (!showTimeZone) {
+ return formatted
+ }
+ return `${formatted} (${timeZone})`
+}
+
+export function formatUnixTimestamp(timestamp: number, options: Options) {
+ return formatTimestamp(timestamp * 1000, options)
+}
diff --git a/packages/utils/getTimePeriods.ts b/packages/utils/getTimePeriods.ts
index 4ec334080d109..bb14efae13d23 100644
--- a/packages/utils/getTimePeriods.ts
+++ b/packages/utils/getTimePeriods.ts
@@ -2,7 +2,7 @@ const MINUTE_IN_SECONDS = 60
const HOUR_IN_SECONDS = 3600
export const DAY_IN_SECONDS = 86400
const MONTH_IN_SECONDS = 2629800
-const YEAR_IN_SECONDS = 31557600
+export const YEAR_IN_SECONDS = 31557600
/**
* Format number of seconds into year, month, day, hour, minute, seconds
diff --git a/packages/utils/package.json b/packages/utils/package.json
index 7f00c3263b6ae..90e93022a35fb 100644
--- a/packages/utils/package.json
+++ b/packages/utils/package.json
@@ -24,5 +24,8 @@
"@pancakeswap/localization": {
"optional": true
}
+ },
+ "dependencies": {
+ "url-parse": "^1.5.10"
}
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 24dbd9758135a..35307228deed8 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -671,6 +671,9 @@ importers:
'@pancakeswap/pools':
specifier: workspace:*
version: link:../../packages/pools
+ '@pancakeswap/position-managers':
+ specifier: workspace:*
+ version: link:../../packages/position-managers
'@pancakeswap/sdk':
specifier: workspace:*
version: link:../../packages/swap-sdk
@@ -1346,6 +1349,52 @@ importers:
specifier: ^0.27.2
version: 0.27.2
+ packages/position-managers:
+ dependencies:
+ '@pancakeswap/chains':
+ specifier: workspace:*
+ version: link:../chains
+ '@pancakeswap/sdk':
+ specifier: workspace:*
+ version: link:../swap-sdk
+ '@pancakeswap/token-lists':
+ specifier: workspace:*
+ version: link:../token-lists
+ '@pancakeswap/tokens':
+ specifier: workspace:*
+ version: link:../tokens
+ '@pancakeswap/v3-sdk':
+ specifier: workspace:*
+ version: link:../v3-sdk
+ bignumber.js:
+ specifier: ^9.0.0
+ version: 9.1.1
+ lodash:
+ specifier: ^4.17.21
+ version: 4.17.21
+ viem:
+ specifier: ^1.15.1
+ version: 1.15.1(typescript@5.1.3)(zod@3.22.3)
+ wagmi:
+ specifier: ^1.4.3
+ version: 1.4.3(@types/react@18.2.21)(encoding@0.1.13)(react-dom@18.2.0)(react@18.2.0)(typescript@5.1.3)(viem@1.15.1)(zod@3.22.3)
+ devDependencies:
+ '@pancakeswap/tsconfig':
+ specifier: workspace:*
+ version: link:../tsconfig
+ '@pancakeswap/utils':
+ specifier: workspace:*
+ version: link:../utils
+ '@types/lodash':
+ specifier: ^4.14.168
+ version: 4.14.186
+ typescript:
+ specifier: ^5.1.3
+ version: 5.1.3
+ vitest:
+ specifier: ^0.27.2
+ version: 0.27.2
+
packages/smart-router:
dependencies:
'@pancakeswap/chains':
@@ -1845,6 +1894,9 @@ importers:
lodash:
specifier: ^4.17.21
version: 4.17.21
+ url-parse:
+ specifier: ^1.5.10
+ version: 1.5.10
devDependencies:
'@pancakeswap/swap-sdk-core':
specifier: workspace:*
@@ -5468,7 +5520,7 @@ packages:
dependencies:
'@babel/core': 7.21.4
'@babel/helper-plugin-utils': 7.22.5
- '@babel/helper-validator-option': 7.21.0
+ '@babel/helper-validator-option': 7.22.15
'@babel/plugin-syntax-jsx': 7.21.4(@babel/core@7.21.4)
'@babel/plugin-transform-modules-commonjs': 7.21.5(@babel/core@7.21.4)
'@babel/plugin-transform-typescript': 7.21.3(@babel/core@7.21.4)
@@ -12293,7 +12345,7 @@ packages:
'@vitest/utils': 0.30.1
concordance: 5.0.4
p-limit: 4.0.0
- pathe: 1.1.0
+ pathe: 1.1.1
dev: true
/@vitest/runner@0.34.4:
@@ -12308,7 +12360,7 @@ packages:
resolution: {integrity: sha512-fJZqKrE99zo27uoZA/azgWyWbFvM1rw2APS05yB0JaLwUIg9aUtvvnBf4q7JWhEcAHmSwbrxKFgyBUga6tq9Tw==}
dependencies:
magic-string: 0.30.0
- pathe: 1.1.0
+ pathe: 1.1.1
pretty-format: 27.5.1
dev: true
@@ -12323,7 +12375,7 @@ packages:
/@vitest/spy@0.30.1:
resolution: {integrity: sha512-YfJeIf37GvTZe04ZKxzJfnNNuNSmTEGnla2OdL60C8od16f3zOfv9q9K0nNii0NfjDJRt/CVN/POuY5/zTS+BA==}
dependencies:
- tinyspy: 2.1.0
+ tinyspy: 2.1.1
dev: true
/@vitest/spy@0.34.4:
@@ -13483,13 +13535,6 @@ packages:
dependencies:
acorn: 8.10.0
- /acorn-jsx@5.3.2(acorn@8.8.2):
- resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
- peerDependencies:
- acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
- dependencies:
- acorn: 8.8.2
-
/acorn-walk@7.2.0:
resolution: {integrity: sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==}
engines: {node: '>=0.4.0'}
@@ -13513,6 +13558,7 @@ packages:
resolution: {integrity: sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==}
engines: {node: '>=0.4.0'}
hasBin: true
+ dev: true
/address@1.2.2:
resolution: {integrity: sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==}
@@ -17421,8 +17467,8 @@ packages:
resolution: {integrity: sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dependencies:
- acorn: 8.8.2
- acorn-jsx: 5.3.2(acorn@8.8.2)
+ acorn: 8.10.0
+ acorn-jsx: 5.3.2(acorn@8.10.0)
eslint-visitor-keys: 3.4.3
/espree@9.6.1:
@@ -20931,12 +20977,13 @@ packages:
dependencies:
'@lit-labs/ssr-dom-shim': 1.1.0
'@lit/reactive-element': 1.6.1
- lit-html: 2.7.0
+ lit-html: 2.8.0
/lit-html@2.7.0:
resolution: {integrity: sha512-/zPOl8EfeB3HHpTzINSpnWgvgQ8N07g/j272EOAIyB0Ys2RzBqTVT23i+JZuUlNbB2WHHeSsTCFi92NtWrtpqQ==}
dependencies:
'@types/trusted-types': 2.0.3
+ dev: false
/lit-html@2.8.0:
resolution: {integrity: sha512-o9t+MQM3P4y7M7yNzqAyjp7z+mQGa4NS4CxiyLqFPyFWyc4O+nodLrkrxSaCTrla6M5YOLaT3RpbbqjszB5g3Q==}
@@ -21747,8 +21794,8 @@ packages:
dependencies:
acorn: 8.10.0
pathe: 1.1.1
- pkg-types: 1.0.2
- ufo: 1.1.1
+ pkg-types: 1.0.3
+ ufo: 1.3.0
/mlly@1.4.2:
resolution: {integrity: sha512-i/Ykufi2t1EZ6NaPLdfnZk2AX8cs0d+mTzVKuPfqPKPatxLApaBoxJQ9x1/uckXtrS/U5oisPMDkNs0yQTaBRg==}
@@ -22679,13 +22726,6 @@ packages:
find-up: 5.0.0
dev: true
- /pkg-types@1.0.2:
- resolution: {integrity: sha512-hM58GKXOcj8WTqUXnsQyJYXdeAPbythQgEF3nTcEo+nkD49chjQ9IKm/QJy9xf6JakXptz86h7ecP2024rrLaQ==}
- dependencies:
- jsonc-parser: 3.2.0
- mlly: 1.4.2
- pathe: 1.1.1
-
/pkg-types@1.0.3:
resolution: {integrity: sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==}
dependencies:
@@ -23099,6 +23139,7 @@ packages:
/qrcode@1.5.3:
resolution: {integrity: sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg==}
engines: {node: '>=10.13.0'}
+ hasBin: true
dependencies:
dijkstrajs: 1.0.2
encode-utf8: 1.0.3
@@ -23162,7 +23203,6 @@ packages:
/querystringify@2.2.0:
resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==}
- dev: true
/queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
@@ -23962,7 +24002,6 @@ packages:
/requires-port@1.0.0:
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
- dev: true
/reselect@4.1.7:
resolution: {integrity: sha512-Zu1xbUt3/OPwsXL46hvOOoQrap2azE7ZQbokq61BQfiXvhewsKDwhMeZjTX9sX0nvw1t/U5Audyn1I9P/m9z0A==}
@@ -25630,11 +25669,6 @@ packages:
engines: {node: '>=14.0.0'}
dev: true
- /tinyspy@2.1.0:
- resolution: {integrity: sha512-7eORpyqImoOvkQJCSkL0d0mB4NHHIFAy4b1u8PHdDa7SjGS2njzl6/lyGoZLm+eyYEtlUmFGE0rFj66SWxZgQQ==}
- engines: {node: '>=14.0.0'}
- dev: true
-
/tinyspy@2.1.1:
resolution: {integrity: sha512-XPJL2uSzcOyBMky6OFrusqWlzfFrXtE0hPuMgW8A2HmaqrPo4ZQHRN/V0QXN3FSjKxpsbRrFc5LI7KOwBsT1/w==}
engines: {node: '>=14.0.0'}
@@ -26133,9 +26167,6 @@ packages:
/ua-parser-js@0.7.31:
resolution: {integrity: sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ==}
- /ufo@1.1.1:
- resolution: {integrity: sha512-MvlCc4GHrmZdAllBc0iUDowff36Q9Ndw/UzqmEKyrfSzokTd9ZCy1i+IIk5hrYKkjoYVQyNbrw7/F8XJ2rEwTg==}
-
/ufo@1.3.0:
resolution: {integrity: sha512-bRn3CsoojyNStCZe0BG0Mt4Nr/4KF+rhFlnNXybgqt5pXHNFRlqinSoQaTrGyzE4X8aHplSb+TorH+COin9Yxw==}
@@ -26320,7 +26351,6 @@ packages:
dependencies:
querystringify: 2.2.0
requires-port: 1.0.0
- dev: true
/url@0.11.0:
resolution: {integrity: sha512-kbailJa29QrtXnxgq+DdCEGlbTeYM2eJUxsz6vjZavrCYPMIFHMKQmSKYAIuUK2i7hgPm28a8piX5NTUtM/LKQ==}
@@ -26550,6 +26580,7 @@ packages:
/vite-node@0.27.2(@types/node@18.16.2):
resolution: {integrity: sha512-IDwuVhslF10qCnWOGJui7/2KksAOBHi+UbVo6Pqt4f5lgn+kS2sVvYDsETRG5PSuslisGB5CFGvb9I6FQgymBQ==}
engines: {node: '>=v14.16.0'}
+ hasBin: true
dependencies:
cac: 6.7.14
debug: 4.3.4(supports-color@8.1.1)
@@ -26572,6 +26603,7 @@ packages:
/vite-node@0.28.5(@types/node@13.13.52):
resolution: {integrity: sha512-LmXb9saMGlrMZbXTvOveJKwMTBTNUH66c8rJnQ0ZPNX+myPEol64+szRzXtV5ORb0Hb/91yq+/D3oERoyAt6LA==}
engines: {node: '>=v14.16.0'}
+ hasBin: true
dependencies:
cac: 6.7.14
debug: 4.3.4(supports-color@8.1.1)
@@ -26593,6 +26625,7 @@ packages:
/vite-node@0.28.5(@types/node@18.0.4):
resolution: {integrity: sha512-LmXb9saMGlrMZbXTvOveJKwMTBTNUH66c8rJnQ0ZPNX+myPEol64+szRzXtV5ORb0Hb/91yq+/D3oERoyAt6LA==}
engines: {node: '>=v14.16.0'}
+ hasBin: true
dependencies:
cac: 6.7.14
debug: 4.3.4(supports-color@8.1.1)
@@ -26614,6 +26647,7 @@ packages:
/vite-node@0.28.5(@types/node@18.16.2):
resolution: {integrity: sha512-LmXb9saMGlrMZbXTvOveJKwMTBTNUH66c8rJnQ0ZPNX+myPEol64+szRzXtV5ORb0Hb/91yq+/D3oERoyAt6LA==}
engines: {node: '>=v14.16.0'}
+ hasBin: true
dependencies:
cac: 6.7.14
debug: 4.3.4(supports-color@8.1.1)
@@ -26639,8 +26673,8 @@ packages:
dependencies:
cac: 6.7.14
debug: 4.3.4(supports-color@8.1.1)
- mlly: 1.2.0
- pathe: 1.1.0
+ mlly: 1.4.2
+ pathe: 1.1.1
picocolors: 1.0.0
vite: 4.3.9(@types/node@18.16.2)
transitivePeerDependencies:
@@ -27189,7 +27223,7 @@ packages:
resolution: {integrity: sha512-j9b8ynpJS4K+zfO5GGwsAcQX4ZHpWV+yRiHDiL+bE0XHJ8NiPYLTNVQdlFYWxtpg9lfAQNlwJg16J9AJtFSXRg==}
engines: {node: '>= 10.13.0'}
dependencies:
- acorn: 8.8.2
+ acorn: 8.10.0
acorn-walk: 8.2.0
chalk: 4.1.2
commander: 7.2.0