diff --git a/apps/lend/src/abis/totalSupply.json b/apps/lend/src/abis/totalSupply.json
new file mode 100644
index 000000000..84b36c9a7
--- /dev/null
+++ b/apps/lend/src/abis/totalSupply.json
@@ -0,0 +1 @@
+{"abi":[{"name":"totalSupply","outputs":[{"type":"uint256","name":""}],"inputs":[],"stateMutability":"view","type":"function"}]}
diff --git a/apps/lend/src/components/DetailInfoCrvIncentives.tsx b/apps/lend/src/components/DetailInfoCrvIncentives.tsx
new file mode 100644
index 000000000..2d33056fe
--- /dev/null
+++ b/apps/lend/src/components/DetailInfoCrvIncentives.tsx
@@ -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 (
+
+ {tooltip}
+
+ }
+ >
+ {aprCurr}
+ {ratio > 1.25 && (
+ <>
+ {aprNew}
+ >
+ )}{' '}
+
+ )
+ })}
+ >
+ )
+}
+
+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
+}
diff --git a/apps/lend/src/components/PageVault/VaultStake/index.tsx b/apps/lend/src/components/PageVault/VaultStake/index.tsx
index dc6a035d5..4623f4ea4 100644
--- a/apps/lend/src/components/PageVault/VaultStake/index.tsx
+++ b/apps/lend/src/components/PageVault/VaultStake/index.tsx
@@ -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'
@@ -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 (
<>
@@ -198,15 +200,20 @@ const VaultStake = ({ rChainId, rOwmId, rFormType, isLoaded, api, owmData, userA
{/* detail info */}
-
- {signerAddress && (
- 1 ? { active: activeStep, total: steps.length } : null}
- />
- )}
-
+ {(detailInfoCrvIncentivesComp || signerAddress) && (
+
+ {detailInfoCrvIncentivesComp}
+
+ {signerAddress && (
+ 1 ? { active: activeStep, total: steps.length } : null}
+ />
+ )}
+
+ )}
{/* actions */}
diff --git a/apps/lend/src/hooks/useAbiTotalSupply.tsx b/apps/lend/src/hooks/useAbiTotalSupply.tsx
new file mode 100644
index 000000000..e3fa96ac8
--- /dev/null
+++ b/apps/lend/src/hooks/useAbiTotalSupply.tsx
@@ -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(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
diff --git a/apps/lend/src/hooks/useContract.ts b/apps/lend/src/hooks/useContract.ts
new file mode 100644
index 000000000..87a82db0c
--- /dev/null
+++ b/apps/lend/src/hooks/useContract.ts
@@ -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(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
diff --git a/apps/lend/src/hooks/useSupplyTotalApr.ts b/apps/lend/src/hooks/useSupplyTotalApr.ts
index 367945b19..e7127c693 100644
--- a/apps/lend/src/hooks/useSupplyTotalApr.ts
+++ b/apps/lend/src/hooks/useSupplyTotalApr.ts
@@ -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}`),
diff --git a/packages/ui/src/Tooltip/TooltipButton.tsx b/packages/ui/src/Tooltip/TooltipButton.tsx
index 5d19009fb..81934108b 100644
--- a/packages/ui/src/Tooltip/TooltipButton.tsx
+++ b/packages/ui/src/Tooltip/TooltipButton.tsx
@@ -10,6 +10,8 @@ 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,
@@ -17,6 +19,7 @@ function TooltipButton({
customIcon,
onClick,
increaseZIndex,
+ iconStyles = {},
...props
}: React.PropsWithChildren<
TooltipTriggerProps &
@@ -27,6 +30,7 @@ function TooltipButton({
customIcon?: React.ReactNode
increaseZIndex?: boolean
onClick?: () => void
+ iconStyles?: IconStyles
}
>) {
const state = useTooltipTriggerState({ delay: 0, ...props })
@@ -61,7 +65,7 @@ function TooltipButton({
{state.isOpen && (
@@ -102,4 +106,9 @@ const Button = styled.span`
}
`
+const StyledIcon = styled(Icon)`
+ position: relative;
+ top: ${({ $svgTop }) => $svgTop || `0.2rem`};
+`
+
export default TooltipButton
diff --git a/packages/ui/src/Tooltip/TooltipIcon.tsx b/packages/ui/src/Tooltip/TooltipIcon.tsx
index f5a14050a..e9278fe78 100644
--- a/packages/ui/src/Tooltip/TooltipIcon.tsx
+++ b/packages/ui/src/Tooltip/TooltipIcon.tsx
@@ -2,7 +2,7 @@ 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,
@@ -10,6 +10,7 @@ const TooltipIcon = ({
...props
}: React.PropsWithChildren<
TooltipProps & {
+ iconStyles?: IconStyles
customIcon?: React.ReactNode
}
>) => {