Commit 976b1598 authored by Noah Zinsmeister's avatar Noah Zinsmeister

add fee calculations

parent 361d17c9
import { BigNumber } from '@ethersproject/bignumber'
import PositionListItem from 'components/PositionListItem' import PositionListItem from 'components/PositionListItem'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
...@@ -36,7 +37,7 @@ const MobileHeader = styled.div` ...@@ -36,7 +37,7 @@ const MobileHeader = styled.div`
` `
export type PositionListProps = React.PropsWithChildren<{ export type PositionListProps = React.PropsWithChildren<{
positions: PositionDetails[] positions: (PositionDetails & { tokenId: BigNumber })[]
}> }>
export default function PositionList({ positions }: PositionListProps) { export default function PositionList({ positions }: PositionListProps) {
......
...@@ -15,6 +15,8 @@ import { TokenAmount } from '@uniswap/sdk-core' ...@@ -15,6 +15,8 @@ import { TokenAmount } from '@uniswap/sdk-core'
import { formatPrice, formatTokenAmount } from 'utils/formatTokenAmount' import { formatPrice, formatTokenAmount } from 'utils/formatTokenAmount'
import Loader from 'components/Loader' import Loader from 'components/Loader'
import { unwrappedToken } from 'utils/wrappedCurrency' import { unwrappedToken } from 'utils/wrappedCurrency'
import { useV3PositionFees } from 'hooks/useV3PositionFees'
import { BigNumber } from '@ethersproject/bignumber'
const ActiveDot = styled.span` const ActiveDot = styled.span`
background-color: ${({ theme }) => theme.success}; background-color: ${({ theme }) => theme.success};
...@@ -119,7 +121,7 @@ const DataText = styled.div` ...@@ -119,7 +121,7 @@ const DataText = styled.div`
` `
export interface PositionListItemProps { export interface PositionListItemProps {
positionDetails: PositionDetails positionDetails: PositionDetails & { tokenId: BigNumber }
positionIndex: number positionIndex: number
} }
...@@ -133,8 +135,6 @@ export default function PositionListItem({ positionDetails, positionIndex }: Pos ...@@ -133,8 +135,6 @@ export default function PositionListItem({ positionDetails, positionIndex }: Pos
liquidity, liquidity,
tickLower, tickLower,
tickUpper, tickUpper,
feeGrowthInside0LastX128,
feeGrowthInside1LastX128,
} = positionDetails } = positionDetails
const token0 = useToken(token0Address) const token0 = useToken(token0Address)
...@@ -166,6 +166,9 @@ export default function PositionListItem({ positionDetails, positionIndex }: Pos ...@@ -166,6 +166,9 @@ export default function PositionListItem({ positionDetails, positionIndex }: Pos
const price1Lower = price0Upper ? price0Upper.invert() : undefined const price1Lower = price0Upper ? price0Upper.invert() : undefined
const price1Upper = price0Lower ? price0Lower.invert() : undefined const price1Upper = price0Lower ? price0Lower.invert() : undefined
// fees
const [feeValue0, feeValue1] = useV3PositionFees(pool ?? undefined, positionDetails)
// check if price is within range // check if price is within range
const outOfRange: boolean = pool ? pool.tickCurrent < tickLower || pool.tickCurrent > tickUpper : false const outOfRange: boolean = pool ? pool.tickCurrent < tickLower || pool.tickCurrent > tickUpper : false
...@@ -232,12 +235,18 @@ export default function PositionListItem({ positionDetails, positionIndex }: Pos ...@@ -232,12 +235,18 @@ export default function PositionListItem({ positionDetails, positionIndex }: Pos
)} )}
</AmountData> </AmountData>
<FeeData> <FeeData>
{feeValue0 && feeValue1 ? (
<>
<DataLineItem> <DataLineItem>
{feeGrowthInside0LastX128.toString()}&nbsp;{currency0?.symbol} {formatTokenAmount(feeValue0, 4)}&nbsp;{currency0?.symbol}
</DataLineItem> </DataLineItem>
<DataLineItem> <DataLineItem>
{feeGrowthInside1LastX128.toString()}&nbsp;{currency1?.symbol} {formatTokenAmount(feeValue1, 4)}&nbsp;{currency1?.symbol}
</DataLineItem> </DataLineItem>
</>
) : (
<Loader />
)}
</FeeData> </FeeData>
</Row> </Row>
) )
......
import { useSingleCallResult } from 'state/multicall/hooks'
import { useMemo } from 'react'
import { PositionDetails } from 'types/position'
import { useV3Pool } from './useContract'
import { BigNumber } from '@ethersproject/bignumber'
import { computePoolAddress, Pool } from '@uniswap/v3-sdk'
import { FACTORY_ADDRESSES } from 'constants/v3'
import { useActiveWeb3React } from 'hooks'
import { TokenAmount } from '@uniswap/sdk-core'
// TODO port these utility functions to the SDK
function subIn256(x: BigNumber, y: BigNumber): BigNumber {
const difference = x.sub(y)
return difference.lt(0) ? BigNumber.from(2).pow(256).sub(difference) : difference
}
function getCounterfactualFees(
feeGrowthGlobal: BigNumber,
feeGrowthOutsideLower: BigNumber,
feeGrowthOutsideUpper: BigNumber,
feeGrowthInsideLast: BigNumber,
pool: Pool,
liquidity: BigNumber,
tickLower: number,
tickUpper: number
) {
let feeGrowthBelow: BigNumber
if (pool.tickCurrent >= tickLower) {
feeGrowthBelow = feeGrowthOutsideLower
} else {
feeGrowthBelow = subIn256(feeGrowthGlobal, feeGrowthOutsideLower)
}
let feeGrowthAbove: BigNumber
if (pool.tickCurrent < tickUpper) {
feeGrowthAbove = feeGrowthOutsideUpper
} else {
feeGrowthAbove = subIn256(feeGrowthGlobal, feeGrowthOutsideUpper)
}
const feeGrowthInside = subIn256(subIn256(feeGrowthGlobal, feeGrowthBelow), feeGrowthAbove)
return subIn256(feeGrowthInside, feeGrowthInsideLast).mul(liquidity).div(BigNumber.from(2).pow(128))
}
// compute current + counterfactual fees for a v3 position
export function useV3PositionFees(
pool?: Pool,
positionDetails?: PositionDetails & { tokenId: BigNumber }
): [TokenAmount, TokenAmount] | [undefined, undefined] {
const { chainId } = useActiveWeb3React()
const poolAddress = useMemo(() => {
try {
return chainId && pool && positionDetails
? computePoolAddress({
factoryAddress: FACTORY_ADDRESSES[chainId],
tokenA: pool.token0,
tokenB: pool.token1,
fee: positionDetails.fee,
})
: undefined
} catch {
return undefined
}
}, [chainId, pool, positionDetails])
const poolContract = useV3Pool(poolAddress)
// data fetching
const feeGrowthGlobal0: BigNumber | undefined = useSingleCallResult(poolContract, 'feeGrowthGlobal0X128')?.result?.[0]
const feeGrowthGlobal1: BigNumber | undefined = useSingleCallResult(poolContract, 'feeGrowthGlobal1X128')?.result?.[0]
const { feeGrowthOutside0X128: feeGrowthOutsideLower0 } = (useSingleCallResult(poolContract, 'ticks', [
positionDetails?.tickLower,
])?.result ?? {}) as { feeGrowthOutside0X128?: BigNumber }
const { feeGrowthOutside1X128: feeGrowthOutsideLower1 } = (useSingleCallResult(poolContract, 'ticks', [
positionDetails?.tickLower,
])?.result ?? {}) as { feeGrowthOutside1X128?: BigNumber }
const { feeGrowthOutside0X128: feeGrowthOutsideUpper0 } = (useSingleCallResult(poolContract, 'ticks', [
positionDetails?.tickUpper,
])?.result ?? {}) as { feeGrowthOutside0X128?: BigNumber }
const { feeGrowthOutside1X128: feeGrowthOutsideUpper1 } = (useSingleCallResult(poolContract, 'ticks', [
positionDetails?.tickUpper,
])?.result ?? {}) as { feeGrowthOutside1X128?: BigNumber }
// calculate fees
const counterfactualFees0 =
positionDetails && pool && feeGrowthGlobal0 && feeGrowthOutsideLower0 && feeGrowthOutsideUpper0
? getCounterfactualFees(
feeGrowthGlobal0,
feeGrowthOutsideLower0,
feeGrowthOutsideUpper0,
positionDetails.feeGrowthInside0LastX128,
pool,
positionDetails.liquidity,
positionDetails.tickLower,
positionDetails.tickUpper
)
: undefined
const counterfactualFees1 =
positionDetails && pool && feeGrowthGlobal1 && feeGrowthOutsideLower1 && feeGrowthOutsideUpper1
? getCounterfactualFees(
feeGrowthGlobal1,
feeGrowthOutsideLower1,
feeGrowthOutsideUpper1,
positionDetails.feeGrowthInside1LastX128,
pool,
positionDetails.liquidity,
positionDetails.tickLower,
positionDetails.tickUpper
)
: undefined
if (
pool &&
positionDetails?.tokensOwed0 &&
positionDetails?.tokensOwed1 &&
counterfactualFees0 &&
counterfactualFees1
) {
return [
new TokenAmount(pool.token0, positionDetails.tokensOwed0.add(counterfactualFees0).toString()),
new TokenAmount(pool.token1, positionDetails.tokensOwed1.add(counterfactualFees1).toString()),
]
} else {
return [undefined, undefined]
}
}
...@@ -19,6 +19,8 @@ import { DarkCard, DarkGreyCard } from 'components/Card' ...@@ -19,6 +19,8 @@ import { DarkCard, DarkGreyCard } from 'components/Card'
import CurrencyLogo from 'components/CurrencyLogo' import CurrencyLogo from 'components/CurrencyLogo'
import { AlertTriangle } from 'react-feather' import { AlertTriangle } from 'react-feather'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useV3PositionFees } from 'hooks/useV3PositionFees'
import { formatTokenAmount } from 'utils/formatTokenAmount'
const PageWrapper = styled.div` const PageWrapper = styled.div`
min-width: 800px; min-width: 800px;
...@@ -37,10 +39,10 @@ const ResponsiveGrid = styled.div` ...@@ -37,10 +39,10 @@ const ResponsiveGrid = styled.div`
display: grid; display: grid;
grid-gap: 1em; grid-gap: 1em;
grid-template-columns: 1.5fr repeat(4, 1fr); grid-template-columns: 1.5fr repeat(3, 1fr);
@media screen and (max-width: 900px) { @media screen and (max-width: 900px) {
grid-template-columns: 1.5fr repeat(4, 1fr); grid-template-columns: 1.5fr repeat(3, 1fr);
& :nth-child(4) { & :nth-child(4) {
display: none; display: none;
} }
...@@ -92,17 +94,8 @@ export function PositionPage({ ...@@ -92,17 +94,8 @@ export function PositionPage({
const positionDetails = positionIndex && positions ? positions[parseInt(positionIndex)] : undefined const positionDetails = positionIndex && positions ? positions[parseInt(positionIndex)] : undefined
const { const { token0: token0Address, token1: token1Address, fee: feeAmount, liquidity, tickLower, tickUpper, tokenId } =
token0: token0Address, positionDetails || {}
token1: token1Address,
fee: feeAmount,
liquidity,
tickLower,
tickUpper,
tokenId,
// feeGrowthInside0LastX128,
// feeGrowthInside1LastX128,
} = positionDetails || {}
const token0 = useToken(token0Address) const token0 = useToken(token0Address)
const token1 = useToken(token1Address) const token1 = useToken(token1Address)
...@@ -128,6 +121,9 @@ export function PositionPage({ ...@@ -128,6 +121,9 @@ export function PositionPage({
const outOfRange: boolean = const outOfRange: boolean =
pool && tickLower && tickUpper ? pool.tickCurrent < tickLower || pool.tickCurrent > tickUpper : false pool && tickLower && tickUpper ? pool.tickCurrent < tickLower || pool.tickCurrent > tickUpper : false
// fees
const [feeValue0, feeValue1] = useV3PositionFees(pool ?? undefined, positionDetails)
return loading || poolState === PoolState.LOADING || !feeAmount ? ( return loading || poolState === PoolState.LOADING || !feeAmount ? (
<LoadingRows> <LoadingRows>
<div /> <div />
...@@ -184,7 +180,6 @@ export function PositionPage({ ...@@ -184,7 +180,6 @@ export function PositionPage({
<AutoColumn gap="lg"> <AutoColumn gap="lg">
<ResponsiveGrid> <ResponsiveGrid>
<Label>Tokens</Label> <Label>Tokens</Label>
<Label end={true}>Entry</Label>
<Label end={true}>Current</Label> <Label end={true}>Current</Label>
<Label end={true}>Fees</Label> <Label end={true}>Fees</Label>
<Label end={true}>USD Value</Label> <Label end={true}>USD Value</Label>
...@@ -195,9 +190,8 @@ export function PositionPage({ ...@@ -195,9 +190,8 @@ export function PositionPage({
<TYPE.label ml="10px">{currency0?.symbol}</TYPE.label> <TYPE.label ml="10px">{currency0?.symbol}</TYPE.label>
</RowFixed> </RowFixed>
<Label end={true}>{position?.amount0.toSignificant(4)}</Label> <Label end={true}>{position?.amount0.toSignificant(4)}</Label>
<Label end={true}>{position?.amount0.toSignificant(4)}</Label> <Label end={true}>{feeValue0 ? formatTokenAmount(feeValue0, 4) : '-'}</Label>
<Label end={true}>1</Label> <Label end={true}>-</Label>
<Label end={true}>$100</Label>
</ResponsiveGrid> </ResponsiveGrid>
<ResponsiveGrid> <ResponsiveGrid>
<RowFixed> <RowFixed>
...@@ -205,9 +199,8 @@ export function PositionPage({ ...@@ -205,9 +199,8 @@ export function PositionPage({
<TYPE.label ml="10px">{currency1?.symbol}</TYPE.label> <TYPE.label ml="10px">{currency1?.symbol}</TYPE.label>
</RowFixed> </RowFixed>
<Label end={true}>{position?.amount1.toSignificant(4)}</Label> <Label end={true}>{position?.amount1.toSignificant(4)}</Label>
<Label end={true}>{position?.amount1.toSignificant(4)}</Label> <Label end={true}>{feeValue1 ? formatTokenAmount(feeValue1, 4) : '-'}</Label>
<Label end={true}>1</Label> <Label end={true}>-</Label>
<Label end={true}>$100</Label>
</ResponsiveGrid> </ResponsiveGrid>
</AutoColumn> </AutoColumn>
</DarkCard> </DarkCard>
......
import React, { useContext, useMemo } from 'react' import React, { useContext, useMemo } from 'react'
import Badge, { BadgeVariant } from 'components/Badge'
import { ButtonGray, ButtonPrimary } from 'components/Button' import { ButtonGray, ButtonPrimary } from 'components/Button'
import { AutoColumn } from 'components/Column' import { AutoColumn } from 'components/Column'
import { FlyoutAlignment, NewMenu } from 'components/Menu' import { FlyoutAlignment, NewMenu } from 'components/Menu'
...@@ -8,13 +7,12 @@ import PositionList from 'components/PositionList' ...@@ -8,13 +7,12 @@ import PositionList from 'components/PositionList'
import { RowBetween, RowFixed } from 'components/Row' import { RowBetween, RowFixed } from 'components/Row'
import { useActiveWeb3React } from 'hooks' import { useActiveWeb3React } from 'hooks'
import { useV3Positions } from 'hooks/useV3Positions' import { useV3Positions } from 'hooks/useV3Positions'
import { BookOpen, ChevronDown, Download, Inbox, Info, PlusCircle } from 'react-feather' import { BookOpen, ChevronDown, Download, Inbox, PlusCircle } from 'react-feather'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { useWalletModalToggle } from 'state/application/hooks' import { useWalletModalToggle } from 'state/application/hooks'
import styled, { ThemeContext } from 'styled-components' import styled, { ThemeContext } from 'styled-components'
import { HideSmall, MEDIA_WIDTHS, TYPE } from 'theme' import { HideSmall, TYPE } from 'theme'
import { PositionDetails } from 'types/position'
import { LoadingRows } from './styleds' import { LoadingRows } from './styleds'
const PageWrapper = styled(AutoColumn)` const PageWrapper = styled(AutoColumn)`
...@@ -40,15 +38,6 @@ const ButtonRow = styled(RowFixed)` ...@@ -40,15 +38,6 @@ const ButtonRow = styled(RowFixed)`
justify-content: space-between; justify-content: space-between;
`}; `};
` `
const InactivePositionsBadge = styled(Badge)`
border-radius: 12px;
height: 100%;
padding: 6px 8px;
display: none;
@media screen and (min-width: ${MEDIA_WIDTHS.upToMedium}px) {
display: flex;
}
`
const Menu = styled(NewMenu)` const Menu = styled(NewMenu)`
margin-left: 0; margin-left: 0;
${({ theme }) => theme.mediaWidth.upToSmall` ${({ theme }) => theme.mediaWidth.upToSmall`
...@@ -105,16 +94,6 @@ export default function Pool() { ...@@ -105,16 +94,6 @@ export default function Pool() {
const hasPositions = useMemo(() => Boolean(positions && positions.length > 0), [positions]) const hasPositions = useMemo(() => Boolean(positions && positions.length > 0), [positions])
const numInactivePositions = useMemo(() => {
return hasPositions && positions
? positions.reduce((acc: any, position: PositionDetails) => {
const { tokensOwed0, tokensOwed1 } = position
const limitCrossed = tokensOwed0.eq(0) || tokensOwed1.eq(0)
return limitCrossed ? acc + 1 : acc
}, 0)
: 0
}, [positions, hasPositions])
const hasV2Liquidity = true const hasV2Liquidity = true
const showMigrateHeaderLink = Boolean(hasV2Liquidity && hasPositions) const showMigrateHeaderLink = Boolean(hasV2Liquidity && hasPositions)
...@@ -160,14 +139,6 @@ export default function Pool() { ...@@ -160,14 +139,6 @@ export default function Pool() {
<TYPE.mediumHeader>{t('Pool Overview')}</TYPE.mediumHeader> <TYPE.mediumHeader>{t('Pool Overview')}</TYPE.mediumHeader>
</HideSmall> </HideSmall>
<ButtonRow> <ButtonRow>
{numInactivePositions > 0 && (
<InactivePositionsBadge variant={BadgeVariant.WARNING_OUTLINE}>
<Info size={20} />
&nbsp;&nbsp;
{numInactivePositions}{' '}
{numInactivePositions === 1 ? t('Inactive position') : t('Inactive positions')}
</InactivePositionsBadge>
)}
<Menu <Menu
flyoutAlignment={FlyoutAlignment.LEFT} flyoutAlignment={FlyoutAlignment.LEFT}
ToggleUI={(props: any) => ( ToggleUI={(props: any) => (
......
...@@ -96,6 +96,7 @@ function Remove({ tokenId }: { tokenId: BigNumber }) { ...@@ -96,6 +96,7 @@ function Remove({ tokenId }: { tokenId: BigNumber }) {
) )
} }
// TODO allow collection in ETH
data.push( data.push(
positionManager.interface.encodeFunctionData('collect', [ positionManager.interface.encodeFunctionData('collect', [
{ {
......
...@@ -4,6 +4,7 @@ import { Position } from '@uniswap/v3-sdk' ...@@ -4,6 +4,7 @@ import { Position } from '@uniswap/v3-sdk'
import { usePool } from 'data/Pools' import { usePool } from 'data/Pools'
import { useActiveWeb3React } from 'hooks' import { useActiveWeb3React } from 'hooks'
import { useToken } from 'hooks/Tokens' import { useToken } from 'hooks/Tokens'
import { useV3PositionFees } from 'hooks/useV3PositionFees'
import { useCallback, useMemo } from 'react' import { useCallback, useMemo } from 'react'
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
import { PositionDetails } from 'types/position' import { PositionDetails } from 'types/position'
...@@ -33,29 +34,23 @@ export function useDerivedV3BurnInfo( ...@@ -33,29 +34,23 @@ export function useDerivedV3BurnInfo(
const [, pool] = usePool(token0 ?? undefined, token1 ?? undefined, position?.fee) const [, pool] = usePool(token0 ?? undefined, token1 ?? undefined, position?.fee)
const liquidity = position?.liquidity ? position.liquidity.mul(percent).div(100) : undefined const partialPosition = useMemo(
const positionSDK = useMemo(
() => () =>
pool && liquidity && position?.tickLower && position?.tickLower pool && position?.liquidity && position?.tickLower && position?.tickLower
? new Position({ ? new Position({
pool, pool,
liquidity: liquidity.toString(), liquidity: position.liquidity.mul(percent).div(100).toString(),
tickLower: position?.tickLower, tickLower: position.tickLower,
tickUpper: position?.tickUpper, tickUpper: position.tickUpper,
}) })
: undefined, : undefined,
[pool, liquidity, position] [pool, percent, position]
) )
const liquidityValue0 = positionSDK?.amount0 const liquidityValue0 = partialPosition?.amount0
const liquidityValue1 = positionSDK?.amount1 const liquidityValue1 = partialPosition?.amount1
// TODO include counterfactual fees calculate from fee growth snapshots here const [feeValue0, feeValue1] = useV3PositionFees(pool ?? undefined, position)
const feeValue0 =
token0 && position?.tokensOwed0 ? new TokenAmount(token0, position.tokensOwed0.toString()) : undefined
const feeValue1 =
token1 && position?.tokensOwed1 ? new TokenAmount(token1, position.tokensOwed1.toString()) : undefined
let error: string | undefined let error: string | undefined
if (!account) { if (!account) {
...@@ -64,7 +59,14 @@ export function useDerivedV3BurnInfo( ...@@ -64,7 +59,14 @@ export function useDerivedV3BurnInfo(
if (percent === 0) { if (percent === 0) {
error = error ?? 'Enter an percent' error = error ?? 'Enter an percent'
} }
return { liquidity, liquidityValue0, liquidityValue1, feeValue0, feeValue1, error } return {
liquidity: partialPosition?.liquidity ? BigNumber.from(partialPosition?.liquidity.toString()) : undefined,
liquidityValue0,
liquidityValue1,
feeValue0,
feeValue1,
error,
}
} }
export function useBurnV3ActionHandlers(): { export function useBurnV3ActionHandlers(): {
......
import { Price, TokenAmount } from '@uniswap/sdk-core' import { Price, TokenAmount } from '@uniswap/sdk-core'
import JSBI from 'jsbi'
export function formatTokenAmount(amount: TokenAmount | undefined, sigFigs: number) { export function formatTokenAmount(amount: TokenAmount | undefined, sigFigs: number) {
if (!amount) { if (!amount) {
return '-' return '-'
} }
if (JSBI.equal(amount.raw, JSBI.BigInt(0))) {
return '0'
}
if (parseFloat(amount.toFixed(sigFigs)) < 0.0001) { if (parseFloat(amount.toFixed(sigFigs)) < 0.0001) {
return '<0.0001' return '<0.0001'
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment