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:
parent
0c0305a53d
commit
9f5584c37d
@ -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>
|
||||
{symbol0} / {symbol1}
|
||||
</DataText>
|
||||
|
||||
<Badge>
|
||||
<BadgeText>{feeLevel.toSignificant(2)}%</BadgeText>
|
||||
</Badge>
|
||||
</PrimaryPositionIdData>
|
||||
<BadgeWrapper>
|
||||
{limitCrossed ? (
|
||||
<Badge variant={BadgeVariant.WARNING}>
|
||||
<AlertTriangle width={14} height={14} style={{ marginRight: '4px' }} />
|
||||
|
||||
<BadgeText>{t('Out of range')}</BadgeText>
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant={BadgeVariant.DEFAULT}>
|
||||
<ActiveDot />
|
||||
<BadgeText>{t('Active')}</BadgeText>
|
||||
</Badge>
|
||||
)}
|
||||
</BadgeWrapper>
|
||||
</LabelData>
|
||||
<RangeData>
|
||||
<DataLineItem>
|
||||
1,672 <DoubleArrow>↔</DoubleArrow> 1,688 {symbol0} / {symbol1}
|
||||
</DataLineItem>
|
||||
<DataLineItem>
|
||||
0.0002 <DoubleArrow>↔</DoubleArrow> 0.0001 {symbol1} / {symbol0}
|
||||
</DataLineItem>
|
||||
</RangeData>
|
||||
<AmountData>
|
||||
<DataLineItem>
|
||||
{tokenAmount0.toSignificant()} {symbol0}
|
||||
</DataLineItem>
|
||||
<DataLineItem>
|
||||
{tokenAmount1.toSignificant()} {symbol1}
|
||||
</DataLineItem>
|
||||
</AmountData>
|
||||
<FeeData>
|
||||
<DataLineItem>
|
||||
{feesEarned[symbol0]} {symbol0}
|
||||
</DataLineItem>
|
||||
<DataLineItem>
|
||||
{feesEarned[symbol1]} {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} />
|
||||
})}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
31
src/components/PositionListItem/PositionListItem.stories.tsx
Normal file
31
src/components/PositionListItem/PositionListItem.stories.tsx
Normal file
@ -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}
|
244
src/components/PositionListItem/index.tsx
Normal file
244
src/components/PositionListItem/index.tsx
Normal file
@ -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>
|
||||
{currency0?.symbol} / {currency1?.symbol}
|
||||
</DataText>
|
||||
|
||||
<Badge>
|
||||
<BadgeText>{basisPointsToPercent(feeAmount / 100).toSignificant()}%</BadgeText>
|
||||
</Badge>
|
||||
</PrimaryPositionIdData>
|
||||
<BadgeWrapper>
|
||||
{outOfRange ? (
|
||||
<Badge variant={BadgeVariant.WARNING}>
|
||||
<AlertTriangle width={14} height={14} style={{ marginRight: '4px' }} />
|
||||
|
||||
<BadgeText>{t('Out of range')}</BadgeText>
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant={BadgeVariant.DEFAULT}>
|
||||
<ActiveDot />
|
||||
<BadgeText>{t('Active')}</BadgeText>
|
||||
</Badge>
|
||||
)}
|
||||
</BadgeWrapper>
|
||||
</LabelData>
|
||||
<RangeData>
|
||||
{price0Lower && price1Lower && price0Upper && price1Upper ? (
|
||||
<>
|
||||
<DataLineItem>
|
||||
{formatPrice(price0Lower, 4)} <DoubleArrow>↔</DoubleArrow> {formatPrice(price0Upper, 4)}{' '}
|
||||
{currency0?.symbol} /
|
||||
{currency1?.symbol}
|
||||
</DataLineItem>
|
||||
<DataLineItem>
|
||||
{formatPrice(price1Lower, 4)} <DoubleArrow>↔</DoubleArrow> {formatPrice(price1Upper, 4)}{' '}
|
||||
{currency1?.symbol} /
|
||||
{currency0?.symbol}
|
||||
</DataLineItem>
|
||||
</>
|
||||
) : (
|
||||
<Loader />
|
||||
)}
|
||||
</RangeData>
|
||||
<AmountData>
|
||||
{!poolLoading ? (
|
||||
<>
|
||||
<DataLineItem>
|
||||
{formattedAmount0} {currency0?.symbol}
|
||||
</DataLineItem>
|
||||
<DataLineItem>
|
||||
{formattedAmount1} {currency1?.symbol}
|
||||
</DataLineItem>
|
||||
</>
|
||||
) : (
|
||||
<Loader />
|
||||
)}
|
||||
</AmountData>
|
||||
<FeeData>
|
||||
<DataLineItem>
|
||||
{feeGrowthInside0LastX128.toString()} {currency0?.symbol}
|
||||
</DataLineItem>
|
||||
<DataLineItem>
|
||||
{feeGrowthInside1LastX128.toString()} {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 }
|
||||
}
|
95
src/hooks/useV3Positions.ts
Normal file
95
src/hooks/useV3Positions.ts
Normal file
@ -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
|
||||
|
212
src/pages/Pool/PositionPage.tsx
Normal file
212
src/pages/Pool/PositionPage.tsx
Normal file
@ -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">
|
||||
{currency0?.symbol} / {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' }} />
|
||||
|
||||
<BadgeText>{t('Out of range')}</BadgeText>
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant={BadgeVariant.DEFAULT}>
|
||||
<ActiveDot />
|
||||
<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
|
||||
|
22
src/types/position.d.ts
vendored
22
src/types/position.d.ts
vendored
@ -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
|
||||
}
|
||||
|
25
src/utils/formatTokenAmount.ts
Normal file
25
src/utils/formatTokenAmount.ts
Normal file
@ -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)
|
||||
}
|
Loading…
Reference in New Issue
Block a user