Start Position management, bug fix on add amounts (#40)

* very rough positions/pools data fetching and position list rendering

* fix formatting

* fix loading

* position page routing, bug on add page

Co-authored-by: ianlapham <ianlapham@gmail.com>
This commit is contained in:
Jordan Frankfurt 2021-04-16 15:33:26 -04:00 committed by GitHub
parent 0c0305a53d
commit 9f5584c37d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 1117 additions and 728 deletions

@ -10,7 +10,7 @@ module.exports = {
reactDocgen: 'react-docgen-typescript',
reactDocgenTypescriptOptions: {
shouldExtractLiteralValuesFromEnum: true,
propFilter: prop => (prop.parent ? !/node_modules/.test(prop.parent.fileName) : true)
}
}
propFilter: (prop) => (prop.parent ? !/node_modules/.test(prop.parent.fileName) : true),
},
},
}

@ -4,11 +4,11 @@ import { create } from '@storybook/theming'
const uniswapBaseTheme = {
brandTitle: 'Uniswap Design',
brandUrl: 'https://uniswap.org',
brandImage: 'https://ipfs.io/ipfs/QmNa8mQkrNKp1WEEeGjFezDmDeodkWRevGFN8JCV7b4Xir'
brandImage: 'https://ipfs.io/ipfs/QmNa8mQkrNKp1WEEeGjFezDmDeodkWRevGFN8JCV7b4Xir',
}
export const light = create({
base: 'light',
...uniswapBaseTheme
...uniswapBaseTheme,
})
// export const dark = create({

@ -28,6 +28,10 @@ export const DarkGreyCard = styled(Card)`
background-color: ${({ theme }) => theme.bg2};
`
export const DarkCard = styled(Card)`
background-color: ${({ theme }) => theme.bg0};
`
export const OutlineCard = styled(Card)`
border: 1px solid ${({ theme }) => theme.bg3};
`

@ -1,51 +1,53 @@
import { Story } from '@storybook/react/types-6-0'
import React from 'react'
import { basisPointsToPercent } from 'utils'
import { DAI, WBTC } from '../../constants'
import Component, { PositionListProps } from './index'
import { TokenAmount } from '@uniswap/sdk-core'
import JSBI from 'jsbi'
// import { Story } from '@storybook/react/types-6-0'
// import React from 'react'
// import { Position } from 'types/position'
// import { basisPointsToPercent } from 'utils'
// import { DAI, WBTC } from '../../constants'
// import Component, { PositionListProps } from './index'
// import { TokenAmount } from '@uniswap/sdk-core'
// import JSBI from 'jsbi'
const FEE_BIPS = {
FIVE: basisPointsToPercent(5),
THIRTY: basisPointsToPercent(30),
ONE_HUNDRED: basisPointsToPercent(100),
}
const daiAmount = new TokenAmount(DAI, JSBI.BigInt(500 * 10 ** 18))
const wbtcAmount = new TokenAmount(WBTC, JSBI.BigInt(10 ** 7))
const positions = [
{
feesEarned: {
DAI: 1000,
WBTC: 0.005,
},
feeLevel: FEE_BIPS.FIVE,
tokenAmount0: daiAmount,
tokenAmount1: wbtcAmount,
tickLower: 40000,
tickUpper: 60000,
},
{
feesEarned: {
DAI: 1000,
WBTC: 0.005,
},
feeLevel: FEE_BIPS.THIRTY,
tokenAmount0: daiAmount,
tokenAmount1: wbtcAmount,
tickLower: 45000,
tickUpper: 55000,
},
]
// const FEE_BIPS = {
// FIVE: basisPointsToPercent(5),
// THIRTY: basisPointsToPercent(30),
// ONE_HUNDRED: basisPointsToPercent(100),
// }
// const daiAmount = new TokenAmount(DAI, JSBI.BigInt(500 * 10 ** 18))
// const wbtcAmount = new TokenAmount(WBTC, JSBI.BigInt(10 ** 7))
// const positions = [
// {
// feesEarned: {
// DAI: 1000,
// WBTC: 0.005,
// },
// feeLevel: FEE_BIPS.FIVE,
// tokenAmount0: daiAmount,
// tokenAmount1: wbtcAmount,
// tickLower: 40000,
// tickUpper: 60000,
// },
// {
// feesEarned: {
// DAI: 1000,
// WBTC: 0.005,
// },
// feeLevel: FEE_BIPS.THIRTY,
// tokenAmount0: daiAmount,
// tokenAmount1: wbtcAmount,
// tickLower: 45000,
// tickUpper: 55000,
// },
// ]
// const positions: Position[] = []
export default {
title: 'PositionList',
}
const Template: Story<PositionListProps> = (args) => <Component {...args} />
// const Template: Story<PositionListProps> = (args) => <Component {...args} />
export const PositionList = Template.bind({})
PositionList.args = {
positions,
showUnwrapped: true,
}
// export const PositionList = Template.bind({})
// PositionList.args = {
// positions,
// showUnwrapped: true,
// }

@ -1,63 +1,10 @@
import Badge, { BadgeVariant } from 'components/Badge'
import DoubleCurrencyLogo from 'components/DoubleLogo'
import PositionListItem from 'components/PositionListItem'
import React from 'react'
import { AlertTriangle } from 'react-feather'
import { useTranslation } from 'react-i18next'
import { unwrappedToken } from 'utils/wrappedCurrency'
import styled, { keyframes } from 'styled-components'
import { Link } from 'react-router-dom'
import styled from 'styled-components'
import { MEDIA_WIDTHS } from 'theme'
import { Position } from 'types/position'
import { PositionDetails } from 'types/position'
const ActiveDot = styled.span`
background-color: ${({ theme }) => theme.success};
border-radius: 50%;
height: 8px;
width: 8px;
margin-right: 4px;
`
const Row = styled(Link)`
align-items: center;
border-radius: 20px;
display: flex;
flex-direction: column;
color: ${({ theme }) => theme.text1};
margin: 8px 0;
padding: 8px;
text-decoration: none;
font-weight: 500;
&:first-of-type {
margin: 0 0 8px 0;
}
&:last-of-type {
margin: 8px 0 0 0;
}
& > div:not(:first-child) {
text-align: right;
min-width: 18%;
}
@media screen and (min-width: ${MEDIA_WIDTHS.upToSmall}px) {
flex-direction: row;
}
:hover {
background-color: ${({ theme }) => theme.bg1};
}
`
const BadgeText = styled.div`
font-weight: 500;
font-size: 14px;
`
const BadgeWrapper = styled.div`
font-size: 14px;
`
const DataLineItem = styled.div`
text-align: right;
font-size: 14px;
`
const DoubleArrow = styled.span`
color: ${({ theme }) => theme.text3};
`
const DesktopHeader = styled.div`
display: none;
font-size: 14px;
@ -78,81 +25,7 @@ const DesktopHeader = styled.div`
}
}
`
const loadingAnimation = keyframes`
0% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
`
const LoadingRows = styled.div`
display: grid;
grid-column-gap: 0.5em;
grid-row-gap: 0.8em;
grid-template-columns: repeat(3, 1fr);
& > div {
animation: ${loadingAnimation} 1.5s infinite;
animation-fill-mode: both;
background: linear-gradient(
to left,
${({ theme }) => theme.bg3} 25%,
${({ theme }) => theme.bg5} 50%,
${({ theme }) => theme.bg3} 75%
);
background-size: 400%;
border-radius: 0.2em;
height: 1.4em;
will-change: background-position;
}
& > div:nth-child(4n + 1) {
grid-column: 1 / 3;
}
& > div:nth-child(4n) {
grid-column: 3 / 4;
margin-bottom: 2em;
}
`
const RangeData = styled.div`
display: flex;
flex-direction: column;
width: 100%;
& > div {
align-items: center;
display: flex;
justify-content: space-between;
width: 100%;
}
@media screen and (min-width: ${MEDIA_WIDTHS.upToSmall}px) {
display: block;
& > div {
display: block;
}
}
`
const AmountData = styled.div`
display: none;
@media screen and (min-width: ${MEDIA_WIDTHS.upToSmall}px) {
display: block;
}
`
const FeeData = styled.div`
display: none;
@media screen and (min-width: ${MEDIA_WIDTHS.upToSmall}px) {
display: block;
}
`
const LabelData = styled.div`
align-items: center;
display: flex;
flex: 1 1 auto;
justify-content: space-between;
width: 100%;
@media screen and (min-width: ${MEDIA_WIDTHS.upToSmall}px) {
display: block;
}
`
const MobileHeader = styled.div`
font-weight: medium;
font-size: 16px;
@ -161,28 +34,14 @@ const MobileHeader = styled.div`
display: none;
}
`
const PrimaryPositionIdData = styled.div`
display: flex;
flex-direction: row;
align-items: center;
padding: 6px 0 12px 0;
> * {
margin-right: 8px;
}
`
const DataText = styled.div`
font-weight: 500;
`
export type PositionListProps = React.PropsWithChildren<{
loading: boolean
positions: Position[]
showUnwrapped?: boolean
positions: PositionDetails[]
}>
export default function PositionList({ loading, positions, showUnwrapped }: PositionListProps) {
export default function PositionList({ positions }: PositionListProps) {
const { t } = useTranslation()
return (
<>
<DesktopHeader>
@ -192,90 +51,10 @@ export default function PositionList({ loading, positions, showUnwrapped }: Posi
<div>{t('Fees Earned')}</div>
</DesktopHeader>
<MobileHeader>Your positions</MobileHeader>
{loading ? (
<LoadingRows>
<div />
<div />
<div />
<div />
<div />
<div />
<div />
<div />
<div />
<div />
<div />
<div />
</LoadingRows>
) : (
positions.map((position) => {
const { feeLevel, feesEarned, tokenAmount0, tokenAmount1 } = position
const symbol0 = tokenAmount0.token.symbol || ''
const symbol1 = tokenAmount1.token.symbol || ''
const currency0 = showUnwrapped ? tokenAmount0.token : unwrappedToken(tokenAmount0.token)
const currency1 = showUnwrapped ? tokenAmount1.token : unwrappedToken(tokenAmount1.token)
const limitCrossed = tokenAmount0.equalTo(0) || tokenAmount1.equalTo(0)
const key = `${feeLevel.toFixed()}-${symbol0}-${tokenAmount0.toFixed(2)}-${symbol1}-${tokenAmount1.toFixed(
2
)}`
return (
<Row key={key} to="/asdf">
<LabelData>
<PrimaryPositionIdData>
<DoubleCurrencyLogo currency0={currency0} currency1={currency1} size={16} margin />
<DataText>
&nbsp;{symbol0}&nbsp;/&nbsp;{symbol1}
</DataText>
&nbsp;
<Badge>
<BadgeText>{feeLevel.toSignificant(2)}%</BadgeText>
</Badge>
</PrimaryPositionIdData>
<BadgeWrapper>
{limitCrossed ? (
<Badge variant={BadgeVariant.WARNING}>
<AlertTriangle width={14} height={14} style={{ marginRight: '4px' }} />
&nbsp;
<BadgeText>{t('Out of range')}</BadgeText>
</Badge>
) : (
<Badge variant={BadgeVariant.DEFAULT}>
<ActiveDot /> &nbsp;
<BadgeText>{t('Active')}</BadgeText>
</Badge>
)}
</BadgeWrapper>
</LabelData>
<RangeData>
<DataLineItem>
1,672 <DoubleArrow></DoubleArrow> 1,688 {symbol0}&nbsp;/&nbsp;{symbol1}
</DataLineItem>
<DataLineItem>
0.0002 <DoubleArrow></DoubleArrow> 0.0001 {symbol1}&nbsp;/&nbsp;{symbol0}
</DataLineItem>
</RangeData>
<AmountData>
<DataLineItem>
{tokenAmount0.toSignificant()}&nbsp;{symbol0}
</DataLineItem>
<DataLineItem>
{tokenAmount1.toSignificant()}&nbsp;{symbol1}
</DataLineItem>
</AmountData>
<FeeData>
<DataLineItem>
{feesEarned[symbol0]}&nbsp;{symbol0}
</DataLineItem>
<DataLineItem>
{feesEarned[symbol1]}&nbsp;{symbol1}
</DataLineItem>
</FeeData>
</Row>
)
})
)}
{positions.map((p, i) => {
const key = `${i}-${p.nonce.toString()} ${p.token0} ${p.token1} ${p.tokensOwed0} ${p.tokensOwed1}`
return <PositionListItem key={key} positionDetails={p} positionIndex={i} />
})}
</>
)
}

@ -0,0 +1,31 @@
// import { Story } from '@storybook/react/types-6-0'
// import { FeeAmount, MAX_TICK, MIN_TICK, TICK_SPACINGS } from '@uniswap/v3-sdk'
// import { BigNumber } from 'ethers'
// import React from 'react'
// import { Position } from 'types/position'
// import Component, { PositionListItemProps } from './index'
// const position: Position = {
// nonce: BigNumber.from(0),
// operator: '',
// token0: '',
// token1: '',
// fee: FeeAmount.LOW,
// tickLower: MIN_TICK(TICK_SPACINGS[FeeAmount.LOW]),
// tickUpper: MAX_TICK(TICK_SPACINGS[FeeAmount.LOW]),
// liquidity,
// feeGrowthInside0LastX128fee
// feeGrowthInside0LastX128
// feeGrowthInside1LastX128
// tokensOwed0
// tokensOwed1
// }
export default {
title: 'PositionListItem',
}
// const Template: Story<PositionListItemProps> = (args) => <Component {...args} />
// export const PositionListItem = Template.bind({})
// PositionListItem.args = {position}

@ -0,0 +1,244 @@
import React, { useMemo } from 'react'
import { Position } from '@uniswap/v3-sdk'
import Badge, { BadgeVariant } from 'components/Badge'
import DoubleCurrencyLogo from 'components/DoubleLogo'
import { PoolState, usePool } from 'data/Pools'
import { useToken } from 'hooks/Tokens'
import { AlertTriangle } from 'react-feather'
import { useTranslation } from 'react-i18next'
import { Link } from 'react-router-dom'
import styled from 'styled-components'
import { MEDIA_WIDTHS } from 'theme'
import { PositionDetails } from 'types/position'
import { basisPointsToPercent } from 'utils'
import { TokenAmount } from '@uniswap/sdk-core'
import { formatPrice, formatTokenAmount } from 'utils/formatTokenAmount'
import Loader from 'components/Loader'
import { unwrappedToken } from 'utils/wrappedCurrency'
const ActiveDot = styled.span`
background-color: ${({ theme }) => theme.success};
border-radius: 50%;
height: 8px;
width: 8px;
margin-right: 4px;
`
const Row = styled(Link)`
align-items: center;
border-radius: 20px;
display: flex;
flex-direction: column;
color: ${({ theme }) => theme.text1};
margin: 8px 0;
padding: 8px;
text-decoration: none;
font-weight: 500;
&:first-of-type {
margin: 0 0 8px 0;
}
&:last-of-type {
margin: 8px 0 0 0;
}
& > div:not(:first-child) {
text-align: right;
min-width: 18%;
}
@media screen and (min-width: ${MEDIA_WIDTHS.upToSmall}px) {
flex-direction: row;
}
:hover {
background-color: ${({ theme }) => theme.bg1};
}
`
const BadgeText = styled.div`
font-weight: 500;
font-size: 14px;
`
const BadgeWrapper = styled.div`
font-size: 14px;
`
const DataLineItem = styled.div`
text-align: right;
font-size: 14px;
`
const DoubleArrow = styled.span`
color: ${({ theme }) => theme.text3};
`
const RangeData = styled.div`
display: flex;
flex-direction: column;
width: 100%;
& > div {
align-items: center;
display: flex;
justify-content: space-between;
width: 100%;
}
@media screen and (min-width: ${MEDIA_WIDTHS.upToSmall}px) {
display: block;
& > div {
display: block;
}
}
`
const AmountData = styled.div`
display: none;
@media screen and (min-width: ${MEDIA_WIDTHS.upToSmall}px) {
display: block;
}
`
const FeeData = styled.div`
display: none;
@media screen and (min-width: ${MEDIA_WIDTHS.upToSmall}px) {
display: block;
}
`
const LabelData = styled.div`
align-items: center;
display: flex;
flex: 1 1 auto;
justify-content: space-between;
width: 100%;
@media screen and (min-width: ${MEDIA_WIDTHS.upToSmall}px) {
display: block;
}
`
const PrimaryPositionIdData = styled.div`
display: flex;
flex-direction: row;
align-items: center;
padding: 6px 0 12px 0;
> * {
margin-right: 8px;
}
`
const DataText = styled.div`
font-weight: 500;
`
export interface PositionListItemProps {
positionDetails: PositionDetails
positionIndex: number
}
export default function PositionListItem({ positionDetails, positionIndex }: PositionListItemProps) {
const { t } = useTranslation()
const {
token0: token0Address,
token1: token1Address,
fee: feeAmount,
liquidity,
tickLower,
tickUpper,
feeGrowthInside0LastX128,
feeGrowthInside1LastX128,
} = positionDetails
const token0 = useToken(token0Address)
const token1 = useToken(token1Address)
const currency0 = token0 ? unwrappedToken(token0) : undefined
const currency1 = token1 ? unwrappedToken(token1) : undefined
// construct Position from details returned
const [poolState, pool] = usePool(currency0 ?? undefined, currency1 ?? undefined, feeAmount)
const position = useMemo(() => {
if (pool) {
return new Position({ pool, liquidity, tickLower, tickUpper })
}
return undefined
}, [liquidity, pool, tickLower, tickUpper])
const poolLoading = poolState === PoolState.LOADING
// liquidity amounts in tokens
const amount0: TokenAmount | undefined = position?.amount0
const amount1: TokenAmount | undefined = position?.amount1
const formattedAmount0 = formatTokenAmount(amount0, 4)
const formattedAmount1 = formatTokenAmount(amount1, 4)
// prices
const price0Lower = position ? position.token0PriceLower : undefined
const price0Upper = position ? position.token0PriceUpper : undefined
const price1Lower = price0Upper ? price0Upper.invert() : undefined
const price1Upper = price0Lower ? price0Lower.invert() : undefined
// check if price is within range
const outOfRange: boolean = pool ? pool.tickCurrent < tickLower || pool.tickCurrent > tickUpper : false
const positionSummaryLink = '/pool/' + positionIndex.toString()
return (
<Row to={positionSummaryLink}>
<LabelData>
<PrimaryPositionIdData>
<DoubleCurrencyLogo currency0={currency0 ?? undefined} currency1={currency1 ?? undefined} size={16} margin />
<DataText>
&nbsp;{currency0?.symbol}&nbsp;/&nbsp;{currency1?.symbol}
</DataText>
&nbsp;
<Badge>
<BadgeText>{basisPointsToPercent(feeAmount / 100).toSignificant()}%</BadgeText>
</Badge>
</PrimaryPositionIdData>
<BadgeWrapper>
{outOfRange ? (
<Badge variant={BadgeVariant.WARNING}>
<AlertTriangle width={14} height={14} style={{ marginRight: '4px' }} />
&nbsp;
<BadgeText>{t('Out of range')}</BadgeText>
</Badge>
) : (
<Badge variant={BadgeVariant.DEFAULT}>
<ActiveDot /> &nbsp;
<BadgeText>{t('Active')}</BadgeText>
</Badge>
)}
</BadgeWrapper>
</LabelData>
<RangeData>
{price0Lower && price1Lower && price0Upper && price1Upper ? (
<>
<DataLineItem>
{formatPrice(price0Lower, 4)} <DoubleArrow></DoubleArrow> {formatPrice(price0Upper, 4)}{' '}
{currency0?.symbol}&nbsp;/&nbsp;
{currency1?.symbol}
</DataLineItem>
<DataLineItem>
{formatPrice(price1Lower, 4)} <DoubleArrow></DoubleArrow> {formatPrice(price1Upper, 4)}{' '}
{currency1?.symbol}&nbsp;/&nbsp;
{currency0?.symbol}
</DataLineItem>
</>
) : (
<Loader />
)}
</RangeData>
<AmountData>
{!poolLoading ? (
<>
<DataLineItem>
{formattedAmount0}&nbsp;{currency0?.symbol}
</DataLineItem>
<DataLineItem>
{formattedAmount1}&nbsp;{currency1?.symbol}
</DataLineItem>
</>
) : (
<Loader />
)}
</AmountData>
<FeeData>
<DataLineItem>
{feeGrowthInside0LastX128.toString()}&nbsp;{currency0?.symbol}
</DataLineItem>
<DataLineItem>
{feeGrowthInside1LastX128.toString()}&nbsp;{currency1?.symbol}
</DataLineItem>
</FeeData>
</Row>
)
}

@ -1,7 +1,6 @@
import { ChainId, Percent, Token, WETH9 } from '@uniswap/sdk-core'
import { AbstractConnector } from '@web3-react/abstract-connector'
import JSBI from 'jsbi'
import { fortmatic, injected, portis, walletconnect, walletlink } from '../connectors'
export const MULTICALL_ADDRESSES: { [chainId in ChainId]: string } = {

@ -1,4 +1,3 @@
import { UniswapV3Pool } from './../types/v3/UniswapV3Pool.d'
import { Contract } from '@ethersproject/contracts'
import { abi as GOVERNANCE_ABI } from '@uniswap/governance/build/GovernorAlpha.json'
import { abi as UNI_ABI } from '@uniswap/governance/build/Uni.json'
@ -6,10 +5,9 @@ import { abi as STAKING_REWARDS_ABI } from '@uniswap/liquidity-staker/build/Stak
import { abi as MERKLE_DISTRIBUTOR_ABI } from '@uniswap/merkle-distributor/build/MerkleDistributor.json'
import { ChainId, WETH9 } from '@uniswap/sdk-core'
import { abi as IUniswapV2PairABI } from '@uniswap/v2-core/build/IUniswapV2Pair.json'
import { abi as NFTPositionManagerABI } from '@uniswap/v3-periphery/artifacts/contracts/NonfungiblePositionManager.sol/NonfungiblePositionManager.json'
import { abi as V3FactoryABI } from '@uniswap/v3-core/artifacts/contracts/UniswapV3Factory.sol/UniswapV3Factory.json'
import { abi as TickLensABI } from '@uniswap/v3-periphery/artifacts/contracts/lens/TickLens.sol/TickLens.json'
import { abi as V3PoolABI } from '@uniswap/v3-core/artifacts/contracts/UniswapV3Pool.sol/UniswapV3Pool.json'
import { abi as TickLensABI } from '@uniswap/v3-periphery/artifacts/contracts/lens/TickLens.sol/TickLens.json'
import ARGENT_WALLET_DETECTOR_ABI from 'abis/argent-wallet-detector.json'
import ENS_PUBLIC_RESOLVER_ABI from 'abis/ens-public-resolver.json'
@ -17,6 +15,7 @@ import ENS_ABI from 'abis/ens-registrar.json'
import ERC20_ABI from 'abis/erc20.json'
import ERC20_BYTES32_ABI from 'abis/erc20_bytes32.json'
import MIGRATOR_ABI from 'abis/migrator.json'
import MULTICALL_ABI from 'abis/multicall2.json'
import { Unisocks } from 'abis/types/Unisocks'
import UNISOCKS_ABI from 'abis/unisocks.json'
import WETH_ABI from 'abis/weth.json'
@ -28,11 +27,11 @@ import {
UNI,
MULTICALL_ADDRESSES,
} from 'constants/index'
import MULTICALL_ABI from 'abis/multicall2.json'
import { abi as NFTPositionManagerABI } from '@uniswap/v3-periphery/artifacts/contracts/NonfungiblePositionManager.sol/NonfungiblePositionManager.json'
import { V1_EXCHANGE_ABI, V1_FACTORY_ABI, V1_FACTORY_ADDRESSES } from 'constants/v1'
import { NONFUNGIBLE_POSITION_MANAGER_ADDRESSES, FACTORY_ADDRESSES, TICK_LENS_ADDRESSES } from 'constants/v3'
import { FACTORY_ADDRESSES, NONFUNGIBLE_POSITION_MANAGER_ADDRESSES, TICK_LENS_ADDRESSES } from 'constants/v3'
import { useMemo } from 'react'
import { TickLens, UniswapV3Factory } from 'types/v3'
import { TickLens, UniswapV3Factory, UniswapV3Pool } from 'types/v3'
import { NonfungiblePositionManager } from 'types/v3/NonfungiblePositionManager'
import { getContract } from 'utils'
import { useActiveWeb3React } from './index'

@ -1,63 +0,0 @@
import { OptionalMethodInputs, useSingleCallResult, useSingleContractMultipleData } from 'state/multicall/hooks'
import { Position } from 'types/position'
import { useV3NFTPositionManagerContract } from './useContract'
interface UseV3PositionsResults {
error?: (string | boolean) | (string | boolean)[]
loading: boolean
positions: Position[]
}
export function useV3Positions(account: string | null | undefined): UseV3PositionsResults {
const positionManager = useV3NFTPositionManagerContract()
let loading = false
let error: any
const {
error: balanceOfError,
loading: balanceOfLoading,
result: balanceOfResult,
} = useSingleCallResult(positionManager, 'balanceOf', [account || undefined])
loading = balanceOfLoading
error = balanceOfError
const tokenOfOwnerByIndexArgs: OptionalMethodInputs[] = balanceOfResult
? balanceOfResult.filter((x) => Boolean(x)).map((index) => [account, index])
: []
const tokensCallResults = useSingleContractMultipleData(
positionManager,
'tokenOfOwnerByIndex',
tokenOfOwnerByIndexArgs
)
const callData: any[] = []
tokensCallResults.forEach(({ error: e, loading: l, result: data }) => {
if (e && !error) {
error = e
}
loading = loading || l
if (data) {
callData.push([account, data])
}
})
const positionsCallResults = useSingleContractMultipleData(positionManager, 'positions', callData)
const positions: any[] = []
positionsCallResults.forEach(({ error: e, loading: l, result: data }) => {
if (e) {
if (!error) {
error = e
}
if (error && Array.isArray(error)) {
error = [...error, error]
}
}
loading = loading || l
if (data) {
positions.push(data)
}
})
return { error, loading, positions }
}

@ -0,0 +1,95 @@
import { useSingleCallResult, useSingleContractMultipleData } from 'state/multicall/hooks'
import { useMemo } from 'react'
import { PositionDetails } from 'types/position'
import { useV3NFTPositionManagerContract } from './useContract'
import JSBI from 'jsbi'
interface UseV3PositionsResults {
error?: (string | boolean) | (string | boolean)[]
loading: boolean
positions: PositionDetails[] | undefined
}
export function useV3Positions(account: string | null | undefined): UseV3PositionsResults {
const positionManager = useV3NFTPositionManagerContract()
const { loading: balanceLoading, error: balanceError, result: balanceResult } = useSingleCallResult(
positionManager ?? undefined,
'balanceOf',
[account ?? undefined]
)
const accountBalance: number | undefined = balanceResult
? parseFloat(JSBI.BigInt(balanceResult[0]).toString())
: undefined
const positionIndicesArgs = useMemo(() => {
if (accountBalance && account) {
const tokenRequests = []
for (let i = 0; i < accountBalance; i++) {
tokenRequests.push([account, i])
}
return tokenRequests
}
return []
}, [account, accountBalance])
const positionIndicesResults = useSingleContractMultipleData(
positionManager ?? undefined,
'tokenOfOwnerByIndex',
positionIndicesArgs
)
const positionIndicesLoading = useMemo(() => positionIndicesResults.some(({ loading }) => loading), [
positionIndicesResults,
])
const positionIndicesError = useMemo(() => positionIndicesResults.some(({ error }) => error), [
positionIndicesResults,
])
const formattedIndicesArgs = useMemo(() => {
if (positionIndicesResults && account) {
return positionIndicesResults.map((call) => {
return [call.result?.[0] ? parseFloat(JSBI.BigInt(call.result?.[0]).toString()) : undefined]
})
}
return []
}, [account, positionIndicesResults])
const positionsResults = useSingleContractMultipleData(
positionManager ?? undefined,
'positions',
formattedIndicesArgs
)
const positionResultsLoading = useMemo(() => positionsResults.some(({ loading }) => loading), [positionsResults])
const positionResultsError = useMemo(() => positionsResults.some(({ error }) => error), [positionsResults])
const loading = balanceLoading || positionResultsLoading || positionIndicesLoading
const positions = useMemo(() => {
if (positionsResults && !loading) {
return positionsResults.map((entry) => {
const rp = entry.result
return {
fee: rp?.fee,
feeGrowthInside0LastX128: rp?.feeGrowthInside0LastX128,
feeGrowthInside1LastX128: rp?.feeGrowthInside1LastX128,
liquidity: rp?.liquidity,
nonce: rp?.nonce,
operator: rp?.operator,
tickLower: rp?.tickLower,
tickUpper: rp?.tickUpper,
token0: rp?.token0,
token1: rp?.token1,
tokensOwed0: rp?.tokensOwed0,
tokensOwed1: rp?.tokensOwed1,
}
})
}
return undefined
}, [positionsResults, loading])
return {
error: balanceError || positionIndicesError || positionResultsError,
loading,
positions,
}
}

@ -32,6 +32,7 @@ import VotePage from './Vote/VotePage'
import { RedirectDuplicateTokenIdsV2 } from './AddLiquidityV2/redirects'
import AddLiquidity from './AddLiquidity'
import AddLiquidityV2 from './AddLiquidityV2'
import { PositionPage } from './Pool/PositionPage'
const AppWrapper = styled.div`
display: flex;
@ -106,11 +107,11 @@ export default function App() {
<Route exact strict path="/find" component={PoolFinder} />
<Route exact strict path="/pool/v2" component={PoolV2} />
<Route exact strict path="/pool" component={Pool} />
<Route exact strict path="/pool/:positionIndex" component={PositionPage} />
<Route exact path="/add" component={AddLiquidity} />
<Route exact path="/add/v2/" component={AddLiquidityV2} />
<Route exact path="/add/v2/:currencyIdA" component={AddLiquidityV2} />
<Route exact path="/add/:currencyIdA" component={AddLiquidity} />
<Route exact strict path="/add/v2/:currencyIdA?/:currencyIdB?" component={RedirectDuplicateTokenIdsV2} />
<Route

@ -0,0 +1,212 @@
import React, { useMemo } from 'react'
import { Position } from '@uniswap/v3-sdk'
import { PoolState, usePool } from 'data/Pools'
import { useActiveWeb3React } from 'hooks'
import { useToken } from 'hooks/Tokens'
import { useV3Positions } from 'hooks/useV3Positions'
import { RouteComponentProps } from 'react-router-dom'
import { unwrappedToken } from 'utils/wrappedCurrency'
import { LoadingRows } from './styleds'
import styled from 'styled-components'
import { AutoColumn } from 'components/Column'
import { RowBetween, RowFixed } from 'components/Row'
import DoubleCurrencyLogo from 'components/DoubleLogo'
import { TYPE } from 'theme'
import Badge, { BadgeVariant } from 'components/Badge'
import { basisPointsToPercent } from 'utils'
import { ButtonPrimary } from 'components/Button'
import { DarkCard } from 'components/Card'
import CurrencyLogo from 'components/CurrencyLogo'
import { AlertTriangle } from 'react-feather'
import { useTranslation } from 'react-i18next'
const PageWrapper = styled.div`
min-width: 800px;
`
const BadgeWrapper = styled.div`
font-size: 14px;
`
const BadgeText = styled.div`
font-weight: 500;
font-size: 14px;
`
const ResponsiveGrid = styled.div`
width: 100%;
display: grid;
grid-gap: 1em;
grid-template-columns: 1.5fr repeat(4, 1fr);
@media screen and (max-width: 900px) {
grid-template-columns: 1.5fr repeat(4, 1fr);
& :nth-child(4) {
display: none;
}
}
@media screen and (max-width: 700px) {
grid-template-columns: 20px 1.5fr repeat(3, 1fr);
& :nth-child(4) {
display: none;
}
& :nth-child(5) {
display: none;
}
}
`
// responsive text
const Label = styled(TYPE.label)<{ end?: boolean }>`
display: flex;
font-size: 16px;
justify-content: ${({ end }) => (end ? 'flex-end' : 'flex-start')};
align-items: center;
`
const ActiveDot = styled.span`
background-color: ${({ theme }) => theme.success};
border-radius: 50%;
height: 8px;
width: 8px;
margin-right: 4px;
`
export function PositionPage({
match: {
params: { positionIndex },
},
}: RouteComponentProps<{ positionIndex?: string }>) {
const { account } = useActiveWeb3React()
const { t } = useTranslation()
const { loading, positions } = useV3Positions(account ?? undefined)
const positionDetails = positionIndex && positions ? positions[parseInt(positionIndex)] : undefined
const {
token0: token0Address,
token1: token1Address,
fee: feeAmount,
liquidity,
tickLower,
tickUpper,
// feeGrowthInside0LastX128,
// feeGrowthInside1LastX128,
} = positionDetails || {}
const token0 = useToken(token0Address)
const token1 = useToken(token1Address)
const currency0 = token0 ? unwrappedToken(token0) : undefined
const currency1 = token1 ? unwrappedToken(token1) : undefined
// construct Position from details returned
const [poolState, pool] = usePool(currency0 ?? undefined, currency1 ?? undefined, feeAmount)
const position = useMemo(() => {
if (pool && tickLower && tickUpper) {
return new Position({ pool, liquidity, tickLower, tickUpper })
}
return undefined
}, [liquidity, pool, tickLower, tickUpper])
// const price0Lower = position ? position.token0PriceLower : undefined
// const price0Upper = position ? position.token0PriceUpper : undefined
// const price1Lower = price0Upper ? price0Upper.invert() : undefined
// const price1Upper = price0Lower ? price0Lower.invert() : undefined
// check if price is within range
const outOfRange: boolean =
pool && tickLower && tickUpper ? pool.tickCurrent < tickLower || pool.tickCurrent > tickUpper : false
return loading || poolState === PoolState.LOADING || !feeAmount ? (
<LoadingRows>
<div />
<div />
<div />
<div />
<div />
<div />
<div />
<div />
<div />
<div />
<div />
<div />
</LoadingRows>
) : (
<PageWrapper>
<AutoColumn gap="lg">
<AutoColumn gap="sm">
<RowBetween>
<RowFixed>
<DoubleCurrencyLogo currency0={currency0} currency1={currency1} size={20} margin={true} />
<TYPE.label fontSize={'20px'} mr="10px">
&nbsp;{currency0?.symbol}&nbsp;/&nbsp;{currency1?.symbol}
</TYPE.label>
<Badge>
<BadgeText>{basisPointsToPercent(feeAmount / 100).toSignificant()}%</BadgeText>
</Badge>
</RowFixed>
<ButtonPrimary width="200px" padding="8px" borderRadius="12px">
Remove liquidity
</ButtonPrimary>
</RowBetween>
<RowBetween>
<BadgeWrapper>
{outOfRange ? (
<Badge variant={BadgeVariant.WARNING}>
<AlertTriangle width={14} height={14} style={{ marginRight: '4px' }} />
&nbsp;
<BadgeText>{t('Out of range')}</BadgeText>
</Badge>
) : (
<Badge variant={BadgeVariant.DEFAULT}>
<ActiveDot /> &nbsp;
<BadgeText>{t('Active')}</BadgeText>
</Badge>
)}
</BadgeWrapper>
</RowBetween>
</AutoColumn>
<DarkCard>
<AutoColumn gap="lg">
<ResponsiveGrid>
<Label>Tokens</Label>
<Label end={true}>Entry</Label>
<Label end={true}>Current</Label>
<Label end={true}>Fees</Label>
<Label end={true}>USD Value</Label>
</ResponsiveGrid>
<ResponsiveGrid>
<RowFixed>
<CurrencyLogo currency={currency0} />
<TYPE.label ml="10px">{currency0?.symbol}</TYPE.label>
</RowFixed>
<Label end={true}>{position?.amount0.toSignificant(4)}</Label>
<Label end={true}>{position?.amount0.toSignificant(4)}</Label>
<Label end={true}>1</Label>
<Label end={true}>$100</Label>
</ResponsiveGrid>
<ResponsiveGrid>
<RowFixed>
<CurrencyLogo currency={currency1} />
<TYPE.label ml="10px">{currency1?.symbol}</TYPE.label>
</RowFixed>
<Label end={true}>{position?.amount1.toSignificant(4)}</Label>
<Label end={true}>{position?.amount1.toSignificant(4)}</Label>
<Label end={true}>1</Label>
<Label end={true}>$100</Label>
</ResponsiveGrid>
</AutoColumn>
</DarkCard>
<DarkCard>
<AutoColumn gap="lg">
<TYPE.label>Position Limits</TYPE.label>
</AutoColumn>
</DarkCard>
</AutoColumn>
</PageWrapper>
)
}

@ -1,3 +1,4 @@
import React, { useContext, useMemo } from 'react'
import Badge, { BadgeVariant } from 'components/Badge'
import { ButtonGray, ButtonPrimary } from 'components/Button'
import { AutoColumn } from 'components/Column'
@ -6,14 +7,15 @@ import { SwapPoolTabs } from 'components/NavigationTabs'
import PositionList from 'components/PositionList'
import { RowBetween, RowFixed } from 'components/Row'
import { useActiveWeb3React } from 'hooks'
import { useV3Positions } from 'hooks/useV3PositionManager'
import React, { useContext, useMemo } from 'react'
import { useV3Positions } from 'hooks/useV3Positions'
import { BookOpen, ChevronDown, Download, Inbox, Info, PlusCircle } from 'react-feather'
import { useTranslation } from 'react-i18next'
import styled, { ThemeContext } from 'styled-components'
import { Link } from 'react-router-dom'
import { useWalletModalToggle } from 'state/application/hooks'
import styled, { ThemeContext } from 'styled-components'
import { HideSmall, MEDIA_WIDTHS, TYPE } from 'theme'
import { PositionDetails } from 'types/position'
import { LoadingRows } from './styleds'
const PageWrapper = styled(AutoColumn)`
max-width: 870px;
@ -91,24 +93,27 @@ const MainContentWrapper = styled.main`
`
export default function Pool() {
const { account } = useActiveWeb3React()
const { error, loading, positions } = useV3Positions(account)
const toggleWalletModal = useWalletModalToggle()
const { t } = useTranslation()
const theme = useContext(ThemeContext)
const { error, positions } = useV3Positions(account)
if (error) {
console.error('error fetching v3 positions', error)
}
const hasPositions = Boolean(positions?.length > 0)
const hasPositions = useMemo(() => Boolean(positions && positions.length > 0), [positions])
const numInactivePositions = useMemo(() => {
return positions.reduce((acc: any, position: any) => {
const { tokenAmount0, tokenAmount1 } = position
const limitCrossed = tokenAmount0.equalTo(BigInt(0)) || tokenAmount1.equalTo(BigInt(0))
return limitCrossed ? acc + 1 : acc
}, 0)
}, [positions])
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 showMigrateHeaderLink = Boolean(hasV2Liquidity && hasPositions)
@ -182,9 +187,9 @@ export default function Pool() {
</TitleRow>
<MainContentWrapper>
{hasPositions ? (
<PositionList loading={loading} positions={positions} />
) : (
{hasPositions && positions ? (
<PositionList positions={positions} />
) : positions && !hasPositions ? (
<NoLiquidity>
<TYPE.largeHeader color={theme.text3} textAlign="center">
<Inbox />
@ -208,6 +213,21 @@ export default function Pool() {
)
)}
</NoLiquidity>
) : (
<LoadingRows>
<div />
<div />
<div />
<div />
<div />
<div />
<div />
<div />
<div />
<div />
<div />
<div />
</LoadingRows>
)}
</MainContentWrapper>
</AutoColumn>

@ -1,5 +1,5 @@
import { Text } from 'rebass'
import styled from 'styled-components'
import styled, { keyframes } from 'styled-components'
export const Wrapper = styled.div`
position: relative;
@ -55,3 +55,40 @@ export const Dots = styled.span`
}
}
`
const loadingAnimation = keyframes`
0% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
`
export const LoadingRows = styled.div`
display: grid;
grid-column-gap: 0.5em;
grid-row-gap: 0.8em;
grid-template-columns: repeat(3, 1fr);
& > div {
animation: ${loadingAnimation} 1.5s infinite;
animation-fill-mode: both;
background: linear-gradient(
to left,
${({ theme }) => theme.bg1} 25%,
${({ theme }) => theme.bg2} 50%,
${({ theme }) => theme.bg1} 75%
);
background-size: 400%;
border-radius: 12px;
height: 2.4em;
will-change: background-position;
}
& > div:nth-child(4n + 1) {
grid-column: 1 / 3;
}
& > div:nth-child(4n) {
grid-column: 3 / 4;
margin-bottom: 2em;
}
`

@ -145,7 +145,7 @@ export function useDerivedMintInfo(
const price = useMemo(() => {
// if no liquidity use typed value
if (noLiquidity) {
const parsedAmount = tryParseAmount(startPriceTypedValue, tokenA)
const parsedAmount = tryParseAmount(startPriceTypedValue, tokenB)
if (parsedAmount && tokenA && tokenB) {
const amountOne = tryParseAmount('1', tokenA)
return amountOne ? new Price(tokenA, tokenB, amountOne.raw, parsedAmount.raw) : undefined

@ -108,7 +108,7 @@ function useCallsData(calls: (Call | undefined)[], options?: ListenerOptions): C
)
}
interface CallState {
export interface CallState {
readonly valid: boolean
// the result, or undefined if loading or errored/no data
readonly result: Result | undefined

@ -1,10 +1,14 @@
import { BigNumberish } from '@ethersproject/bignumber'
export interface Position {
feesEarned: Record<string, BigNumberish>
feeLevel: FEE_BIPS
tokenAmount0: TokenAmount
tokenAmount1: TokenAmount
tickLower: BigNumberish
tickUpper: BigNumberish
export interface PositionDetails {
nonce: BigNumber
operator: string
token0: string
token1: string
fee: number
tickLower: number
tickUpper: number
liquidity: BigNumber
feeGrowthInside0LastX128: BigNumber
feeGrowthInside1LastX128: BigNumber
tokensOwed0: BigNumber
tokensOwed1: BigNumber
}

@ -0,0 +1,25 @@
import { Price, TokenAmount } from '@uniswap/sdk-core'
export function formatTokenAmount(amount: TokenAmount | undefined, sigFigs: number) {
if (!amount) {
return '-'
}
if (parseFloat(amount.toFixed(sigFigs)) < 0.0001) {
return '<0.0001'
}
return amount.toSignificant(sigFigs)
}
export function formatPrice(price: Price | undefined, sigFigs: number) {
if (!price) {
return '-'
}
if (parseFloat(price.toFixed(sigFigs)) < 0.0001) {
return '<0.0001'
}
return price.toSignificant(sigFigs)
}

702
yarn.lock

File diff suppressed because it is too large Load Diff