Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

task: add CRV and Incentives APR to Stake form #268

Merged
merged 1 commit into from
Jul 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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