From 6fca8d2ad79270f0d18e5cd8496c8cb4da048850 Mon Sep 17 00:00:00 2001 From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com> Date: Wed, 30 Oct 2024 18:18:57 +0100 Subject: [PATCH 01/23] feat: free fees for thor holders before tip 014 --- .env.base | 1 + .env.dev | 1 + .env.develop | 1 + src/assets/translations/en/main.json | 6 +- .../SharedTradeInput/SharedTradeInput.tsx | 38 ++++--- .../useGetTradeQuotes/useGetTradeQuotes.tsx | 4 + .../ThorFreeFeeBanner/ThorFreeFeeBanner.tsx | 52 +++++++++ .../ThorFreeFeeBanner/asset-icons.png | Bin 0 -> 15571 bytes src/config.ts | 1 + src/context/AppProvider/AppContext.tsx | 8 +- src/lib/fees/model.ts | 34 +++++- src/lib/fees/parameters/index.ts | 4 + src/lib/fees/parameters/types.ts | 2 +- src/state/apis/snapshot/selectors.ts | 2 + src/state/apis/snapshot/snapshot.ts | 99 ++++++++++++++++++ .../preferencesSlice/preferencesSlice.ts | 2 + src/state/slices/tradeQuoteSlice/selectors.ts | 6 +- src/test/mocks/store.ts | 3 + 18 files changed, 238 insertions(+), 26 deletions(-) create mode 100644 src/components/ThorFreeFeeBanner/ThorFreeFeeBanner.tsx create mode 100644 src/components/ThorFreeFeeBanner/asset-icons.png diff --git a/.env.base b/.env.base index e81974a7976..97215213398 100644 --- a/.env.base +++ b/.env.base @@ -49,6 +49,7 @@ REACT_APP_FEATURE_FOX_PAGE_GOVERNANCE=true REACT_APP_FEATURE_PHANTOM_WALLET=true REACT_APP_FEATURE_ZRX_PERMIT2=true REACT_APP_FEATURE_PUBLIC_TRADE_ROUTE=false +REACT_APP_FEATURE_THOR_FREE_FEES=false # absolute URL prefix REACT_APP_ABSOLUTE_URL_PREFIX=https://app.shapeshift.com diff --git a/.env.dev b/.env.dev index bd1c54c4273..f3ed13195db 100644 --- a/.env.dev +++ b/.env.dev @@ -1,5 +1,6 @@ # feature flags REACT_APP_FEATURE_PUBLIC_TRADE_ROUTE=true +REACT_APP_FEATURE_THOR_FREE_FEES=true # logging REACT_APP_REDUX_WINDOW=false diff --git a/.env.develop b/.env.develop index 2c691e13604..d4ed854ee9e 100644 --- a/.env.develop +++ b/.env.develop @@ -1,6 +1,7 @@ # feature flags REACT_APP_FEATURE_LIMIT_ORDERS=true REACT_APP_FEATURE_PUBLIC_TRADE_ROUTE=true +REACT_APP_FEATURE_THOR_FREE_FEES=true # mixpanel REACT_APP_MIXPANEL_TOKEN=1c1369f6ea23a6404bac41b42817cc4b diff --git a/src/assets/translations/en/main.json b/src/assets/translations/en/main.json index f2738e2bbfc..b8d16c9d54e 100644 --- a/src/assets/translations/en/main.json +++ b/src/assets/translations/en/main.json @@ -1015,7 +1015,7 @@ "limitPrice": "Limit Price", "provider": "Provider", "expiration": "Expiration", - "networkFee":"Network Fee", + "networkFee": "Network Fee", "confirmInfo": "The limit order may not fill exactly when on-chain prices reach the limit price.", "learnMore": "Learn More", "previewOrder": "Preview Order", @@ -2704,5 +2704,9 @@ "cancelled": "Cancelled", "expired": "Expired" } + }, + "thorFees": { + "title": "Here from THORSwap?", + "description": "THOR holders trade for free through 2024, for any trades above $1000." } } diff --git a/src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInput.tsx b/src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInput.tsx index 91eb17a01e1..b003e271f1a 100644 --- a/src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInput.tsx +++ b/src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInput.tsx @@ -1,6 +1,7 @@ -import { Card, Center, Flex, useMediaQuery } from '@chakra-ui/react' +import { Box, Card, Center, Flex, useMediaQuery } from '@chakra-ui/react' import type { FormEvent } from 'react' import type { TradeInputTab } from 'components/MultiHopTrade/types' +import { ThorFreeFeeBanner } from 'components/ThorFreeFeeBanner/ThorFreeFeeBanner' import { breakpoints } from 'theme/theme' import { SharedTradeInputHeader } from '../SharedTradeInput/SharedTradeInputHeader' @@ -45,22 +46,25 @@ export const SharedTradeInput: React.FC = ({ maxWidth={isCompact || isSmallerThanXl ? '500px' : undefined} >
- - - {bodyContent} - {footerContent} - + + + + + {bodyContent} + {footerContent} + + [], @@ -164,6 +165,7 @@ export const useGetTradeQuotes = () => { const isSnapshotApiQueriesPending = useAppSelector(selectIsSnapshotApiQueriesPending) const votingPower = useAppSelector(state => selectVotingPower(state, votingPowerParams)) + const thorVotingPower = useAppSelector(state => selectVotingPower(state, thorVotingPowerParams)) const isVotingPowerLoading = useMemo( () => isSnapshotApiQueriesPending && votingPower === undefined, [isSnapshotApiQueriesPending, votingPower], @@ -225,6 +227,7 @@ export const useGetTradeQuotes = () => { const { feeBps, feeBpsBeforeDiscount } = calculateFees({ tradeAmountUsd, foxHeld: bnOrZero(votingPower), + thorHeld: bnOrZero(thorVotingPower), feeModel: 'SWAPPER', }) @@ -266,6 +269,7 @@ export const useGetTradeQuotes = () => { sellAmountCryptoPrecision, sellAsset, votingPower, + thorVotingPower, wallet, receiveAccountMetadata?.bip44Params, userSlippageTolerancePercentageDecimal, diff --git a/src/components/ThorFreeFeeBanner/ThorFreeFeeBanner.tsx b/src/components/ThorFreeFeeBanner/ThorFreeFeeBanner.tsx new file mode 100644 index 00000000000..3c92a8b2ed0 --- /dev/null +++ b/src/components/ThorFreeFeeBanner/ThorFreeFeeBanner.tsx @@ -0,0 +1,52 @@ +import { Box, Card, CardBody, Link } from '@chakra-ui/react' +import { Text } from 'components/Text' + +import icons from './asset-icons.png' + +const cardHover = { + textDecoration: 'none', + opacity: '.7', +} + +const cardBefore = { + content: "''", + backgroundImage: icons, + position: 'absolute', + top: 0, + left: 0, + width: '115px', + height: '100%', + backgroundPosition: 'left center', + backgroundRepeat: 'no-repeat', + backgroundSize: 'cover', + zIndex: 1, + pointerEvents: 'none', +} + +export const ThorFreeFeeBanner = () => { + return ( + + + + + + + + + + ) +} diff --git a/src/components/ThorFreeFeeBanner/asset-icons.png b/src/components/ThorFreeFeeBanner/asset-icons.png new file mode 100644 index 0000000000000000000000000000000000000000..e48c679cf4d8ebf866cc80959c3347106aff1c3a GIT binary patch literal 15571 zcmV;^JS@YBP)z1^@s6fA_v~00001b5ch_0Itp) z=>Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91cAx_Q1ONa40RR91iU0rr0DCg7MF0RiJxN4CRCodHeFtC^#rOYbucn7I zlF)naT|f{K5D<{Aei1C7hz;xrDi*K{QWOytMN~jViYTCTX^M#S8WJEOr1wj%?EmxT z?kFOdP!oRtfy>_Q&hF04XWpCl-n@CE0nqs=SxM7iiHt-@r&|!wzb^t?wty)p2yi*! zve{tt_gD2{#dHJki_41tt(K=t+X0%FEPMs1_J_{H$&8Y_ad}M512wqLDNqc z|JdXyIQiuq)SUSd{2MidR8A#L+ikEIx~$gxj(JaA@5d4WtijI@A-CL&sG&nqs(pKC zCKFDh#;>H{lqEl6|NTSZ$jpMZY+2Nv_ca2WHa?Y&72T;hv`(u70#01-pi2FUFR%nl z0E!qm5RrrLhELV1r@%Z-iEfB;ckRNi-u;k&>@-1I%a=#}#S7qDtES52zDHZ;%*yNk zTSP>b`_Ak4Z}9_`fcn>~ho}c1M0oE$FqI1CVBtC&0hZbet}`8Bq^BfS}fGpqa^Ty@{oC z<>Vo4&2O-l4nbhECJHFsZpV=^W0COTbYlMx@ICkXlG5w{{3ER8%6fZ)o#XP0YkdOW zBo9A5KzrYHogA+nu%t{GJAG@`BD=iR^x<7mepURco`7}LShxX(x)_|GgH0|Ge z#08&N{4ZX0U`4Vug@z*VhSmrla0i0gwfC@F;z_WD?K(0tk-lyt62F>{>|F=pav4#o zO;gl-??o5`0|BQ4N%Q6*=9y>UNKY?RD+je{jk@!{p(GvR&adN`raAt%);DBrp&n4e zGF>%b4OS~G(Pa>FTTg`b?GHbWqe{AEgJ}j0y4`^S3O^~!Rv~%OPslwK!wY7>Y=vL7 zDyTK>HTaZ^QWTxBW(^KK`Uq@Cjuff6qwc>C)!ustnuX6*N&n5Pn+C4*^E+Oo2bF-# zXAESQzqCSuhJZlDZbN$XLP)pU+|n&y<>9t)<>n!0*Ip!j_XE<`Zh}1{m*7#xG5YXh zhB-V8m0y065^|K{(s_IKAnw&yiVUjME@|F;1hs03(*L{D%^je%vcvftA58v!eT}zQ zhGGe*DKrd$ZEi$ZpZ*AH+a5+SCP?e@k4_7bBPA7Co3<$G%-Xma&U^}~1QKRnz+hA# zgVlm6FO5RPZJp#!xC-)daM)0!Ec`)Ht~y!7KQhkN`toxGG-=ABDA_4fhu(d?Z_xT8 zKB`z}zpTJ&CNq3$)kdk#T~VrQclgz9pxE9iK=W+K*}WTS%U2+SW2o)mLFf*<8zcc! zfVI#ZXtF9cUE#6&5Pi=;f^49o;zZnp*APExBJXire510{mFmy|HD*kQr3{5N7LI1e zO-`4s?{EGQ@fZK#;&ta`0oE84jDXfRBK(d)q|$aU1_z&_PT`-9)D&cG+f&ci+!(v^J^J;$?4&-R8#~ zMaJ^w1oQ{k4(ul=96QZ^1dx;|KsuASKs}Fo?*F{LL zehBVz8>|&8xml$=i61c<}}=8Q@3t65duV>@&Zd;8vw{MaMzXI#d=!L3_!yqyhmRB828UDucT`Z@0U zMGL)rJ?Ju)wHCnYs)iSQv0AfQ!S3PT=!(A^|7zGoxr_dk*H!$Rczy&KLP zO1A=-iB#xj%xtb~oMS9f?QP)vMmEdKGZTDgG~E9VrD{7!Z*fJz4towocF zk05GLUzJC<7a->G5lERgm#;Z*5#^zmlzggGLH%Vv!zzLl6DM#tI-I$^HwQ!}UebfF z@y-8Au}12iel6P~WWZnqwx+5QTuR~NEvqX#2U%NwN7|xA%H-#?iIhsfghE{$FA0k3 zFJ4Jkgd)M179*9%3#?9dY~%$}ahYL*sbBVkE|kNp36m!x<-2dMD5!i5Y=;jcVfu7b ze0iMeP&6>MGQ%>6FMh8iQ?0b2n;h7n=IC+=yqT)XZGGX>pfOn^&37K#tz>j0B_j1l zO14XvB0ok=$mN-H0#%2U{5&a7KU_Ep8zW07^QE>hhT~@B)|!< z0-l_gqhh;cy<$}oDOeJ*_Ni2zEO>;HsO5cmJ9ps7(<84Ms8S63$&)zo$)~6^f$EQ# zH#M!g#p$BBjG1!1BqmeYx9CRFrX!5u;nY9~5`mBgc4cKDXVW^e+XWPcc5*hwLxlpI z0H_lz0Z*YXH}JbRDQfhLr^1d!8Wv!^;L2~3rWoWajCl$cnNs5;>`5mP^YAd(6Hj=b zbk&Z@oK%SBl%Y@MO7czvM~T}T==#@xYZ}>1$xEi7US7MY;N*h+*imGyqN=j^J5uTa zg`Hxj!fGWc>JycPNGP>M0Crze*Cd~pQsufwjZx~2eD_Gn1{w*Bj1MD zCmu!a?wy{8UEM=LVj_~hnTv9xpH*W#`)iwk+}t`W#qU?QB$wxHTI~aQx?W&I?w0jP zoA(8BHm*~ier^gQ5CV<@ODIsN&%<8T7)pR;{1i%8=Yy z=F%%scDS)v6t0MckdYF`QelXiFbx6iJ9$9!fUERUk1bbGEx{Eqh1JS6bGCIAbV!furTLb?jC ziIqv%u;eAc2D1TWMva8B@Gy&m-i1e>e3$~-{$kZ)4o)-(JN63uf^bt3no{ zFoxs9`@nvjx+y8h5E_oM6K5f~$AHt|3dkzC$3w3Gq$pOvmu?|!R|!@ECV#6|@!UsN zZwwAW^kXAYs^cx{k~1?MN1u5F*&EinA6=aOvg|2~7t>Cip*lQekn*f%o8nZ&!j#33 zzke6vAHNg!6UPV$&3S$Sh9Y8y*QF%cmPJI^{KP}{Ayq!iKRD4qYkr>$a?PEPvndmg*>_~RZ zik6%tBnI2xe<1#`y9n-a{_=o3=1tm*+q=OPWWAs&lv!h8vGQH%sRUD08wIcc%S7-s zI-&*MbPK`<520m=z!S){`E!u``3yC^=gwIC{yKAL3+K#_!j*qG&bCZTOA9D|B|U$M z6tHsHwtWi{MiSh_1pXrX42?weo73UfqAh=`DoQp5m}0|9fFig;y~2tGSg|Y#d#04E zQ8$+56_K1EH@;K5QS2ewCoj{C^#wB%q${ADb*$qrm!GH-ZvN) zD*Mt=3Swh$;+6{O=7|CGDqZ>l}CCQ7w61UOvw#>KzXr%QMiuyQ(Y>jqLT!HwCk z1RYc9GKhX_hO#HS{eT5r`4HGMsa7aersJsRilL_T{SUyeQByuq^~y!nrRBHh995l&`yWDZcM4w|mu2cLamwqQ>RaIM5S}L#_k)8*irk&M zRJEk6WipTSd+zI}Mjrd5NmZ;KXB2QbycAo2{}osPD|f?6B)vTb_Si!TJ7Xl5G(Phx zd>b|=V7yJ#8#P-abvq4k@U1-%He{H}BW|LpU(H1N+*urYN#P|a5xLx*J2}d5oMrWL zOaT#u@=Y`BTjLLWzu@@vdfgAsX%(oC8)p!Zi(A#s11yWl(q^w*jO2F+Zrovoor&N^ zzx6SE8nq;-A}sME^RZGd=J##T1d&gTrKce!V1g~S=cEs~Xo`!gN@B|0w->r+!B?gw z1j*KL6gUy&a=C6Z8C{F~a&k8uExTYy!n;kw1l-~T@cDriBv{g{Ult+x-LbGAjZxT` zqRS)t^$+1wpGz49ST%=B?~BMNM87cJ?MA~MZrisDiElm+Cw+=bYAWCqrjtuhgdsVo z)}8P#PbMYib^3Za4HTALbv1|cQv}+!9-m(O-Pmch8}FJ|SB=Bhy3Q8{R!T>SH*3WY zNPbVija8L2m#yFiw=M-mf^YD(B5Ld_lz=PAPmZfOIQixnO2VY{5}fQwNpNOmx(6E8 zYvyiWOk8pjdlT%GB!v6Q8ZuIMb4G;>9Nr)V&Xax{9#Ghh=BtSVo!}znzQ258jVZe-ql*4aC43OQA_cUsz2BrSTC;`X9K&vv!YW^&M|8JAR0~ zdwVXYCkxlcm$4-Z53IbD;0CnshR8QRA-G&*#kWy4QT@<`p&vz_>UQyL!p=orS%O~m$C8{2{Uyx7DYF0#~e&*nmYJpc%Jt`Wos*yf08c zVH^dmEIAn~#Et$7?P(H>jo=TGtp3AE8gYN&HNzTjlTGfs=2&sw^#`Tq7HB zb0^N`84+~*-8|=VQ!dx!l;mWT0M|mTtc3VN{aaFi@iK%PAc{h9~DM1tUg@lb=N&ptl2F8$_qt>QB;iE;}C ztgLMJy^-r?N6}PBp-C*C1xF6!I(4Hh>g-U)tMy+eJ2n_2~8VxG@!Rgc$i*@oY4?(ictjMR^(9^Sz z;c!B(;&l0T7har~2!E>qT^dm6B&muz#2D#cbIk2Pw!G(3!2NGv`E+ZOb_jXuT^Pfn zRAubPk0R-+TYafsyCLld}_GxI0>bE0!vz(wfyUYyXU<-E@6xioDO8zbMBG1Iwpc>NG>h zv+u)Lrjjb7i>um`NvZaP;{=U9PHG7U*)0Kdrk+6RR4yJSN&9#7Q0|?>-QZ3ft0oDG zQn+iLmx7Zig1{;d7##uG$>`TMLc6_9Iowbu1iof2++w{2PRWx_R=aLb4i`ie;PzJG zZm$ux;lfQx`1lvGIiFi20r%n0;_>#NX!LFt;1;Ot_U3^meSX|mZTsl@`t*wd_kzHh zE7wNob5mgAQ5nLs(vkf3^Tg@mcPOGR$4>coi0nzGuIZRJBqT?8~XF&B$BC*z0TP9P}I ziXTR^y?9L~<+mNm!syvYRa2b{tNV$UiK(Jp~++4>`f$MZQZZc%1Pr>S&hU)t2 z{Gx(;Ua8nrp(euLo)1&4ChU#GEhn2S^##~=ZsTvBpjsm~Tp)<=Y`;6abMjToHEEh)(q7S6Co;i_6v=Bmz^kUhMnW<8-pv8c%@kxO)kTM2S12GHmUNzQXZq#jrt-1JLBC$mat{rY5!l`zO={x z>)wC6{;w2kEL|3*Mo)vWe05bR7oES;KYRmuTh@_Md5Zw+jo-{lx|eo%%_@8i0e7aJ zM9O<(*a(1shubJEn-I|RZa5N;BV+ayHSPCr1u9PGEc6u7e0`<6!bHv#nR@~$>A6_9 zD-HA4oWO=%NxDtK2Qmc*Sn%@Q)ls`*5Gl@y0=tHXr|v;QYT@*GY0|W82S(0H#KI>l zkj;xQrSrW7V-8KOyG7S`t}e=i{W-uIONAro(Klde)K=jqK`b+8O-9bDrL3K-%^{FN zoWg?TJE>8D&1i&*g4H#G%t9$}-xEaKa&c}lFwqrh5!b;9R3P?Zk8ZgqFXtT{q1MS z{+T|(Qb7Sx$}M9m7cr~#u~KU9N;_de!k)!(Tm&nY#F@-q{?^mTh5biQfht9(+M{U?_o%4d(=k%@cQHClUUUp! ztvsfr=c3k6!2Nn%62ALAhf=m19!}WTwi@-ZMS0dKU^O;)z i*L?^vC4dAWXVDyF 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 literal 0 HcmV?d00001 diff --git a/src/config.ts b/src/config.ts index e13f3f1d5c1..dd854e2c4f9 100644 --- a/src/config.ts +++ b/src/config.ts @@ -175,6 +175,7 @@ const validators = { REACT_APP_FEATURE_LIMIT_ORDERS: bool({ default: false }), REACT_APP_ZRX_BASE_URL: url(), REACT_APP_FEATURE_PUBLIC_TRADE_ROUTE: bool({ default: false }), + REACT_APP_FEATURE_THOR_FREE_FEES: bool({ default: false }), } function reporter({ errors }: envalid.ReporterOptions) { diff --git a/src/context/AppProvider/AppContext.tsx b/src/context/AppProvider/AppContext.tsx index 07aadc48371..f9e6c36b376 100644 --- a/src/context/AppProvider/AppContext.tsx +++ b/src/context/AppProvider/AppContext.tsx @@ -8,6 +8,7 @@ import React, { useEffect } from 'react' import { useTranslate } from 'react-polyglot' import { useNfts } from 'components/Nfts/hooks/useNfts' import { usePlugins } from 'context/PluginProvider/PluginProvider' +import { useFeatureFlag } from 'hooks/useFeatureFlag/useFeatureFlag' import { useIsSnapInstalled } from 'hooks/useIsSnapInstalled/useIsSnapInstalled' import { useMixpanelPortfolioTracking } from 'hooks/useMixpanelPortfolioTracking/useMixpanelPortfolioTracking' import { useModal } from 'hooks/useModal/useModal' @@ -61,6 +62,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { const routeAssetId = useRouteAssetId() const { isSnapInstalled } = useIsSnapInstalled() const { close: closeModal, open: openModal } = useModal('ledgerOpenApp') + const isThorFreeFeesEnabled = useFeatureFlag('ThorFreeFees') useEffect(() => { const handleLedgerOpenApp = ({ chainId, reject }: LedgerOpenAppEventArgs) => { @@ -134,7 +136,11 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { dispatch( snapshotApi.endpoints.getVotingPower.initiate({ model: 'SWAPPER' }, { forceRefetch: true }), ) - }, [dispatch, isConnected, portfolioLoadingStatus]) + + if (isThorFreeFeesEnabled) { + dispatch(snapshotApi.endpoints.getThorVotingPower.initiate()) + } + }, [dispatch, isConnected, portfolioLoadingStatus, isThorFreeFeesEnabled]) // Resets the sell and buy asset AccountIDs on wallet change to that we don't get stale trade input account selections while we're loading the new wallet useEffect(() => { diff --git a/src/lib/fees/model.ts b/src/lib/fees/model.ts index 4e8791146df..9b3a8898d41 100644 --- a/src/lib/fees/model.ts +++ b/src/lib/fees/model.ts @@ -1,4 +1,5 @@ import BigNumber from 'bignumber.js' +import { getConfig } from 'config' import { bn, bnOrZero } from 'lib/bignumber/bignumber' import { selectIsSnapshotApiQueriesRejected } from 'state/apis/snapshot/selectors' import { store } from 'state/store' @@ -6,9 +7,13 @@ import { store } from 'state/store' import { FEE_CURVE_PARAMETERS } from './parameters' import type { ParameterModel } from './parameters/types' +const THORSWAP_UNIT_THRESHOLD = 1 +const THORSWAP_MAXIMUM_YEAR_TRESHOLD = 2025 + type CalculateFeeBpsArgs = { tradeAmountUsd: BigNumber foxHeld: BigNumber + thorHeld?: BigNumber feeModel: ParameterModel } @@ -34,7 +39,7 @@ export type CalculateFeeBpsReturn = { } type CalculateFeeBps = (args: CalculateFeeBpsArgs) => CalculateFeeBpsReturn -export const calculateFees: CalculateFeeBps = ({ tradeAmountUsd, foxHeld, feeModel }) => { +export const calculateFees: CalculateFeeBps = ({ tradeAmountUsd, foxHeld, feeModel, thorHeld }) => { const { FEE_CURVE_NO_FEE_THRESHOLD_USD, FEE_CURVE_MAX_FEE_BPS, @@ -48,15 +53,26 @@ export const calculateFees: CalculateFeeBps = ({ tradeAmountUsd, foxHeld, feeMod const minFeeBps = bn(FEE_CURVE_MIN_FEE_BPS) const midpointUsd = bn(FEE_CURVE_MIDPOINT_USD) const feeCurveSteepness = bn(FEE_CURVE_STEEPNESS_K) + const isThorFreeEnabled = getConfig().REACT_APP_FEATURE_THOR_FREE_FEES // trades below the fee threshold are free. - const isFree = tradeAmountUsd.lt(noFeeThresholdUsd) + const isFree = + tradeAmountUsd.lt(noFeeThresholdUsd) || + (isThorFreeEnabled && thorHeld?.isGreaterThanOrEqualTo(THORSWAP_UNIT_THRESHOLD)) + + const isThorFree = + isThorFreeEnabled && + thorHeld?.isGreaterThanOrEqualTo(THORSWAP_UNIT_THRESHOLD) && + new Date().getFullYear() <= THORSWAP_MAXIMUM_YEAR_TRESHOLD + // failure to fetch fox discount results in free trades. const isFallbackFees = selectIsSnapshotApiQueriesRejected(store.getState()) // the fox discount before any other logic is applied const foxBaseDiscountPercent = (() => { if (isFree) return bn(100) + // THOR holder before TIP014 are trade free until 2025 + if (isThorFree) return bn(100) // No discount if we cannot fetch FOX holdings if (isFallbackFees) return bn(0) @@ -68,7 +84,7 @@ export const calculateFees: CalculateFeeBps = ({ tradeAmountUsd, foxHeld, feeMod // the fee bps before the fox discount is applied, as a floating point number const feeBpsBeforeDiscountFloat = - isFallbackFees && !isFree + isFallbackFees && !isFree && !isThorFree ? bn(FEE_CURVE_MAX_FEE_BPS) : minFeeBps.plus( maxFeeBps @@ -88,7 +104,7 @@ export const calculateFees: CalculateFeeBps = ({ tradeAmountUsd, foxHeld, feeMod ) const feeBpsFloat = - isFallbackFees && !isFree + isFallbackFees && !isFree && !isThorFree ? bn(FEE_CURVE_MAX_FEE_BPS) : BigNumber.maximum( feeBpsBeforeDiscountFloat.multipliedBy(bn(1).minus(foxBaseDiscountPercent.div(100))), @@ -108,6 +124,16 @@ export const calculateFees: CalculateFeeBps = ({ tradeAmountUsd, foxHeld, feeMod const feeUsd = feeUsdBeforeDiscount.minus(feeUsdDiscount) const foxDiscountUsd = feeUsdBeforeDiscount.times(foxDiscountPercent.div(100)) + console.log({ + feeBps: feeBps.toFixed(), + feeBpsFloat: feeBpsFloat.toFixed(), + feeUsd: feeUsd.toFixed(), + foxDiscountPercent: foxDiscountPercent.toFixed(), + foxDiscountUsd: foxDiscountUsd.toFixed(), + feeUsdBeforeDiscount: feeUsdBeforeDiscount.toFixed(), + feeBpsBeforeDiscount: feeBpsBeforeDiscount.toFixed(), + }) + return { feeBps, feeBpsFloat, diff --git a/src/lib/fees/parameters/index.ts b/src/lib/fees/parameters/index.ts index d732e5a119b..c51ff4dfcc5 100644 --- a/src/lib/fees/parameters/index.ts +++ b/src/lib/fees/parameters/index.ts @@ -5,14 +5,18 @@ import type { FeeCurveParameters, ParameterModel } from './types' export const FEE_CURVE_PARAMETERS: Record = { SWAPPER: swapperParameters, THORCHAIN_LP: thorchainLpParameters, + // THOR asset doesn't have any fee models for now as it's not using curves + THORSWAP: {} as FeeCurveParameters, } export const FEE_MODEL_TO_FEATURE_NAME: Record = { SWAPPER: 'common.trade', THORCHAIN_LP: 'common.lpDeposit', + THORSWAP: 'common.trade', } export const FEE_MODEL_TO_FEATURE_NAME_PLURAL: Record = { SWAPPER: 'common.trades', THORCHAIN_LP: 'common.lpDeposits', + THORSWAP: 'common.trade', } diff --git a/src/lib/fees/parameters/types.ts b/src/lib/fees/parameters/types.ts index 7c0996aa01c..c89793f194b 100644 --- a/src/lib/fees/parameters/types.ts +++ b/src/lib/fees/parameters/types.ts @@ -8,4 +8,4 @@ export type FeeCurveParameters = { FEE_CURVE_FOX_DISCOUNT_DELAY_HOURS: number } -export type ParameterModel = 'SWAPPER' | 'THORCHAIN_LP' +export type ParameterModel = 'SWAPPER' | 'THORCHAIN_LP' | 'THORSWAP' diff --git a/src/state/apis/snapshot/selectors.ts b/src/state/apis/snapshot/selectors.ts index fbb76dbc613..ff0a43ffdee 100644 --- a/src/state/apis/snapshot/selectors.ts +++ b/src/state/apis/snapshot/selectors.ts @@ -32,5 +32,7 @@ export const selectVotingPower = createSelector( return votingPowerByModel[feeModel!] }, ) +export const selectThorVotingPower = (state: ReduxState) => + state.snapshot.votingPowerByModel['THORSWAP'] export const selectProposals = (state: ReduxState) => state.snapshot.proposals diff --git a/src/state/apis/snapshot/snapshot.ts b/src/state/apis/snapshot/snapshot.ts index 4fde1639750..4c77e66cdeb 100644 --- a/src/state/apis/snapshot/snapshot.ts +++ b/src/state/apis/snapshot/snapshot.ts @@ -18,13 +18,18 @@ import { ProposalSchema, SnapshotSchema, VotingPowerSchema } from './validators' type FoxVotingPowerCryptoBalance = string const SNAPSHOT_SPACE = 'shapeshiftdao.eth' +const THORSWAP_SNAPSHOT_SPACE = 'thorswapcommunity.eth' + +const THOR_TIP_014_BLOCK_NUMBER = 21072340 export const initialState: SnapshotState = { votingPowerByModel: { SWAPPER: undefined, THORCHAIN_LP: undefined, + THORSWAP: undefined, }, strategies: undefined, + thorStrategies: undefined, proposals: undefined, } @@ -36,6 +41,7 @@ type ProposalsState = { export type SnapshotState = { votingPowerByModel: Record strategies: Strategy[] | undefined + thorStrategies: Strategy[] | undefined proposals: ProposalsState | undefined } @@ -50,9 +56,15 @@ export const snapshot = createSlice({ const { model, foxHeld } = payload state.votingPowerByModel[model] = foxHeld }, + setThorVotingPower: (state, { payload }: { payload: string }) => { + state.votingPowerByModel['THORSWAP'] = payload + }, setStrategies: (state, { payload }: { payload: Strategy[] }) => { state.strategies = payload }, + setThorStrategies: (state, { payload }: { payload: Strategy[] }) => { + state.thorStrategies = payload + }, setProposals: (state, { payload }: { payload: ProposalsState }) => { state.proposals = payload }, @@ -94,6 +106,36 @@ export const snapshotApi = createApi({ } }, }), + getThorStrategies: build.query({ + keepUnusedDataFor: Number.MAX_SAFE_INTEGER, // never refetch these + queryFn: async (_, { dispatch }) => { + const query = ` + query { + space(id: "${THORSWAP_SNAPSHOT_SPACE}") { + strategies { + name + network + params + } + } + } + ` + // https://hub.snapshot.org/graphql?query=query%20%7B%0A%20%20space(id%3A%20%22shapeshiftdao.eth%22)%20%7B%0A%20%20%20%20strategies%20%7B%0A%20%20%20%20%20%20name%0A%20%20%20%20%20%20network%0A%20%20%20%20%20%20params%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D + const { data: resData } = await axios.post( + 'https://hub.snapshot.org/graphql', + { query }, + { headers: { Accept: 'application/json' } }, + ) + try { + const { strategies } = SnapshotSchema.parse(resData).data.space + dispatch(snapshot.actions.setThorStrategies(strategies)) + return { data: strategies } + } catch (e) { + console.error('snapshotApi getStrategies', e) + return { data: [] } + } + }, + }), getVotingPower: build.query({ queryFn: async ({ model }, { dispatch, getState }) => { try { @@ -152,6 +194,63 @@ export const snapshotApi = createApi({ } }, }), + getThorVotingPower: build.query({ + queryFn: async (_, { dispatch, getState }) => { + try { + const accountIds: AccountId[] = + (getState() as ReduxState).portfolio.accountMetadata.ids ?? [] + const strategies = await (async () => { + const maybeSliceStragies = (getState() as ReduxState).snapshot.thorStrategies + if (maybeSliceStragies) return maybeSliceStragies + + const strategiesResult = await dispatch( + snapshotApi.endpoints.getThorStrategies.initiate(), + ) + return strategiesResult?.data + })() + if (!strategies) { + console.log('snapshotApi getThorVotingPower could not get strategies') + return { data: bn(0).toString() } + } + const evmAddresses = Array.from( + accountIds.reduce>((acc, accountId) => { + const { account, chainId } = fromAccountId(accountId) + isEvmChainId(chainId) && acc.add(account) + return acc + }, new Set()), + ) + const delegation = false // don't let people delegate for discounts - ambiguous in spec + const votingPowerResults = await Promise.all( + evmAddresses.map(async address => { + const votingPowerUnvalidated = await getVotingPower( + address, + '1', + strategies, + THOR_TIP_014_BLOCK_NUMBER, + THORSWAP_SNAPSHOT_SPACE, + delegation, + ) + // vp is THOR in crypto balance + return bnOrZero(VotingPowerSchema.parse(votingPowerUnvalidated).vp) + }), + ) + const thorHeld = BigNumber.sum(...votingPowerResults).toNumber() + + // Return an error tuple in case of an invalid foxHeld value so we don't cache an errored value + if (isNaN(thorHeld)) { + const data = 'NaN thorHeld value' + return { error: { data, status: 400 } } + } + + dispatch(snapshot.actions.setThorVotingPower(thorHeld.toString())) + return { data: thorHeld.toString() } + } catch (e) { + console.error(e) + + return { error: { data: e, status: 400 } } + } + }, + }), getProposals: build.query({ queryFn: async (_, { dispatch }) => { const query = ` diff --git a/src/state/slices/preferencesSlice/preferencesSlice.ts b/src/state/slices/preferencesSlice/preferencesSlice.ts index 3e1d668ee2c..dae1cbe63c5 100644 --- a/src/state/slices/preferencesSlice/preferencesSlice.ts +++ b/src/state/slices/preferencesSlice/preferencesSlice.ts @@ -69,6 +69,7 @@ export type FeatureFlags = { FoxPageGovernance: boolean LimitOrders: boolean PublicTradeRoute: boolean + ThorFreeFees: boolean } export type Flag = keyof FeatureFlags @@ -161,6 +162,7 @@ const initialState: Preferences = { FoxPageGovernance: getConfig().REACT_APP_FEATURE_FOX_PAGE_GOVERNANCE, LimitOrders: getConfig().REACT_APP_FEATURE_LIMIT_ORDERS, PublicTradeRoute: getConfig().REACT_APP_FEATURE_PUBLIC_TRADE_ROUTE, + ThorFreeFees: getConfig().REACT_APP_FEATURE_THOR_FREE_FEES, }, selectedLocale: simpleLocale(), balanceThreshold: '0', diff --git a/src/state/slices/tradeQuoteSlice/selectors.ts b/src/state/slices/tradeQuoteSlice/selectors.ts index f744a26b6ab..916c925124e 100644 --- a/src/state/slices/tradeQuoteSlice/selectors.ts +++ b/src/state/slices/tradeQuoteSlice/selectors.ts @@ -15,7 +15,7 @@ import type { CalculateFeeBpsReturn } from 'lib/fees/model' import { calculateFees } from 'lib/fees/model' import type { ParameterModel } from 'lib/fees/parameters/types' import { fromBaseUnit } from 'lib/math' -import { selectVotingPower } from 'state/apis/snapshot/selectors' +import { selectThorVotingPower, selectVotingPower } from 'state/apis/snapshot/selectors' import { validateQuoteRequest } from 'state/apis/swapper/helpers/validateQuoteRequest' import { selectIsTradeQuoteApiQueryPending } from 'state/apis/swapper/selectors' import type { ApiQuote, ErrorWithMeta, TradeQuoteError } from 'state/apis/swapper/types' @@ -559,11 +559,13 @@ export const selectCalculatedFees: Selector = (_state: ReduxState, { feeModel }: AffiliateFeesProps) => feeModel, (_state: ReduxState, { inputAmountUsd }: AffiliateFeesProps) => inputAmountUsd, selectVotingPower, + selectThorVotingPower, - (feeModel, inputAmountUsd, votingPower) => { + (feeModel, inputAmountUsd, votingPower, thorVotingPower) => { const fees: CalculateFeeBpsReturn = calculateFees({ tradeAmountUsd: bnOrZero(inputAmountUsd), foxHeld: bnOrZero(votingPower), + thorHeld: bnOrZero(thorVotingPower), feeModel, }) diff --git a/src/test/mocks/store.ts b/src/test/mocks/store.ts index 2c70040a685..426cf9dd2a1 100644 --- a/src/test/mocks/store.ts +++ b/src/test/mocks/store.ts @@ -123,6 +123,7 @@ export const mockStore: ReduxState = { FoxPageGovernance: false, LimitOrders: false, PublicTradeRoute: false, + ThorFreeFees: false, }, selectedLocale: 'en', balanceThreshold: '0', @@ -244,8 +245,10 @@ export const mockStore: ReduxState = { votingPowerByModel: { SWAPPER: undefined, THORCHAIN_LP: undefined, + THORSWAP: undefined, }, strategies: undefined, + thorStrategies: undefined, proposals: undefined, }, localWalletSlice: { From 926f33fec47e122d23597fcf393af6c0b94e2515 Mon Sep 17 00:00:00 2001 From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com> Date: Wed, 30 Oct 2024 18:29:53 +0100 Subject: [PATCH 02/23] feat: free fees for thor holders before tip 014 --- .../components/SharedTradeInput/SharedTradeInput.tsx | 2 +- src/components/ThorFreeFeeBanner/ThorFreeFeeBanner.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInput.tsx b/src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInput.tsx index b003e271f1a..6be561c643d 100644 --- a/src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInput.tsx +++ b/src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInput.tsx @@ -45,7 +45,7 @@ export const SharedTradeInput: React.FC = ({ justifyContent='center' maxWidth={isCompact || isSmallerThanXl ? '500px' : undefined} > -
+
{ _hover={cardHover} position='relative' overflow='hidden' - mb={6} + mb={4} _before={cardBefore} > Date: Wed, 30 Oct 2024 18:32:09 +0100 Subject: [PATCH 03/23] feat: free fees for thor holders before tip 014 --- src/lib/fees/model.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/fees/model.ts b/src/lib/fees/model.ts index 9b3a8898d41..1688768834b 100644 --- a/src/lib/fees/model.ts +++ b/src/lib/fees/model.ts @@ -63,7 +63,7 @@ export const calculateFees: CalculateFeeBps = ({ tradeAmountUsd, foxHeld, feeMod const isThorFree = isThorFreeEnabled && thorHeld?.isGreaterThanOrEqualTo(THORSWAP_UNIT_THRESHOLD) && - new Date().getFullYear() <= THORSWAP_MAXIMUM_YEAR_TRESHOLD + new Date().getFullYear() < THORSWAP_MAXIMUM_YEAR_TRESHOLD // failure to fetch fox discount results in free trades. const isFallbackFees = selectIsSnapshotApiQueriesRejected(store.getState()) From 8ea88694e7a5cf502e8f2d9da5ef3dd82cc329fd Mon Sep 17 00:00:00 2001 From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com> Date: Wed, 30 Oct 2024 21:20:46 +0100 Subject: [PATCH 04/23] feat: free fees for thor holders before tip 014 --- src/assets/translations/en/main.json | 2 +- .../MultiHopTradeConfirm/hooks/useTradeExecution.tsx | 8 ++++++++ src/lib/fees/model.ts | 4 ++-- src/lib/mixpanel/types.ts | 1 + 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/assets/translations/en/main.json b/src/assets/translations/en/main.json index b8d16c9d54e..a6c3e85ee22 100644 --- a/src/assets/translations/en/main.json +++ b/src/assets/translations/en/main.json @@ -2707,6 +2707,6 @@ }, "thorFees": { "title": "Here from THORSwap?", - "description": "THOR holders trade for free through 2024, for any trades above $1000." + "description": "THOR holders trade for free through 2024." } } diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useTradeExecution.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useTradeExecution.tsx index 3ee0338bf4f..1d90cba0348 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useTradeExecution.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useTradeExecution.tsx @@ -22,12 +22,15 @@ import { useCallback, useEffect, useMemo, useRef } from 'react' import { useTranslate } from 'react-polyglot' import { useErrorHandler } from 'hooks/useErrorToast/useErrorToast' import { useWallet } from 'hooks/useWallet/useWallet' +import { bnOrZero } from 'lib/bignumber/bignumber' +import { THORSWAP_UNIT_THRESHOLD } from 'lib/fees/model' import { MixPanelEvent } from 'lib/mixpanel/types' import { TradeExecution } from 'lib/tradeExecution' import { assertUnreachable } from 'lib/utils' import { assertGetCosmosSdkChainAdapter } from 'lib/utils/cosmosSdk' import { assertGetEvmChainAdapter, signAndBroadcast } from 'lib/utils/evm' import { assertGetUtxoChainAdapter } from 'lib/utils/utxo' +import { selectThorVotingPower, selectVotingPower } from 'state/apis/snapshot/selectors' import { selectAssetById, selectPortfolioAccountMetadataByAccountId } from 'state/slices/selectors' import { selectActiveQuote, @@ -52,6 +55,7 @@ export const useTradeExecution = ( const { showErrorToast } = useErrorHandler() const trackMixpanelEvent = useMixpanel() const hasMixpanelSuccessOrFailFiredRef = useRef(false) + const thorVotingPower = useAppSelector(selectThorVotingPower) const hopSellAccountIdFilter = useMemo(() => { return { @@ -211,6 +215,10 @@ export const useTradeExecution = ( if (isLastHop && !hasMixpanelSuccessOrFailFiredRef.current) { trackMixpanelEvent(MixPanelEvent.TradeSuccess) hasMixpanelSuccessOrFailFiredRef.current = true + + if (bnOrZero(thorVotingPower).toNumber() >= THORSWAP_UNIT_THRESHOLD) { + trackMixpanelEvent(MixPanelEvent.ThorDiscountTradeSuccess) + } } resolve() diff --git a/src/lib/fees/model.ts b/src/lib/fees/model.ts index 1688768834b..ba7b9cf88c9 100644 --- a/src/lib/fees/model.ts +++ b/src/lib/fees/model.ts @@ -7,7 +7,7 @@ import { store } from 'state/store' import { FEE_CURVE_PARAMETERS } from './parameters' import type { ParameterModel } from './parameters/types' -const THORSWAP_UNIT_THRESHOLD = 1 +export const THORSWAP_UNIT_THRESHOLD = 1 const THORSWAP_MAXIMUM_YEAR_TRESHOLD = 2025 type CalculateFeeBpsArgs = { @@ -63,7 +63,7 @@ export const calculateFees: CalculateFeeBps = ({ tradeAmountUsd, foxHeld, feeMod const isThorFree = isThorFreeEnabled && thorHeld?.isGreaterThanOrEqualTo(THORSWAP_UNIT_THRESHOLD) && - new Date().getFullYear() < THORSWAP_MAXIMUM_YEAR_TRESHOLD + new Date().getUTCFullYear() < THORSWAP_MAXIMUM_YEAR_TRESHOLD // failure to fetch fox discount results in free trades. const isFallbackFees = selectIsSnapshotApiQueriesRejected(store.getState()) diff --git a/src/lib/mixpanel/types.ts b/src/lib/mixpanel/types.ts index 0ad1bf7e62b..4bad8748219 100644 --- a/src/lib/mixpanel/types.ts +++ b/src/lib/mixpanel/types.ts @@ -31,6 +31,7 @@ export enum MixPanelEvent { RepayConfirm = 'Repay Confirm', TradeConfirmSecondHop = 'Trade Confirm Second Hop', TradeSuccess = 'Trade Success', + ThorDiscountTradeSuccess = 'Thor Discount Trade Success', BorrowSuccess = 'Borrow Success', RepaySuccess = 'Repay Success', TradeFailed = 'Trade Failed', From 41ba1d083fdb7f648dbdabce1ccdb7dfa6dd9c44 Mon Sep 17 00:00:00 2001 From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com> Date: Wed, 30 Oct 2024 21:23:09 +0100 Subject: [PATCH 05/23] feat: free fees for thor holders before tip 014 --- .../hooks/useTradeExecution.tsx | 3 ++- .../ThorFreeFeeBanner/ThorFreeFeeBanner.tsx | 18 +++++++++--------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useTradeExecution.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useTradeExecution.tsx index 1d90cba0348..7f27d361811 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useTradeExecution.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useTradeExecution.tsx @@ -30,7 +30,7 @@ import { assertUnreachable } from 'lib/utils' import { assertGetCosmosSdkChainAdapter } from 'lib/utils/cosmosSdk' import { assertGetEvmChainAdapter, signAndBroadcast } from 'lib/utils/evm' import { assertGetUtxoChainAdapter } from 'lib/utils/utxo' -import { selectThorVotingPower, selectVotingPower } from 'state/apis/snapshot/selectors' +import { selectThorVotingPower } from 'state/apis/snapshot/selectors' import { selectAssetById, selectPortfolioAccountMetadataByAccountId } from 'state/slices/selectors' import { selectActiveQuote, @@ -409,6 +409,7 @@ export const useTradeExecution = ( supportedBuyAsset, slippageTolerancePercentageDecimal, permit2.permit2Signature, + thorVotingPower, ]) return executeTrade diff --git a/src/components/ThorFreeFeeBanner/ThorFreeFeeBanner.tsx b/src/components/ThorFreeFeeBanner/ThorFreeFeeBanner.tsx index 0f8be7a2ff0..2745f7f5ccb 100644 --- a/src/components/ThorFreeFeeBanner/ThorFreeFeeBanner.tsx +++ b/src/components/ThorFreeFeeBanner/ThorFreeFeeBanner.tsx @@ -1,12 +1,12 @@ -import { Box, Card, CardBody, Link } from '@chakra-ui/react' +import { Box, Card, CardBody } from '@chakra-ui/react' import { Text } from 'components/Text' import icons from './asset-icons.png' -const cardHover = { - textDecoration: 'none', - opacity: '.7', -} +// const cardHover = { +// textDecoration: 'none', +// opacity: '.7', +// } const cardBefore = { content: "''", @@ -26,12 +26,12 @@ const cardBefore = { export const ThorFreeFeeBanner = () => { return ( { - + {/* */} From 30b7cb926fe202f7e1e9e44fd5a3616439f2f4f9 Mon Sep 17 00:00:00 2001 From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com> Date: Wed, 30 Oct 2024 22:01:34 +0100 Subject: [PATCH 06/23] feat: free fees for thor holders before tip 014 --- .../components/SharedTradeInput/SharedTradeInput.tsx | 2 +- src/lib/fees/model.ts | 10 ---------- src/state/apis/snapshot/snapshot.ts | 4 ++-- 3 files changed, 3 insertions(+), 13 deletions(-) diff --git a/src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInput.tsx b/src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInput.tsx index 6be561c643d..a8a88badb96 100644 --- a/src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInput.tsx +++ b/src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInput.tsx @@ -45,7 +45,7 @@ export const SharedTradeInput: React.FC = ({ justifyContent='center' maxWidth={isCompact || isSmallerThanXl ? '500px' : undefined} > -
+
Date: Wed, 30 Oct 2024 22:05:53 +0100 Subject: [PATCH 07/23] feat: free fees for thor holders before tip 014 --- src/lib/fees/parameters/index.ts | 4 ++-- src/lib/fees/parameters/thor.ts | 19 +++++++++++++++++++ src/lib/fees/parameters/thorchainLp.ts | 1 + 3 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 src/lib/fees/parameters/thor.ts diff --git a/src/lib/fees/parameters/index.ts b/src/lib/fees/parameters/index.ts index c51ff4dfcc5..341a01e1e62 100644 --- a/src/lib/fees/parameters/index.ts +++ b/src/lib/fees/parameters/index.ts @@ -1,12 +1,12 @@ import { swapperParameters } from './swapper' +import { thorParameters } from './thor' import { thorchainLpParameters } from './thorchainLp' import type { FeeCurveParameters, ParameterModel } from './types' export const FEE_CURVE_PARAMETERS: Record = { SWAPPER: swapperParameters, THORCHAIN_LP: thorchainLpParameters, - // THOR asset doesn't have any fee models for now as it's not using curves - THORSWAP: {} as FeeCurveParameters, + THORSWAP: thorParameters, } export const FEE_MODEL_TO_FEATURE_NAME: Record = { diff --git a/src/lib/fees/parameters/thor.ts b/src/lib/fees/parameters/thor.ts new file mode 100644 index 00000000000..55bfce92ff0 --- /dev/null +++ b/src/lib/fees/parameters/thor.ts @@ -0,0 +1,19 @@ +import type { FeeCurveParameters } from './types' + +const FEE_CURVE_MAX_FEE_BPS = 0 // basis points +const FEE_CURVE_MIN_FEE_BPS = 0 // basis points +const FEE_CURVE_NO_FEE_THRESHOLD_USD = 0 // usd +const FEE_CURVE_FOX_MAX_DISCOUNT_THRESHOLD = 0 // fox +const FEE_CURVE_MIDPOINT_USD = 0 // usd +const FEE_CURVE_STEEPNESS_K = 0 // unitless +const FEE_CURVE_FOX_DISCOUNT_DELAY_HOURS = 0 // 7 for thorchain LP, see https://discord.com/channels/554694662431178782/920449863408287855/1207415430025580627 + +export const thorParameters: FeeCurveParameters = { + FEE_CURVE_MAX_FEE_BPS, + FEE_CURVE_MIN_FEE_BPS, + FEE_CURVE_NO_FEE_THRESHOLD_USD, + FEE_CURVE_FOX_MAX_DISCOUNT_THRESHOLD, + FEE_CURVE_MIDPOINT_USD, + FEE_CURVE_STEEPNESS_K, + FEE_CURVE_FOX_DISCOUNT_DELAY_HOURS, +} diff --git a/src/lib/fees/parameters/thorchainLp.ts b/src/lib/fees/parameters/thorchainLp.ts index 759fee4bf51..709be981ca3 100644 --- a/src/lib/fees/parameters/thorchainLp.ts +++ b/src/lib/fees/parameters/thorchainLp.ts @@ -1,5 +1,6 @@ import type { FeeCurveParameters } from './types' +// THOR asset doesn't have any fee models for now as it's not using curves, we only zero out values to make TS happy with less risks const FEE_CURVE_MAX_FEE_BPS = 50 // basis points const FEE_CURVE_MIN_FEE_BPS = 20 // basis points const FEE_CURVE_NO_FEE_THRESHOLD_USD = 2_000 // usd From a12cff36e1f05069d0cfb3f31636704ac02ea784 Mon Sep 17 00:00:00 2001 From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com> Date: Wed, 30 Oct 2024 22:09:09 +0100 Subject: [PATCH 08/23] feat: free fees for thor holders before tip 014 --- .../MultiHopTradeConfirm/hooks/useTradeExecution.tsx | 7 +++++-- src/lib/fees/model.ts | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useTradeExecution.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useTradeExecution.tsx index 7f27d361811..aa74d7dfe1a 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useTradeExecution.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useTradeExecution.tsx @@ -23,7 +23,7 @@ import { useTranslate } from 'react-polyglot' import { useErrorHandler } from 'hooks/useErrorToast/useErrorToast' import { useWallet } from 'hooks/useWallet/useWallet' import { bnOrZero } from 'lib/bignumber/bignumber' -import { THORSWAP_UNIT_THRESHOLD } from 'lib/fees/model' +import { THORSWAP_MAXIMUM_YEAR_TRESHOLD, THORSWAP_UNIT_THRESHOLD } from 'lib/fees/model' import { MixPanelEvent } from 'lib/mixpanel/types' import { TradeExecution } from 'lib/tradeExecution' import { assertUnreachable } from 'lib/utils' @@ -216,7 +216,10 @@ export const useTradeExecution = ( trackMixpanelEvent(MixPanelEvent.TradeSuccess) hasMixpanelSuccessOrFailFiredRef.current = true - if (bnOrZero(thorVotingPower).toNumber() >= THORSWAP_UNIT_THRESHOLD) { + if ( + bnOrZero(thorVotingPower).toNumber() >= THORSWAP_UNIT_THRESHOLD && + new Date().getUTCFullYear() < THORSWAP_MAXIMUM_YEAR_TRESHOLD + ) { trackMixpanelEvent(MixPanelEvent.ThorDiscountTradeSuccess) } } diff --git a/src/lib/fees/model.ts b/src/lib/fees/model.ts index 10bb2e888d3..3d5107daaba 100644 --- a/src/lib/fees/model.ts +++ b/src/lib/fees/model.ts @@ -8,7 +8,7 @@ import { FEE_CURVE_PARAMETERS } from './parameters' import type { ParameterModel } from './parameters/types' export const THORSWAP_UNIT_THRESHOLD = 1 -const THORSWAP_MAXIMUM_YEAR_TRESHOLD = 2025 +export const THORSWAP_MAXIMUM_YEAR_TRESHOLD = 2025 type CalculateFeeBpsArgs = { tradeAmountUsd: BigNumber From 3969eb73a27ecfb4073f3820a983c494efe2c204 Mon Sep 17 00:00:00 2001 From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com> Date: Wed, 30 Oct 2024 22:33:33 +0100 Subject: [PATCH 09/23] feat: proposal link --- src/state/apis/snapshot/snapshot.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/state/apis/snapshot/snapshot.ts b/src/state/apis/snapshot/snapshot.ts index 1b7d55985f2..cdaa97eb25d 100644 --- a/src/state/apis/snapshot/snapshot.ts +++ b/src/state/apis/snapshot/snapshot.ts @@ -20,6 +20,7 @@ type FoxVotingPowerCryptoBalance = string const SNAPSHOT_SPACE = 'shapeshiftdao.eth' const THORSWAP_SNAPSHOT_SPACE = 'thorswapcommunity.eth' +// https://snapshot.org/#/thorswapcommunity.eth/proposal/0x66a6c22cd2f4d88713bd4b4fd9068dfa35fee2fce94bb76fe274b8602cee556d const THOR_TIP_014_BLOCK_NUMBER = 21072340 export const initialState: SnapshotState = { From 68096ca8f6852c390173b26034510d54e6b72296 Mon Sep 17 00:00:00 2001 From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com> Date: Wed, 30 Oct 2024 22:35:44 +0100 Subject: [PATCH 10/23] feat: todo --- src/components/ThorFreeFeeBanner/ThorFreeFeeBanner.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/ThorFreeFeeBanner/ThorFreeFeeBanner.tsx b/src/components/ThorFreeFeeBanner/ThorFreeFeeBanner.tsx index 2745f7f5ccb..9336422a0c5 100644 --- a/src/components/ThorFreeFeeBanner/ThorFreeFeeBanner.tsx +++ b/src/components/ThorFreeFeeBanner/ThorFreeFeeBanner.tsx @@ -3,6 +3,8 @@ import { Text } from 'components/Text' import icons from './asset-icons.png' +// @TODO: uncomment all the comments when implementing the external link to explain why we do this + // const cardHover = { // textDecoration: 'none', // opacity: '.7', From 3ae7cc687304721da8a65d8b8a8c60ba663fdada Mon Sep 17 00:00:00 2001 From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com> Date: Thu, 31 Oct 2024 04:22:30 +0100 Subject: [PATCH 11/23] feat: promise fullfilled --- src/state/apis/snapshot/snapshot.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/state/apis/snapshot/snapshot.ts b/src/state/apis/snapshot/snapshot.ts index cdaa97eb25d..b77c2d513fb 100644 --- a/src/state/apis/snapshot/snapshot.ts +++ b/src/state/apis/snapshot/snapshot.ts @@ -8,6 +8,7 @@ import { BigNumber, bn, bnOrZero } from 'lib/bignumber/bignumber' import { FEE_CURVE_PARAMETERS } from 'lib/fees/parameters' import type { ParameterModel } from 'lib/fees/parameters/types' import { findClosestFoxDiscountDelayBlockNumber } from 'lib/fees/utils' +import { isFulfilled } from 'lib/utils' import type { ReduxState } from 'state/reducer' import { BASE_RTK_CREATE_API_CONFIG } from '../const' @@ -164,7 +165,7 @@ export const snapshotApi = createApi({ FEE_CURVE_PARAMETERS[model].FEE_CURVE_FOX_DISCOUNT_DELAY_HOURS, ) const delegation = false // don't let people delegate for discounts - ambiguous in spec - const votingPowerResults = await Promise.all( + const votingPowerResults = await Promise.allSettled( evmAddresses.map(async address => { const votingPowerUnvalidated = await getVotingPower( address, @@ -178,7 +179,9 @@ export const snapshotApi = createApi({ return bnOrZero(VotingPowerSchema.parse(votingPowerUnvalidated).vp) }), ) - const foxHeld = BigNumber.sum(...votingPowerResults).toNumber() + const foxHeld = BigNumber.sum( + ...votingPowerResults.filter(isFulfilled).map(r => r.value), + ).toNumber() // Return an error tuple in case of an invalid foxHeld value so we don't cache an errored value if (isNaN(foxHeld)) { @@ -221,7 +224,7 @@ export const snapshotApi = createApi({ }, new Set()), ) const delegation = false // don't let people delegate for discounts - ambiguous in spec - const votingPowerResults = await Promise.all( + const votingPowerResults = await Promise.allSettled( evmAddresses.map(async address => { const votingPowerUnvalidated = await getVotingPower( address, @@ -235,7 +238,9 @@ export const snapshotApi = createApi({ return bnOrZero(VotingPowerSchema.parse(votingPowerUnvalidated).vp) }), ) - const thorHeld = BigNumber.sum(...votingPowerResults).toNumber() + const thorHeld = BigNumber.sum( + ...votingPowerResults.filter(isFulfilled).map(r => r.value), + ).toNumber() // Return an error tuple in case of an invalid foxHeld value so we don't cache an errored value if (isNaN(thorHeld)) { From 3162c9df8ee15ca5bd959c8545742973f6488e01 Mon Sep 17 00:00:00 2001 From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com> Date: Thu, 31 Oct 2024 15:31:01 +0100 Subject: [PATCH 12/23] fix: throttle snapshot requests --- src/state/apis/snapshot/snapshot.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/state/apis/snapshot/snapshot.ts b/src/state/apis/snapshot/snapshot.ts index b77c2d513fb..316f5ef6bc3 100644 --- a/src/state/apis/snapshot/snapshot.ts +++ b/src/state/apis/snapshot/snapshot.ts @@ -2,6 +2,7 @@ import { createSlice } from '@reduxjs/toolkit' import { createApi } from '@reduxjs/toolkit/dist/query/react' import { type AccountId, fromAccountId } from '@shapeshiftoss/caip' import { isEvmChainId } from '@shapeshiftoss/chain-adapters' +import { createThrottle } from '@shapeshiftoss/utils' import axios from 'axios' import { PURGE } from 'redux-persist' import { BigNumber, bn, bnOrZero } from 'lib/bignumber/bignumber' @@ -24,6 +25,16 @@ const THORSWAP_SNAPSHOT_SPACE = 'thorswapcommunity.eth' // https://snapshot.org/#/thorswapcommunity.eth/proposal/0x66a6c22cd2f4d88713bd4b4fd9068dfa35fee2fce94bb76fe274b8602cee556d const THOR_TIP_014_BLOCK_NUMBER = 21072340 +// As per https://docs.snapshot.org/tools/api/api-keys#limits rate limit should be 100 +// but sometime the request fail randomly making it hard to know how much requests we can send +// dividing this limit by 2 seems sane enough assuming it's already a edge case +const { throttle, clear } = createThrottle({ + capacity: 50, + costPerReq: 1, + drainPerInterval: 50, + intervalMs: 60_000, // 1mn +}) + export const initialState: SnapshotState = { votingPowerByModel: { SWAPPER: undefined, @@ -92,6 +103,7 @@ export const snapshotApi = createApi({ } } ` + await throttle() // https://hub.snapshot.org/graphql?query=query%20%7B%0A%20%20space(id%3A%20%22shapeshiftdao.eth%22)%20%7B%0A%20%20%20%20strategies%20%7B%0A%20%20%20%20%20%20name%0A%20%20%20%20%20%20network%0A%20%20%20%20%20%20params%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D const { data: resData } = await axios.post( 'https://hub.snapshot.org/graphql', @@ -105,6 +117,8 @@ export const snapshotApi = createApi({ } catch (e) { console.error('snapshotApi getStrategies', e) return { data: [] } + } finally { + clear() } }, }), @@ -122,6 +136,7 @@ export const snapshotApi = createApi({ } } ` + await throttle() // https://hub.snapshot.org/graphql?query=query%20%7B%0A%20%20space(id%3A%20%22shapeshiftdao.eth%22)%20%7B%0A%20%20%20%20strategies%20%7B%0A%20%20%20%20%20%20name%0A%20%20%20%20%20%20network%0A%20%20%20%20%20%20params%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D const { data: resData } = await axios.post( 'https://hub.snapshot.org/graphql', @@ -135,6 +150,8 @@ export const snapshotApi = createApi({ } catch (e) { console.error('snapshotApi getStrategies', e) return { data: [] } + } finally { + clear() } }, }), @@ -167,6 +184,7 @@ export const snapshotApi = createApi({ const delegation = false // don't let people delegate for discounts - ambiguous in spec const votingPowerResults = await Promise.allSettled( evmAddresses.map(async address => { + await throttle() const votingPowerUnvalidated = await getVotingPower( address, '1', @@ -190,11 +208,14 @@ export const snapshotApi = createApi({ } dispatch(snapshot.actions.setVotingPower({ foxHeld: foxHeld.toString(), model })) + return { data: foxHeld.toString() } } catch (e) { console.error(e) return { error: { data: e, status: 400 } } + } finally { + clear() } }, }), @@ -226,6 +247,7 @@ export const snapshotApi = createApi({ const delegation = false // don't let people delegate for discounts - ambiguous in spec const votingPowerResults = await Promise.allSettled( evmAddresses.map(async address => { + await throttle() const votingPowerUnvalidated = await getVotingPower( address, '1', @@ -254,6 +276,8 @@ export const snapshotApi = createApi({ console.error(e) return { error: { data: e, status: 400 } } + } finally { + clear() } }, }), @@ -300,6 +324,7 @@ export const snapshotApi = createApi({ } } ` + await throttle() const { data: resData } = await axios.post( 'https://hub.snapshot.org/graphql', { query }, @@ -312,6 +337,8 @@ export const snapshotApi = createApi({ } catch (e) { console.error('snapshotApi getProposals', e) return { data: { activeProposals: [], closedProposals: [] } } + } finally { + clear() } }, }), From abba2aa8b9655504957d84f435dc5946408bee1a Mon Sep 17 00:00:00 2001 From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com> Date: Thu, 31 Oct 2024 16:01:46 +0100 Subject: [PATCH 13/23] fix: throttle snapshot requests --- src/state/apis/snapshot/getVotingPower.ts | 8 ++++ src/state/apis/snapshot/snapshot.ts | 55 ++++++++++++++++++++--- 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/src/state/apis/snapshot/getVotingPower.ts b/src/state/apis/snapshot/getVotingPower.ts index bdf08d96736..773afa6c33e 100644 --- a/src/state/apis/snapshot/getVotingPower.ts +++ b/src/state/apis/snapshot/getVotingPower.ts @@ -10,6 +10,7 @@ export const getVotingPower = async ( snapshot: number | 'latest', space: string, delegation: boolean, + throttle: () => Promise, ) => { const axiosWithRetry = axios.create() @@ -18,6 +19,11 @@ export const getVotingPower = async ( shouldResetTimeout: true, // We absolutely want to retry on any error or users will pay fees for nothing if we can't fetch its vote power retryCondition: _error => true, + onRetry: async () => { + await throttle() + + return + }, }) const headers = { @@ -37,6 +43,8 @@ export const getVotingPower = async ( }, } + await throttle() + const { data } = await axiosWithRetry.post('https://score.snapshot.org', body, { headers, }) diff --git a/src/state/apis/snapshot/snapshot.ts b/src/state/apis/snapshot/snapshot.ts index 316f5ef6bc3..045d867f1a4 100644 --- a/src/state/apis/snapshot/snapshot.ts +++ b/src/state/apis/snapshot/snapshot.ts @@ -29,10 +29,10 @@ const THOR_TIP_014_BLOCK_NUMBER = 21072340 // but sometime the request fail randomly making it hard to know how much requests we can send // dividing this limit by 2 seems sane enough assuming it's already a edge case const { throttle, clear } = createThrottle({ - capacity: 50, + capacity: 60, costPerReq: 1, - drainPerInterval: 50, - intervalMs: 60_000, // 1mn + drainPerInterval: 100, + intervalMs: 90_000, // 1mn30 }) export const initialState: SnapshotState = { @@ -184,7 +184,6 @@ export const snapshotApi = createApi({ const delegation = false // don't let people delegate for discounts - ambiguous in spec const votingPowerResults = await Promise.allSettled( evmAddresses.map(async address => { - await throttle() const votingPowerUnvalidated = await getVotingPower( address, '1', @@ -192,6 +191,7 @@ export const snapshotApi = createApi({ foxDiscountBlock, SNAPSHOT_SPACE, delegation, + throttle, ) // vp is FOX in crypto balance return bnOrZero(VotingPowerSchema.parse(votingPowerUnvalidated).vp) @@ -247,7 +247,6 @@ export const snapshotApi = createApi({ const delegation = false // don't let people delegate for discounts - ambiguous in spec const votingPowerResults = await Promise.allSettled( evmAddresses.map(async address => { - await throttle() const votingPowerUnvalidated = await getVotingPower( address, '1', @@ -255,6 +254,52 @@ export const snapshotApi = createApi({ THOR_TIP_014_BLOCK_NUMBER, THORSWAP_SNAPSHOT_SPACE, delegation, + throttle, + ) + // vp is THOR in crypto balance + return bnOrZero(VotingPowerSchema.parse(votingPowerUnvalidated).vp) + }), + ) + await Promise.allSettled( + evmAddresses.map(async address => { + const votingPowerUnvalidated = await getVotingPower( + address, + '1', + strategies, + THOR_TIP_014_BLOCK_NUMBER, + THORSWAP_SNAPSHOT_SPACE, + delegation, + throttle, + ) + // vp is THOR in crypto balance + return bnOrZero(VotingPowerSchema.parse(votingPowerUnvalidated).vp) + }), + ) + await Promise.allSettled( + evmAddresses.map(async address => { + const votingPowerUnvalidated = await getVotingPower( + address, + '1', + strategies, + THOR_TIP_014_BLOCK_NUMBER, + THORSWAP_SNAPSHOT_SPACE, + delegation, + throttle, + ) + // vp is THOR in crypto balance + return bnOrZero(VotingPowerSchema.parse(votingPowerUnvalidated).vp) + }), + ) + await Promise.allSettled( + evmAddresses.map(async address => { + const votingPowerUnvalidated = await getVotingPower( + address, + '1', + strategies, + THOR_TIP_014_BLOCK_NUMBER, + THORSWAP_SNAPSHOT_SPACE, + delegation, + throttle, ) // vp is THOR in crypto balance return bnOrZero(VotingPowerSchema.parse(votingPowerUnvalidated).vp) From 7581eaa5493d3991366fafa69725aeb9f21806b8 Mon Sep 17 00:00:00 2001 From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com> Date: Thu, 31 Oct 2024 16:03:51 +0100 Subject: [PATCH 14/23] fix: throttle snapshot requests --- src/state/apis/snapshot/snapshot.ts | 45 ----------------------------- 1 file changed, 45 deletions(-) diff --git a/src/state/apis/snapshot/snapshot.ts b/src/state/apis/snapshot/snapshot.ts index 045d867f1a4..03fff478ddd 100644 --- a/src/state/apis/snapshot/snapshot.ts +++ b/src/state/apis/snapshot/snapshot.ts @@ -260,51 +260,6 @@ export const snapshotApi = createApi({ return bnOrZero(VotingPowerSchema.parse(votingPowerUnvalidated).vp) }), ) - await Promise.allSettled( - evmAddresses.map(async address => { - const votingPowerUnvalidated = await getVotingPower( - address, - '1', - strategies, - THOR_TIP_014_BLOCK_NUMBER, - THORSWAP_SNAPSHOT_SPACE, - delegation, - throttle, - ) - // vp is THOR in crypto balance - return bnOrZero(VotingPowerSchema.parse(votingPowerUnvalidated).vp) - }), - ) - await Promise.allSettled( - evmAddresses.map(async address => { - const votingPowerUnvalidated = await getVotingPower( - address, - '1', - strategies, - THOR_TIP_014_BLOCK_NUMBER, - THORSWAP_SNAPSHOT_SPACE, - delegation, - throttle, - ) - // vp is THOR in crypto balance - return bnOrZero(VotingPowerSchema.parse(votingPowerUnvalidated).vp) - }), - ) - await Promise.allSettled( - evmAddresses.map(async address => { - const votingPowerUnvalidated = await getVotingPower( - address, - '1', - strategies, - THOR_TIP_014_BLOCK_NUMBER, - THORSWAP_SNAPSHOT_SPACE, - delegation, - throttle, - ) - // vp is THOR in crypto balance - return bnOrZero(VotingPowerSchema.parse(votingPowerUnvalidated).vp) - }), - ) const thorHeld = BigNumber.sum( ...votingPowerResults.filter(isFulfilled).map(r => r.value), ).toNumber() From bdeb6fdcc48f66892605a4b86803fa2ba971e82e Mon Sep 17 00:00:00 2001 From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com> Date: Thu, 31 Oct 2024 16:06:23 +0100 Subject: [PATCH 15/23] fix: throttle snapshot requests --- src/state/apis/snapshot/snapshot.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/state/apis/snapshot/snapshot.ts b/src/state/apis/snapshot/snapshot.ts index 03fff478ddd..5e0911a2b44 100644 --- a/src/state/apis/snapshot/snapshot.ts +++ b/src/state/apis/snapshot/snapshot.ts @@ -29,10 +29,10 @@ const THOR_TIP_014_BLOCK_NUMBER = 21072340 // but sometime the request fail randomly making it hard to know how much requests we can send // dividing this limit by 2 seems sane enough assuming it's already a edge case const { throttle, clear } = createThrottle({ - capacity: 60, + capacity: 50, costPerReq: 1, - drainPerInterval: 100, - intervalMs: 90_000, // 1mn30 + drainPerInterval: 50, + intervalMs: 60_000, // 1mn }) export const initialState: SnapshotState = { From cc14d9aefff392e9bbc41920846fa8476e1a5b7a Mon Sep 17 00:00:00 2001 From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com> Date: Thu, 31 Oct 2024 18:14:30 +0100 Subject: [PATCH 16/23] fix: throttle snapshot requests --- src/context/AppProvider/AppContext.tsx | 8 +- src/state/apis/snapshot/getVotingPower.ts | 8 -- src/state/apis/snapshot/snapshot.ts | 125 ++++++++++------------ 3 files changed, 60 insertions(+), 81 deletions(-) diff --git a/src/context/AppProvider/AppContext.tsx b/src/context/AppProvider/AppContext.tsx index f9e6c36b376..07aadc48371 100644 --- a/src/context/AppProvider/AppContext.tsx +++ b/src/context/AppProvider/AppContext.tsx @@ -8,7 +8,6 @@ import React, { useEffect } from 'react' import { useTranslate } from 'react-polyglot' import { useNfts } from 'components/Nfts/hooks/useNfts' import { usePlugins } from 'context/PluginProvider/PluginProvider' -import { useFeatureFlag } from 'hooks/useFeatureFlag/useFeatureFlag' import { useIsSnapInstalled } from 'hooks/useIsSnapInstalled/useIsSnapInstalled' import { useMixpanelPortfolioTracking } from 'hooks/useMixpanelPortfolioTracking/useMixpanelPortfolioTracking' import { useModal } from 'hooks/useModal/useModal' @@ -62,7 +61,6 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { const routeAssetId = useRouteAssetId() const { isSnapInstalled } = useIsSnapInstalled() const { close: closeModal, open: openModal } = useModal('ledgerOpenApp') - const isThorFreeFeesEnabled = useFeatureFlag('ThorFreeFees') useEffect(() => { const handleLedgerOpenApp = ({ chainId, reject }: LedgerOpenAppEventArgs) => { @@ -136,11 +134,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { dispatch( snapshotApi.endpoints.getVotingPower.initiate({ model: 'SWAPPER' }, { forceRefetch: true }), ) - - if (isThorFreeFeesEnabled) { - dispatch(snapshotApi.endpoints.getThorVotingPower.initiate()) - } - }, [dispatch, isConnected, portfolioLoadingStatus, isThorFreeFeesEnabled]) + }, [dispatch, isConnected, portfolioLoadingStatus]) // Resets the sell and buy asset AccountIDs on wallet change to that we don't get stale trade input account selections while we're loading the new wallet useEffect(() => { diff --git a/src/state/apis/snapshot/getVotingPower.ts b/src/state/apis/snapshot/getVotingPower.ts index 773afa6c33e..bdf08d96736 100644 --- a/src/state/apis/snapshot/getVotingPower.ts +++ b/src/state/apis/snapshot/getVotingPower.ts @@ -10,7 +10,6 @@ export const getVotingPower = async ( snapshot: number | 'latest', space: string, delegation: boolean, - throttle: () => Promise, ) => { const axiosWithRetry = axios.create() @@ -19,11 +18,6 @@ export const getVotingPower = async ( shouldResetTimeout: true, // We absolutely want to retry on any error or users will pay fees for nothing if we can't fetch its vote power retryCondition: _error => true, - onRetry: async () => { - await throttle() - - return - }, }) const headers = { @@ -43,8 +37,6 @@ export const getVotingPower = async ( }, } - await throttle() - const { data } = await axiosWithRetry.post('https://score.snapshot.org', body, { headers, }) diff --git a/src/state/apis/snapshot/snapshot.ts b/src/state/apis/snapshot/snapshot.ts index 5e0911a2b44..c5bb739f773 100644 --- a/src/state/apis/snapshot/snapshot.ts +++ b/src/state/apis/snapshot/snapshot.ts @@ -1,9 +1,11 @@ +import type { ThunkDispatch } from '@reduxjs/toolkit' import { createSlice } from '@reduxjs/toolkit' import { createApi } from '@reduxjs/toolkit/dist/query/react' import { type AccountId, fromAccountId } from '@shapeshiftoss/caip' import { isEvmChainId } from '@shapeshiftoss/chain-adapters' import { createThrottle } from '@shapeshiftoss/utils' import axios from 'axios' +import { getConfig } from 'config' import { PURGE } from 'redux-persist' import { BigNumber, bn, bnOrZero } from 'lib/bignumber/bignumber' import { FEE_CURVE_PARAMETERS } from 'lib/fees/parameters' @@ -85,6 +87,59 @@ export const snapshot = createSlice({ extraReducers: builder => builder.addCase(PURGE, () => initialState), }) +const getThorVotingPower = async ( + dispatch: ThunkDispatch, + getState: () => unknown, +) => { + const accountIds: AccountId[] = (getState() as ReduxState).portfolio.accountMetadata.ids ?? [] + const strategies = await (async () => { + const maybeSliceStragies = (getState() as ReduxState).snapshot.thorStrategies + if (maybeSliceStragies) return maybeSliceStragies + + const strategiesResult = await dispatch(snapshotApi.endpoints.getThorStrategies.initiate()) + return strategiesResult?.data + })() + if (!strategies) { + console.error('snapshotApi getThorVotingPower could not get strategies') + return { data: bn(0).toString() } + } + const evmAddresses = Array.from( + accountIds.reduce>((acc, accountId) => { + const { account, chainId } = fromAccountId(accountId) + isEvmChainId(chainId) && acc.add(account) + return acc + }, new Set()), + ) + const delegation = false // don't let people delegate for discounts - ambiguous in spec + const votingPowerResults = await Promise.allSettled( + evmAddresses.map(async address => { + await throttle() + const votingPowerUnvalidated = await getVotingPower( + address, + '1', + strategies, + THOR_TIP_014_BLOCK_NUMBER, + THORSWAP_SNAPSHOT_SPACE, + delegation, + ) + // vp is THOR in crypto balance + return bnOrZero(VotingPowerSchema.parse(votingPowerUnvalidated).vp) + }), + ) + const thorHeld = BigNumber.sum( + ...votingPowerResults.filter(isFulfilled).map(r => r.value), + ).toNumber() + + // Return an error tuple in case of an invalid foxHeld value so we don't cache an errored value + if (isNaN(thorHeld)) { + const data = 'NaN thorHeld value' + return { error: { data, status: 400 } } + } + + dispatch(snapshot.actions.setThorVotingPower(thorHeld.toString())) + return { data: thorHeld.toString() } +} + export const snapshotApi = createApi({ ...BASE_RTK_CREATE_API_CONFIG, reducerPath: 'snapshotApi', @@ -150,8 +205,6 @@ export const snapshotApi = createApi({ } catch (e) { console.error('snapshotApi getStrategies', e) return { data: [] } - } finally { - clear() } }, }), @@ -184,6 +237,7 @@ export const snapshotApi = createApi({ const delegation = false // don't let people delegate for discounts - ambiguous in spec const votingPowerResults = await Promise.allSettled( evmAddresses.map(async address => { + await throttle() const votingPowerUnvalidated = await getVotingPower( address, '1', @@ -191,7 +245,6 @@ export const snapshotApi = createApi({ foxDiscountBlock, SNAPSHOT_SPACE, delegation, - throttle, ) // vp is FOX in crypto balance return bnOrZero(VotingPowerSchema.parse(votingPowerUnvalidated).vp) @@ -209,69 +262,11 @@ export const snapshotApi = createApi({ dispatch(snapshot.actions.setVotingPower({ foxHeld: foxHeld.toString(), model })) - return { data: foxHeld.toString() } - } catch (e) { - console.error(e) - - return { error: { data: e, status: 400 } } - } finally { - clear() - } - }, - }), - getThorVotingPower: build.query({ - queryFn: async (_, { dispatch, getState }) => { - try { - const accountIds: AccountId[] = - (getState() as ReduxState).portfolio.accountMetadata.ids ?? [] - const strategies = await (async () => { - const maybeSliceStragies = (getState() as ReduxState).snapshot.thorStrategies - if (maybeSliceStragies) return maybeSliceStragies - - const strategiesResult = await dispatch( - snapshotApi.endpoints.getThorStrategies.initiate(), - ) - return strategiesResult?.data - })() - if (!strategies) { - console.error('snapshotApi getThorVotingPower could not get strategies') - return { data: bn(0).toString() } - } - const evmAddresses = Array.from( - accountIds.reduce>((acc, accountId) => { - const { account, chainId } = fromAccountId(accountId) - isEvmChainId(chainId) && acc.add(account) - return acc - }, new Set()), - ) - const delegation = false // don't let people delegate for discounts - ambiguous in spec - const votingPowerResults = await Promise.allSettled( - evmAddresses.map(async address => { - const votingPowerUnvalidated = await getVotingPower( - address, - '1', - strategies, - THOR_TIP_014_BLOCK_NUMBER, - THORSWAP_SNAPSHOT_SPACE, - delegation, - throttle, - ) - // vp is THOR in crypto balance - return bnOrZero(VotingPowerSchema.parse(votingPowerUnvalidated).vp) - }), - ) - const thorHeld = BigNumber.sum( - ...votingPowerResults.filter(isFulfilled).map(r => r.value), - ).toNumber() - - // Return an error tuple in case of an invalid foxHeld value so we don't cache an errored value - if (isNaN(thorHeld)) { - const data = 'NaN thorHeld value' - return { error: { data, status: 400 } } + if (getConfig().REACT_APP_FEATURE_THOR_FREE_FEES) { + await getThorVotingPower(dispatch, getState) } - dispatch(snapshot.actions.setThorVotingPower(thorHeld.toString())) - return { data: thorHeld.toString() } + return { data: foxHeld.toString() } } catch (e) { console.error(e) @@ -337,8 +332,6 @@ export const snapshotApi = createApi({ } catch (e) { console.error('snapshotApi getProposals', e) return { data: { activeProposals: [], closedProposals: [] } } - } finally { - clear() } }, }), From 13d190ef5ff44a9da3cfc977712f5750e483932f Mon Sep 17 00:00:00 2001 From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com> Date: Mon, 4 Nov 2024 10:58:13 +0100 Subject: [PATCH 17/23] fix: wouhouuuuu --- src/state/apis/snapshot/getVotingPower.ts | 8 +- src/state/apis/snapshot/snapshot.ts | 94 ++++++++++------------- src/state/apis/snapshot/validators.ts | 11 ++- 3 files changed, 48 insertions(+), 65 deletions(-) diff --git a/src/state/apis/snapshot/getVotingPower.ts b/src/state/apis/snapshot/getVotingPower.ts index bdf08d96736..c40abb654c4 100644 --- a/src/state/apis/snapshot/getVotingPower.ts +++ b/src/state/apis/snapshot/getVotingPower.ts @@ -4,7 +4,7 @@ import axiosRetry from 'axios-retry' import type { Strategy } from './validators' export const getVotingPower = async ( - address: string, + addresses: string[], network: string, strategies: Strategy[], snapshot: number | 'latest', @@ -26,9 +26,9 @@ export const getVotingPower = async ( } const body = { jsonrpc: '2.0', - method: 'get_vp', + method: 'POST', params: { - address, + addresses, network, strategies, snapshot, @@ -37,7 +37,7 @@ export const getVotingPower = async ( }, } - const { data } = await axiosWithRetry.post('https://score.snapshot.org', body, { + const { data } = await axiosWithRetry.post('https://score.snapshot.org/api/scores', body, { headers, }) return data.result diff --git a/src/state/apis/snapshot/snapshot.ts b/src/state/apis/snapshot/snapshot.ts index c5bb739f773..c68af54fb56 100644 --- a/src/state/apis/snapshot/snapshot.ts +++ b/src/state/apis/snapshot/snapshot.ts @@ -3,7 +3,6 @@ import { createSlice } from '@reduxjs/toolkit' import { createApi } from '@reduxjs/toolkit/dist/query/react' import { type AccountId, fromAccountId } from '@shapeshiftoss/caip' import { isEvmChainId } from '@shapeshiftoss/chain-adapters' -import { createThrottle } from '@shapeshiftoss/utils' import axios from 'axios' import { getConfig } from 'config' import { PURGE } from 'redux-persist' @@ -11,13 +10,12 @@ import { BigNumber, bn, bnOrZero } from 'lib/bignumber/bignumber' import { FEE_CURVE_PARAMETERS } from 'lib/fees/parameters' import type { ParameterModel } from 'lib/fees/parameters/types' import { findClosestFoxDiscountDelayBlockNumber } from 'lib/fees/utils' -import { isFulfilled } from 'lib/utils' import type { ReduxState } from 'state/reducer' import { BASE_RTK_CREATE_API_CONFIG } from '../const' import { getVotingPower } from './getVotingPower' import type { Proposal, Strategy } from './validators' -import { ProposalSchema, SnapshotSchema, VotingPowerSchema } from './validators' +import { ProposalSchema, ScoresSchema, SnapshotSchema } from './validators' type FoxVotingPowerCryptoBalance = string @@ -27,16 +25,6 @@ const THORSWAP_SNAPSHOT_SPACE = 'thorswapcommunity.eth' // https://snapshot.org/#/thorswapcommunity.eth/proposal/0x66a6c22cd2f4d88713bd4b4fd9068dfa35fee2fce94bb76fe274b8602cee556d const THOR_TIP_014_BLOCK_NUMBER = 21072340 -// As per https://docs.snapshot.org/tools/api/api-keys#limits rate limit should be 100 -// but sometime the request fail randomly making it hard to know how much requests we can send -// dividing this limit by 2 seems sane enough assuming it's already a edge case -const { throttle, clear } = createThrottle({ - capacity: 50, - costPerReq: 1, - drainPerInterval: 50, - intervalMs: 60_000, // 1mn -}) - export const initialState: SnapshotState = { votingPowerByModel: { SWAPPER: undefined, @@ -111,26 +99,26 @@ const getThorVotingPower = async ( }, new Set()), ) const delegation = false // don't let people delegate for discounts - ambiguous in spec - const votingPowerResults = await Promise.allSettled( - evmAddresses.map(async address => { - await throttle() - const votingPowerUnvalidated = await getVotingPower( - address, - '1', - strategies, - THOR_TIP_014_BLOCK_NUMBER, - THORSWAP_SNAPSHOT_SPACE, - delegation, - ) - // vp is THOR in crypto balance - return bnOrZero(VotingPowerSchema.parse(votingPowerUnvalidated).vp) - }), + const votingPowerResults = await getVotingPower( + evmAddresses, + '1', + strategies, + THOR_TIP_014_BLOCK_NUMBER, + THORSWAP_SNAPSHOT_SPACE, + delegation, ) - const thorHeld = BigNumber.sum( - ...votingPowerResults.filter(isFulfilled).map(r => r.value), - ).toNumber() - // Return an error tuple in case of an invalid foxHeld value so we don't cache an errored value + const scores = ScoresSchema.parse(votingPowerResults).scores + + const thorHeld = scores.reduce((acc: number, scoreByAddress: Record) => { + const values = Object.values(scoreByAddress) + + if (!values.length) return acc + + return acc + BigNumber.sum(...values.map(bnOrZero)).toNumber() + }, 0) + + // Return an error tuple in case of an invalid thorHeld value so we don't cache an errored value if (isNaN(thorHeld)) { const data = 'NaN thorHeld value' return { error: { data, status: 400 } } @@ -158,7 +146,6 @@ export const snapshotApi = createApi({ } } ` - await throttle() // https://hub.snapshot.org/graphql?query=query%20%7B%0A%20%20space(id%3A%20%22shapeshiftdao.eth%22)%20%7B%0A%20%20%20%20strategies%20%7B%0A%20%20%20%20%20%20name%0A%20%20%20%20%20%20network%0A%20%20%20%20%20%20params%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D const { data: resData } = await axios.post( 'https://hub.snapshot.org/graphql', @@ -172,8 +159,6 @@ export const snapshotApi = createApi({ } catch (e) { console.error('snapshotApi getStrategies', e) return { data: [] } - } finally { - clear() } }, }), @@ -191,7 +176,6 @@ export const snapshotApi = createApi({ } } ` - await throttle() // https://hub.snapshot.org/graphql?query=query%20%7B%0A%20%20space(id%3A%20%22shapeshiftdao.eth%22)%20%7B%0A%20%20%20%20strategies%20%7B%0A%20%20%20%20%20%20name%0A%20%20%20%20%20%20network%0A%20%20%20%20%20%20params%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D const { data: resData } = await axios.post( 'https://hub.snapshot.org/graphql', @@ -235,24 +219,27 @@ export const snapshotApi = createApi({ FEE_CURVE_PARAMETERS[model].FEE_CURVE_FOX_DISCOUNT_DELAY_HOURS, ) const delegation = false // don't let people delegate for discounts - ambiguous in spec - const votingPowerResults = await Promise.allSettled( - evmAddresses.map(async address => { - await throttle() - const votingPowerUnvalidated = await getVotingPower( - address, - '1', - strategies, - foxDiscountBlock, - SNAPSHOT_SPACE, - delegation, - ) - // vp is FOX in crypto balance - return bnOrZero(VotingPowerSchema.parse(votingPowerUnvalidated).vp) - }), + + const votingPowerResults = await getVotingPower( + evmAddresses, + '1', + strategies, + foxDiscountBlock, + SNAPSHOT_SPACE, + delegation, ) - const foxHeld = BigNumber.sum( - ...votingPowerResults.filter(isFulfilled).map(r => r.value), - ).toNumber() + + const scores = ScoresSchema.parse(votingPowerResults).scores + + console.log(scores, ScoresSchema.parse(votingPowerResults)) + + const foxHeld = scores.reduce((acc: number, scoreByAddress: Record) => { + const values = Object.values(scoreByAddress) + + if (!values.length) return acc + + return acc + BigNumber.sum(...values.map(bnOrZero)).toNumber() + }, 0) // Return an error tuple in case of an invalid foxHeld value so we don't cache an errored value if (isNaN(foxHeld)) { @@ -271,8 +258,6 @@ export const snapshotApi = createApi({ console.error(e) return { error: { data: e, status: 400 } } - } finally { - clear() } }, }), @@ -319,7 +304,6 @@ export const snapshotApi = createApi({ } } ` - await throttle() const { data: resData } = await axios.post( 'https://hub.snapshot.org/graphql', { query }, diff --git a/src/state/apis/snapshot/validators.ts b/src/state/apis/snapshot/validators.ts index 22e2c5677ef..8e5db0c110e 100644 --- a/src/state/apis/snapshot/validators.ts +++ b/src/state/apis/snapshot/validators.ts @@ -1,5 +1,5 @@ import type { Infer } from 'myzod' -import { array, boolean, keySignature, number, object, string, unknown } from 'myzod' +import { array, boolean, keySignature, number, object, record, string, unknown } from 'myzod' const MethodABI = object({ name: string(), @@ -80,13 +80,12 @@ export type SnapshotStrategies = Infer // ###### voting power -export const VotingPowerSchema = object({ - vp: number(), - vp_by_strategy: array(number()), - vp_state: string(), +export const ScoresSchema = object({ + state: string(), + scores: array(record(number())), }) -export type VotingPower = Infer +export type VotingPower = Infer // ###### proposals type From 7f0a42df1bc7e49411aa4d505096b9be967eb30e Mon Sep 17 00:00:00 2001 From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com> Date: Mon, 4 Nov 2024 11:02:06 +0100 Subject: [PATCH 18/23] fix: wouhouuuuu --- src/state/apis/snapshot/snapshot.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/state/apis/snapshot/snapshot.ts b/src/state/apis/snapshot/snapshot.ts index c68af54fb56..6242f527189 100644 --- a/src/state/apis/snapshot/snapshot.ts +++ b/src/state/apis/snapshot/snapshot.ts @@ -231,8 +231,6 @@ export const snapshotApi = createApi({ const scores = ScoresSchema.parse(votingPowerResults).scores - console.log(scores, ScoresSchema.parse(votingPowerResults)) - const foxHeld = scores.reduce((acc: number, scoreByAddress: Record) => { const values = Object.values(scoreByAddress) From f33a793092088bbe884c018b285c1ee29c7e57cf Mon Sep 17 00:00:00 2001 From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com> Date: Mon, 4 Nov 2024 11:06:13 +0100 Subject: [PATCH 19/23] fix: wouhouuuuu --- src/state/apis/snapshot/snapshot.ts | 91 ++++++----------------------- 1 file changed, 18 insertions(+), 73 deletions(-) diff --git a/src/state/apis/snapshot/snapshot.ts b/src/state/apis/snapshot/snapshot.ts index 244107eac6f..fcf20c6759d 100644 --- a/src/state/apis/snapshot/snapshot.ts +++ b/src/state/apis/snapshot/snapshot.ts @@ -76,59 +76,6 @@ export const snapshot = createSlice({ extraReducers: builder => builder.addCase(PURGE, () => initialState), }) -const getThorVotingPower = async ( - dispatch: ThunkDispatch, - getState: () => unknown, -) => { - const accountIds: AccountId[] = (getState() as ReduxState).portfolio.accountMetadata.ids ?? [] - const strategies = await (async () => { - const maybeSliceStragies = (getState() as ReduxState).snapshot.thorStrategies - if (maybeSliceStragies) return maybeSliceStragies - - const strategiesResult = await dispatch(snapshotApi.endpoints.getThorStrategies.initiate()) - return strategiesResult?.data - })() - if (!strategies) { - console.error('snapshotApi getThorVotingPower could not get strategies') - return { data: bn(0).toString() } - } - const evmAddresses = Array.from( - accountIds.reduce>((acc, accountId) => { - const { account, chainId } = fromAccountId(accountId) - isEvmChainId(chainId) && acc.add(account) - return acc - }, new Set()), - ) - const delegation = false // don't let people delegate for discounts - ambiguous in spec - const votingPowerResults = await getVotingPower( - evmAddresses, - '1', - strategies, - THOR_TIP_014_BLOCK_NUMBER, - THORSWAP_SNAPSHOT_SPACE, - delegation, - ) - - const scores = ScoresSchema.parse(votingPowerResults).scores - - const thorHeld = scores.reduce((acc: number, scoreByAddress: Record) => { - const values = Object.values(scoreByAddress) - - if (!values.length) return acc - - return acc + BigNumber.sum(...values.map(bnOrZero)).toNumber() - }, 0) - - // Return an error tuple in case of an invalid thorHeld value so we don't cache an errored value - if (isNaN(thorHeld)) { - const data = 'NaN thorHeld value' - return { error: { data, status: 400 } } - } - - dispatch(snapshot.actions.setThorVotingPower(thorHeld.toString())) - return { data: thorHeld.toString() } -} - export const snapshotApi = createApi({ ...BASE_RTK_CREATE_API_CONFIG, reducerPath: 'snapshotApi', @@ -248,10 +195,6 @@ export const snapshotApi = createApi({ dispatch(snapshot.actions.setVotingPower({ foxHeld: foxHeld.toString(), model })) - if (getConfig().REACT_APP_FEATURE_THOR_FREE_FEES) { - await getThorVotingPower(dispatch, getState) - } - return { data: foxHeld.toString() } } catch (e) { console.error(e) @@ -286,23 +229,25 @@ export const snapshotApi = createApi({ }, new Set()), ) const delegation = false // don't let people delegate for discounts - ambiguous in spec - const votingPowerResults = await Promise.allSettled( - evmAddresses.map(async address => { - const votingPowerUnvalidated = await getVotingPower( - address, - '1', - strategies, - THOR_TIP_014_BLOCK_NUMBER, - THORSWAP_SNAPSHOT_SPACE, - delegation, - ) - // vp is THOR in crypto balance - return bnOrZero(VotingPowerSchema.parse(votingPowerUnvalidated).vp) - }), + + const votingPowerResults = await getVotingPower( + evmAddresses, + '1', + strategies, + THOR_TIP_014_BLOCK_NUMBER, + THORSWAP_SNAPSHOT_SPACE, + delegation, ) - const thorHeld = BigNumber.sum( - ...votingPowerResults.filter(isFulfilled).map(r => r.value), - ).toNumber() + + const scores = ScoresSchema.parse(votingPowerResults).scores + + const thorHeld = scores.reduce((acc: number, scoreByAddress: Record) => { + const values = Object.values(scoreByAddress) + + if (!values.length) return acc + + return acc + BigNumber.sum(...values.map(bnOrZero)).toNumber() + }, 0) // Return an error tuple in case of an invalid thorHeld value so we don't cache an errored value if (isNaN(thorHeld)) { From 77530efda3da781b384f2a682a397b56cc72d84b Mon Sep 17 00:00:00 2001 From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com> Date: Mon, 4 Nov 2024 11:14:11 +0100 Subject: [PATCH 20/23] fix: wouhouuuuu --- src/state/apis/snapshot/snapshot.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/state/apis/snapshot/snapshot.ts b/src/state/apis/snapshot/snapshot.ts index fcf20c6759d..e2d03f2ce9a 100644 --- a/src/state/apis/snapshot/snapshot.ts +++ b/src/state/apis/snapshot/snapshot.ts @@ -1,16 +1,13 @@ -import type { ThunkDispatch } from '@reduxjs/toolkit' import { createSlice } from '@reduxjs/toolkit' import { createApi } from '@reduxjs/toolkit/dist/query/react' import { type AccountId, fromAccountId } from '@shapeshiftoss/caip' import { isEvmChainId } from '@shapeshiftoss/chain-adapters' import axios from 'axios' -import { getConfig } from 'config' import { PURGE } from 'redux-persist' import { BigNumber, bn, bnOrZero } from 'lib/bignumber/bignumber' import { FEE_CURVE_PARAMETERS } from 'lib/fees/parameters' import type { ParameterModel } from 'lib/fees/parameters/types' import { findClosestFoxDiscountDelayBlockNumber } from 'lib/fees/utils' -import { isFulfilled } from 'lib/utils' import type { ReduxState } from 'state/reducer' import { BASE_RTK_CREATE_API_CONFIG } from '../const' From 11ddab3fa7adcee7939f8b0c6b1d4df34827572a Mon Sep 17 00:00:00 2001 From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com> Date: Mon, 4 Nov 2024 22:35:19 +0100 Subject: [PATCH 21/23] fix: review feedbacks --- src/lib/fees/parameters/thorchainLp.ts | 1 - src/state/apis/snapshot/getVotingPower.ts | 2 -- 2 files changed, 3 deletions(-) diff --git a/src/lib/fees/parameters/thorchainLp.ts b/src/lib/fees/parameters/thorchainLp.ts index 709be981ca3..759fee4bf51 100644 --- a/src/lib/fees/parameters/thorchainLp.ts +++ b/src/lib/fees/parameters/thorchainLp.ts @@ -1,6 +1,5 @@ import type { FeeCurveParameters } from './types' -// THOR asset doesn't have any fee models for now as it's not using curves, we only zero out values to make TS happy with less risks const FEE_CURVE_MAX_FEE_BPS = 50 // basis points const FEE_CURVE_MIN_FEE_BPS = 20 // basis points const FEE_CURVE_NO_FEE_THRESHOLD_USD = 2_000 // usd diff --git a/src/state/apis/snapshot/getVotingPower.ts b/src/state/apis/snapshot/getVotingPower.ts index c40abb654c4..72d5497b660 100644 --- a/src/state/apis/snapshot/getVotingPower.ts +++ b/src/state/apis/snapshot/getVotingPower.ts @@ -25,8 +25,6 @@ export const getVotingPower = async ( 'Content-Type': 'application/json', } const body = { - jsonrpc: '2.0', - method: 'POST', params: { addresses, network, From 0275becb4da42956c26e33f854a40e300206058a Mon Sep 17 00:00:00 2001 From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com> Date: Mon, 4 Nov 2024 23:18:48 +0100 Subject: [PATCH 22/23] fix: review feedbacks --- src/state/apis/snapshot/getVotingPower.ts | 7 ++-- src/state/apis/snapshot/snapshot.ts | 44 +++++++++-------------- 2 files changed, 22 insertions(+), 29 deletions(-) diff --git a/src/state/apis/snapshot/getVotingPower.ts b/src/state/apis/snapshot/getVotingPower.ts index 72d5497b660..f9716829d3d 100644 --- a/src/state/apis/snapshot/getVotingPower.ts +++ b/src/state/apis/snapshot/getVotingPower.ts @@ -1,7 +1,7 @@ import axios from 'axios' import axiosRetry from 'axios-retry' -import type { Strategy } from './validators' +import { ScoresSchema, type Strategy } from './validators' export const getVotingPower = async ( addresses: string[], @@ -38,5 +38,8 @@ export const getVotingPower = async ( const { data } = await axiosWithRetry.post('https://score.snapshot.org/api/scores', body, { headers, }) - return data.result + + const scores = ScoresSchema.parse(data.result).scores + + return scores } diff --git a/src/state/apis/snapshot/snapshot.ts b/src/state/apis/snapshot/snapshot.ts index e2d03f2ce9a..a0392283809 100644 --- a/src/state/apis/snapshot/snapshot.ts +++ b/src/state/apis/snapshot/snapshot.ts @@ -13,7 +13,7 @@ import type { ReduxState } from 'state/reducer' import { BASE_RTK_CREATE_API_CONFIG } from '../const' import { getVotingPower } from './getVotingPower' import type { Proposal, Strategy } from './validators' -import { ProposalSchema, ScoresSchema, SnapshotSchema } from './validators' +import { ProposalSchema, SnapshotSchema } from './validators' type FoxVotingPowerCryptoBalance = string @@ -174,21 +174,16 @@ export const snapshotApi = createApi({ delegation, ) - const scores = ScoresSchema.parse(votingPowerResults).scores + const foxHeld = votingPowerResults.reduce( + (acc: BigNumber, scoreByAddress: Record) => { + const values = Object.values(scoreByAddress) - const foxHeld = scores.reduce((acc: number, scoreByAddress: Record) => { - const values = Object.values(scoreByAddress) + if (!values.length) return acc - if (!values.length) return acc - - return acc + BigNumber.sum(...values.map(bnOrZero)).toNumber() - }, 0) - - // Return an error tuple in case of an invalid foxHeld value so we don't cache an errored value - if (isNaN(foxHeld)) { - const data = 'NaN foxHeld value' - return { error: { data, status: 400 } } - } + return acc.plus(BigNumber.sum(...values.map(bnOrZero))) + }, + bnOrZero(0), + ) dispatch(snapshot.actions.setVotingPower({ foxHeld: foxHeld.toString(), model })) @@ -236,21 +231,16 @@ export const snapshotApi = createApi({ delegation, ) - const scores = ScoresSchema.parse(votingPowerResults).scores + const thorHeld = votingPowerResults.reduce( + (acc: BigNumber, scoreByAddress: Record) => { + const values = Object.values(scoreByAddress) - const thorHeld = scores.reduce((acc: number, scoreByAddress: Record) => { - const values = Object.values(scoreByAddress) + if (!values.length) return acc - if (!values.length) return acc - - return acc + BigNumber.sum(...values.map(bnOrZero)).toNumber() - }, 0) - - // Return an error tuple in case of an invalid thorHeld value so we don't cache an errored value - if (isNaN(thorHeld)) { - const data = 'NaN thorHeld value' - return { error: { data, status: 400 } } - } + return acc.plus(BigNumber.sum(...values.map(bnOrZero))) + }, + bnOrZero(0), + ) dispatch(snapshot.actions.setThorVotingPower(thorHeld.toString())) return { data: thorHeld.toString() } From c3ab74e2bd07f6b9c515a4da532e71d34b112053 Mon Sep 17 00:00:00 2001 From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com> Date: Mon, 4 Nov 2024 23:19:34 +0100 Subject: [PATCH 23/23] fix: review feedbacks --- src/state/apis/snapshot/getVotingPower.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/state/apis/snapshot/getVotingPower.ts b/src/state/apis/snapshot/getVotingPower.ts index f9716829d3d..bfca1d16038 100644 --- a/src/state/apis/snapshot/getVotingPower.ts +++ b/src/state/apis/snapshot/getVotingPower.ts @@ -1,7 +1,8 @@ import axios from 'axios' import axiosRetry from 'axios-retry' -import { ScoresSchema, type Strategy } from './validators' +import type { Strategy } from './validators' +import { ScoresSchema } from './validators' export const getVotingPower = async ( addresses: string[],