Skip to content

Commit

Permalink
Merge pull request #268 from curvefi/task/add-crv-incentives-apr
Browse files Browse the repository at this point in the history
task: add CRV and Incentives APR to Stake form
  • Loading branch information
amytsang authored Jul 25, 2024
2 parents d20ec6e + 58dcf64 commit eaa4888
Show file tree
Hide file tree
Showing 8 changed files with 251 additions and 11 deletions.
1 change: 1 addition & 0 deletions apps/lend/src/abis/totalSupply.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"abi":[{"name":"totalSupply","outputs":[{"type":"uint256","name":""}],"inputs":[],"stateMutability":"view","type":"function"}]}
126 changes: 126 additions & 0 deletions apps/lend/src/components/DetailInfoCrvIncentives.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { t } from '@lingui/macro'

import React, { useMemo } from 'react'
import styled from 'styled-components'

import { FORMAT_OPTIONS, formatNumber } from '@/ui/utils'
import { INVALID_ADDRESS } from '@/constants'
import useAbiTotalSupply from '@/hooks/useAbiTotalSupply'
import useStore from '@/store/useStore'
import useSupplyTotalApr from '@/hooks/useSupplyTotalApr'

import DetailInfo from '@/ui/DetailInfo'
import Icon from '@/ui/Icon'
import TooltipIcon from '@/ui/Tooltip/TooltipIcon'

type Data = {
label: string
tooltip: string
skeleton: [number, number]
aprCurr: string
aprNew: string
ratio: number
}

const DetailInfoCrvIncentives = ({
rChainId,
rOwmId,
lpTokenAmount,
}: {
rChainId: ChainId
rOwmId: string
lpTokenAmount: string
}) => {
const { tooltipValues } = useSupplyTotalApr(rChainId, rOwmId)
const owmData = useStore((state) => state.markets.owmDatasMapper[rChainId]?.[rOwmId])
const { gauge: gaugeAddress } = owmData?.owm?.addresses ?? {}
const gaugeTotalSupply = useAbiTotalSupply(rChainId, gaugeAddress)
const isGaugeAddressInvalid = gaugeAddress === INVALID_ADDRESS

const { crvBase = '', incentivesObj = [] } = tooltipValues ?? {}

const data = useMemo(() => {
let data: Data[] = []

if (!isGaugeAddressInvalid) {
if (+crvBase > 0) {
data.push({
label: t`CRV APR:`,
tooltip: t`As the number of staked vault shares increases, the CRV APR will decrease.`,
skeleton: [50, 23],
..._getDataApr(crvBase, gaugeTotalSupply, lpTokenAmount),
})
}

if (incentivesObj.length > 0) {
incentivesObj.forEach(({ apy, symbol }) => {
data.push({
label: t`Incentives ${symbol} APR:`,
tooltip: t`As the number of staked vault shares increases, the ${symbol} APR will decrease.`,
skeleton: [60, 23],
..._getDataApr(apy, gaugeTotalSupply, lpTokenAmount),
})
})
}
}

return data
}, [crvBase, gaugeTotalSupply, incentivesObj, isGaugeAddressInvalid, lpTokenAmount])

if (data.length === 0 || isGaugeAddressInvalid) {
return null
}

return (
<>
{data.map(({ label, tooltip, skeleton, aprCurr, aprNew, ratio }, idx) => {
return (
<DetailInfo
key={`${label}${idx}`}
loading={aprCurr === ''}
loadingSkeleton={skeleton ?? [140, 23]}
label={label}
tooltip={
<StyledTooltipIcon minWidth="200px" textAlign="left" iconStyles={{ $svgTop: '0.2rem' }}>
{tooltip}
</StyledTooltipIcon>
}
>
<strong>{aprCurr}</strong>
{ratio > 1.25 && (
<>
<StyledIcon name="ArrowRight" size={16} /> <strong>{aprNew}</strong>
</>
)}{' '}
</DetailInfo>
)
})}
</>
)
}

const StyledTooltipIcon = styled(TooltipIcon)`
margin-left: var(--spacing-1);
`

const StyledIcon = styled(Icon)`
margin: 0 var(--spacing-1);
`

export default DetailInfoCrvIncentives

function _getDataApr(
currApr: string | number | undefined = '',
gaugeTotalSupply: number | null,
lpTokenAmount: string
) {
let resp = { aprCurr: formatNumber(currApr, FORMAT_OPTIONS.PERCENT), aprNew: '', ratio: 0 }

if (+currApr > 0 && gaugeTotalSupply && +(gaugeTotalSupply || '0') > 0 && +lpTokenAmount > 0) {
const newGaugeTotalLocked = Number(lpTokenAmount) + gaugeTotalSupply
const aprNew = (gaugeTotalSupply / newGaugeTotalLocked) * +currApr
resp.aprNew = formatNumber(aprNew, FORMAT_OPTIONS.PERCENT)
resp.ratio = +currApr / aprNew
}
return resp
}
25 changes: 16 additions & 9 deletions apps/lend/src/components/PageVault/VaultStake/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { StyledDetailInfoWrapper, StyledInpChip } from '@/components/PageLoanMan
import AlertBox from '@/ui/AlertBox'
import AlertFormError from '@/components/AlertFormError'
import Box from '@/ui/Box'
import DetailInfoCrvIncentives from '@/components/DetailInfoCrvIncentives'
import DetailInfoEstimateGas from '@/components/DetailInfoEstimateGas'
import InputProvider, { InputDebounced, InputMaxBtn } from '@/ui/InputComp'
import LoanFormConnect from '@/components/LoanFormConnect'
Expand Down Expand Up @@ -159,6 +160,7 @@ const VaultStake = ({ rChainId, rOwmId, rFormType, isLoaded, api, owmData, userA

const activeStep = signerAddress ? getActiveStep(steps) : null
const disabled = !!formStatus.step
const detailInfoCrvIncentivesComp = DetailInfoCrvIncentives({ rChainId, rOwmId, lpTokenAmount: formValues.amount })

return (
<>
Expand Down Expand Up @@ -198,15 +200,20 @@ const VaultStake = ({ rChainId, rOwmId, rFormType, isLoaded, api, owmData, userA
</div>

{/* detail info */}
<StyledDetailInfoWrapper>
{signerAddress && (
<DetailInfoEstimateGas
chainId={rChainId}
{...formEstGas}
stepProgress={activeStep && steps.length > 1 ? { active: activeStep, total: steps.length } : null}
/>
)}
</StyledDetailInfoWrapper>
{(detailInfoCrvIncentivesComp || signerAddress) && (
<StyledDetailInfoWrapper>
{detailInfoCrvIncentivesComp}

{signerAddress && (
<DetailInfoEstimateGas
isDivider={detailInfoCrvIncentivesComp !== null}
chainId={rChainId}
{...formEstGas}
stepProgress={activeStep && steps.length > 1 ? { active: activeStep, total: steps.length } : null}
/>
)}
</StyledDetailInfoWrapper>
)}

{/* actions */}
<LoanFormConnect haveSigner={!!signerAddress} loading={!api}>
Expand Down
43 changes: 43 additions & 0 deletions apps/lend/src/hooks/useAbiTotalSupply.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { Contract } from 'ethers'

import { useCallback, useEffect, useState } from 'react'

import { INVALID_ADDRESS, REFRESH_INTERVAL } from '@/constants'
import { weiToEther } from '@/shared/curve-lib'
import useContract from '@/hooks/useContract'
import usePageVisibleInterval from '@/ui/hooks/usePageVisibleInterval'
import useStore from '@/store/useStore'

const useAbiTotalSupply = (rChainId: ChainId, contractAddress: string | undefined) => {
const contract = useContract(rChainId, false, 'totalSupply', contractAddress)
const isValidAddress = contractAddress !== INVALID_ADDRESS

const isPageVisible = useStore((state) => state.isPageVisible)

const [totalSupply, settotalSupply] = useState<number | null>(null)

const getTotalSupply = useCallback(async (contract: Contract) => {
try {
const totalSupply = await contract.totalSupply()
settotalSupply(weiToEther(Number(totalSupply)))
} catch (error) {
console.error(error)
}
}, [])

useEffect(() => {
if (contract && isValidAddress) getTotalSupply(contract)
}, [contract, isValidAddress, getTotalSupply])

usePageVisibleInterval(
() => {
if (contract && isValidAddress) getTotalSupply(contract)
},
REFRESH_INTERVAL['1m'],
isPageVisible
)

return totalSupply
}

export default useAbiTotalSupply
52 changes: 52 additions & 0 deletions apps/lend/src/hooks/useContract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Contract, Interface, JsonRpcProvider } from 'ethers'
import { useCallback, useEffect, useState } from 'react'

import networks from '@/networks'
import useStore from '@/store/useStore'

const useAbiGaugeTotalSupply = (
rChainId: ChainId,
signerRequired: boolean,
jsonModuleName: string,
contractAddress: string | undefined
) => {
const getProvider = useStore((state) => state.wallet.getProvider)

const [contract, setContract] = useState<Contract | null>(null)

const getContract = useCallback(
async (jsonModuleName: string, contractAddress: string, provider: Provider | JsonRpcProvider) => {
try {
const abi = await import(`@/abis/${jsonModuleName}.json`).then((module) => module.default.abi)

if (!abi) {
console.error('cannot find abi')
return null
} else {
const iface = new Interface(abi)
return new Contract(contractAddress, iface.format(), provider)
}
} catch (error) {
console.error(error)
return null
}
},
[]
)

useEffect(() => {
if (rChainId) {
const provider = signerRequired
? getProvider('')
: getProvider('') || new JsonRpcProvider(networks[rChainId].rpcUrl)

if (jsonModuleName && contractAddress && provider) {
;(async () => setContract(await getContract(jsonModuleName, contractAddress, provider)))()
}
}
}, [contractAddress, getContract, getProvider, jsonModuleName, rChainId, signerRequired])

return contract
}

export default useAbiGaugeTotalSupply
1 change: 1 addition & 0 deletions apps/lend/src/hooks/useSupplyTotalApr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ function _getTooltipValue(lendApr: number, lendApy: number, crvBase: number, crv
return {
lendApr: formatNumber(lendApr, FORMAT_OPTIONS.PERCENT),
lendApy: `${formatNumber(lendApy, FORMAT_OPTIONS.PERCENT)} APY`,
crvBase,
crv: crvBase > 0 ? formatNumber(crvBase, FORMAT_OPTIONS.PERCENT) : '',
crvBoosted: crvBoost > 0 ? formatNumber(crvBoost, FORMAT_OPTIONS.PERCENT) : '',
incentives: others.map((o) => `${formatNumber(o.apy, FORMAT_OPTIONS.PERCENT)} ${o.symbol}`),
Expand Down
11 changes: 10 additions & 1 deletion packages/ui/src/Tooltip/TooltipButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,16 @@ import styled from 'styled-components'
import Icon from 'ui/src/Icon'
import Tooltip from 'ui/src/Tooltip/Tooltip'

export type IconStyles = { $svgTop?: string }

function TooltipButton({
className = '',
children,
showIcon,
customIcon,
onClick,
increaseZIndex,
iconStyles = {},
...props
}: React.PropsWithChildren<
TooltipTriggerProps &
Expand All @@ -27,6 +30,7 @@ function TooltipButton({
customIcon?: React.ReactNode
increaseZIndex?: boolean
onClick?: () => void
iconStyles?: IconStyles
}
>) {
const state = useTooltipTriggerState({ delay: 0, ...props })
Expand Down Expand Up @@ -61,7 +65,7 @@ function TooltipButton({
<StyledTooltipButton>
<Button ref={ref} {...triggerProps} className={className} onClick={handleBtnClick}>
{showIcon || customIcon
? customIcon ?? <Icon className="svg-tooltip" name="InformationSquare" size={16} />
? customIcon ?? <StyledIcon {...iconStyles} name="InformationSquare" size={16} />
: children}
</Button>
{state.isOpen && (
Expand Down Expand Up @@ -102,4 +106,9 @@ const Button = styled.span`
}
`

const StyledIcon = styled(Icon)<IconStyles>`
position: relative;
top: ${({ $svgTop }) => $svgTop || `0.2rem`};
`

export default TooltipButton
3 changes: 2 additions & 1 deletion packages/ui/src/Tooltip/TooltipIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import type { TooltipProps } from 'ui/src/Tooltip/types'

import React from 'react'

import TooltipButton from 'ui/src/Tooltip/TooltipButton'
import TooltipButton, { IconStyles } from 'ui/src/Tooltip/TooltipButton'

const TooltipIcon = ({
children,
customIcon,
...props
}: React.PropsWithChildren<
TooltipProps & {
iconStyles?: IconStyles
customIcon?: React.ReactNode
}
>) => {
Expand Down

0 comments on commit eaa4888

Please sign in to comment.