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',
|
reactDocgen: 'react-docgen-typescript',
|
||||||
reactDocgenTypescriptOptions: {
|
reactDocgenTypescriptOptions: {
|
||||||
shouldExtractLiteralValuesFromEnum: true,
|
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 = {
|
const uniswapBaseTheme = {
|
||||||
brandTitle: 'Uniswap Design',
|
brandTitle: 'Uniswap Design',
|
||||||
brandUrl: 'https://uniswap.org',
|
brandUrl: 'https://uniswap.org',
|
||||||
brandImage: 'https://ipfs.io/ipfs/QmNa8mQkrNKp1WEEeGjFezDmDeodkWRevGFN8JCV7b4Xir'
|
brandImage: 'https://ipfs.io/ipfs/QmNa8mQkrNKp1WEEeGjFezDmDeodkWRevGFN8JCV7b4Xir',
|
||||||
}
|
}
|
||||||
export const light = create({
|
export const light = create({
|
||||||
base: 'light',
|
base: 'light',
|
||||||
...uniswapBaseTheme
|
...uniswapBaseTheme,
|
||||||
})
|
})
|
||||||
|
|
||||||
// export const dark = create({
|
// export const dark = create({
|
||||||
|
@ -28,6 +28,10 @@ export const DarkGreyCard = styled(Card)`
|
|||||||
background-color: ${({ theme }) => theme.bg2};
|
background-color: ${({ theme }) => theme.bg2};
|
||||||
`
|
`
|
||||||
|
|
||||||
|
export const DarkCard = styled(Card)`
|
||||||
|
background-color: ${({ theme }) => theme.bg0};
|
||||||
|
`
|
||||||
|
|
||||||
export const OutlineCard = styled(Card)`
|
export const OutlineCard = styled(Card)`
|
||||||
border: 1px solid ${({ theme }) => theme.bg3};
|
border: 1px solid ${({ theme }) => theme.bg3};
|
||||||
`
|
`
|
||||||
|
@ -1,51 +1,53 @@
|
|||||||
import { Story } from '@storybook/react/types-6-0'
|
// import { Story } from '@storybook/react/types-6-0'
|
||||||
import React from 'react'
|
// import React from 'react'
|
||||||
import { basisPointsToPercent } from 'utils'
|
// import { Position } from 'types/position'
|
||||||
import { DAI, WBTC } from '../../constants'
|
// import { basisPointsToPercent } from 'utils'
|
||||||
import Component, { PositionListProps } from './index'
|
// import { DAI, WBTC } from '../../constants'
|
||||||
import { TokenAmount } from '@uniswap/sdk-core'
|
// import Component, { PositionListProps } from './index'
|
||||||
import JSBI from 'jsbi'
|
// import { TokenAmount } from '@uniswap/sdk-core'
|
||||||
|
// import JSBI from 'jsbi'
|
||||||
|
|
||||||
const FEE_BIPS = {
|
// const FEE_BIPS = {
|
||||||
FIVE: basisPointsToPercent(5),
|
// FIVE: basisPointsToPercent(5),
|
||||||
THIRTY: basisPointsToPercent(30),
|
// THIRTY: basisPointsToPercent(30),
|
||||||
ONE_HUNDRED: basisPointsToPercent(100),
|
// ONE_HUNDRED: basisPointsToPercent(100),
|
||||||
}
|
// }
|
||||||
const daiAmount = new TokenAmount(DAI, JSBI.BigInt(500 * 10 ** 18))
|
// const daiAmount = new TokenAmount(DAI, JSBI.BigInt(500 * 10 ** 18))
|
||||||
const wbtcAmount = new TokenAmount(WBTC, JSBI.BigInt(10 ** 7))
|
// const wbtcAmount = new TokenAmount(WBTC, JSBI.BigInt(10 ** 7))
|
||||||
const positions = [
|
// const positions = [
|
||||||
{
|
// {
|
||||||
feesEarned: {
|
// feesEarned: {
|
||||||
DAI: 1000,
|
// DAI: 1000,
|
||||||
WBTC: 0.005,
|
// WBTC: 0.005,
|
||||||
},
|
// },
|
||||||
feeLevel: FEE_BIPS.FIVE,
|
// feeLevel: FEE_BIPS.FIVE,
|
||||||
tokenAmount0: daiAmount,
|
// tokenAmount0: daiAmount,
|
||||||
tokenAmount1: wbtcAmount,
|
// tokenAmount1: wbtcAmount,
|
||||||
tickLower: 40000,
|
// tickLower: 40000,
|
||||||
tickUpper: 60000,
|
// tickUpper: 60000,
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
feesEarned: {
|
// feesEarned: {
|
||||||
DAI: 1000,
|
// DAI: 1000,
|
||||||
WBTC: 0.005,
|
// WBTC: 0.005,
|
||||||
},
|
// },
|
||||||
feeLevel: FEE_BIPS.THIRTY,
|
// feeLevel: FEE_BIPS.THIRTY,
|
||||||
tokenAmount0: daiAmount,
|
// tokenAmount0: daiAmount,
|
||||||
tokenAmount1: wbtcAmount,
|
// tokenAmount1: wbtcAmount,
|
||||||
tickLower: 45000,
|
// tickLower: 45000,
|
||||||
tickUpper: 55000,
|
// tickUpper: 55000,
|
||||||
},
|
// },
|
||||||
]
|
// ]
|
||||||
|
// const positions: Position[] = []
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'PositionList',
|
title: 'PositionList',
|
||||||
}
|
}
|
||||||
|
|
||||||
const Template: Story<PositionListProps> = (args) => <Component {...args} />
|
// const Template: Story<PositionListProps> = (args) => <Component {...args} />
|
||||||
|
|
||||||
export const PositionList = Template.bind({})
|
// export const PositionList = Template.bind({})
|
||||||
PositionList.args = {
|
// PositionList.args = {
|
||||||
positions,
|
// positions,
|
||||||
showUnwrapped: true,
|
// showUnwrapped: true,
|
||||||
}
|
// }
|
||||||
|
@ -1,63 +1,10 @@
|
|||||||
import Badge, { BadgeVariant } from 'components/Badge'
|
import PositionListItem from 'components/PositionListItem'
|
||||||
import DoubleCurrencyLogo from 'components/DoubleLogo'
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { AlertTriangle } from 'react-feather'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { unwrappedToken } from 'utils/wrappedCurrency'
|
import styled from 'styled-components'
|
||||||
import styled, { keyframes } from 'styled-components'
|
|
||||||
import { Link } from 'react-router-dom'
|
|
||||||
import { MEDIA_WIDTHS } from 'theme'
|
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`
|
const DesktopHeader = styled.div`
|
||||||
display: none;
|
display: none;
|
||||||
font-size: 14px;
|
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`
|
const MobileHeader = styled.div`
|
||||||
font-weight: medium;
|
font-weight: medium;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
@ -161,28 +34,14 @@ const MobileHeader = styled.div`
|
|||||||
display: none;
|
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<{
|
export type PositionListProps = React.PropsWithChildren<{
|
||||||
loading: boolean
|
positions: PositionDetails[]
|
||||||
positions: Position[]
|
|
||||||
showUnwrapped?: boolean
|
|
||||||
}>
|
}>
|
||||||
|
|
||||||
export default function PositionList({ loading, positions, showUnwrapped }: PositionListProps) {
|
export default function PositionList({ positions }: PositionListProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DesktopHeader>
|
<DesktopHeader>
|
||||||
@ -192,90 +51,10 @@ export default function PositionList({ loading, positions, showUnwrapped }: Posi
|
|||||||
<div>{t('Fees Earned')}</div>
|
<div>{t('Fees Earned')}</div>
|
||||||
</DesktopHeader>
|
</DesktopHeader>
|
||||||
<MobileHeader>Your positions</MobileHeader>
|
<MobileHeader>Your positions</MobileHeader>
|
||||||
{loading ? (
|
{positions.map((p, i) => {
|
||||||
<LoadingRows>
|
const key = `${i}-${p.nonce.toString()} ${p.token0} ${p.token1} ${p.tokensOwed0} ${p.tokensOwed1}`
|
||||||
<div />
|
return <PositionListItem key={key} positionDetails={p} positionIndex={i} />
|
||||||
<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>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
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 { ChainId, Percent, Token, WETH9 } from '@uniswap/sdk-core'
|
||||||
import { AbstractConnector } from '@web3-react/abstract-connector'
|
import { AbstractConnector } from '@web3-react/abstract-connector'
|
||||||
import JSBI from 'jsbi'
|
import JSBI from 'jsbi'
|
||||||
|
|
||||||
import { fortmatic, injected, portis, walletconnect, walletlink } from '../connectors'
|
import { fortmatic, injected, portis, walletconnect, walletlink } from '../connectors'
|
||||||
|
|
||||||
export const MULTICALL_ADDRESSES: { [chainId in ChainId]: string } = {
|
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 { Contract } from '@ethersproject/contracts'
|
||||||
import { abi as GOVERNANCE_ABI } from '@uniswap/governance/build/GovernorAlpha.json'
|
import { abi as GOVERNANCE_ABI } from '@uniswap/governance/build/GovernorAlpha.json'
|
||||||
import { abi as UNI_ABI } from '@uniswap/governance/build/Uni.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 { abi as MERKLE_DISTRIBUTOR_ABI } from '@uniswap/merkle-distributor/build/MerkleDistributor.json'
|
||||||
import { ChainId, WETH9 } from '@uniswap/sdk-core'
|
import { ChainId, WETH9 } from '@uniswap/sdk-core'
|
||||||
import { abi as IUniswapV2PairABI } from '@uniswap/v2-core/build/IUniswapV2Pair.json'
|
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 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 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 ARGENT_WALLET_DETECTOR_ABI from 'abis/argent-wallet-detector.json'
|
||||||
import ENS_PUBLIC_RESOLVER_ABI from 'abis/ens-public-resolver.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_ABI from 'abis/erc20.json'
|
||||||
import ERC20_BYTES32_ABI from 'abis/erc20_bytes32.json'
|
import ERC20_BYTES32_ABI from 'abis/erc20_bytes32.json'
|
||||||
import MIGRATOR_ABI from 'abis/migrator.json'
|
import MIGRATOR_ABI from 'abis/migrator.json'
|
||||||
|
import MULTICALL_ABI from 'abis/multicall2.json'
|
||||||
import { Unisocks } from 'abis/types/Unisocks'
|
import { Unisocks } from 'abis/types/Unisocks'
|
||||||
import UNISOCKS_ABI from 'abis/unisocks.json'
|
import UNISOCKS_ABI from 'abis/unisocks.json'
|
||||||
import WETH_ABI from 'abis/weth.json'
|
import WETH_ABI from 'abis/weth.json'
|
||||||
@ -28,11 +27,11 @@ import {
|
|||||||
UNI,
|
UNI,
|
||||||
MULTICALL_ADDRESSES,
|
MULTICALL_ADDRESSES,
|
||||||
} from 'constants/index'
|
} 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 { 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 { useMemo } from 'react'
|
||||||
import { TickLens, UniswapV3Factory } from 'types/v3'
|
import { TickLens, UniswapV3Factory, UniswapV3Pool } from 'types/v3'
|
||||||
import { NonfungiblePositionManager } from 'types/v3/NonfungiblePositionManager'
|
import { NonfungiblePositionManager } from 'types/v3/NonfungiblePositionManager'
|
||||||
import { getContract } from 'utils'
|
import { getContract } from 'utils'
|
||||||
import { useActiveWeb3React } from './index'
|
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 { RedirectDuplicateTokenIdsV2 } from './AddLiquidityV2/redirects'
|
||||||
import AddLiquidity from './AddLiquidity'
|
import AddLiquidity from './AddLiquidity'
|
||||||
import AddLiquidityV2 from './AddLiquidityV2'
|
import AddLiquidityV2 from './AddLiquidityV2'
|
||||||
|
import { PositionPage } from './Pool/PositionPage'
|
||||||
|
|
||||||
const AppWrapper = styled.div`
|
const AppWrapper = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -106,11 +107,11 @@ export default function App() {
|
|||||||
<Route exact strict path="/find" component={PoolFinder} />
|
<Route exact strict path="/find" component={PoolFinder} />
|
||||||
<Route exact strict path="/pool/v2" component={PoolV2} />
|
<Route exact strict path="/pool/v2" component={PoolV2} />
|
||||||
<Route exact strict path="/pool" component={Pool} />
|
<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" component={AddLiquidity} />
|
||||||
<Route exact path="/add/v2/" component={AddLiquidityV2} />
|
<Route exact path="/add/v2/" component={AddLiquidityV2} />
|
||||||
<Route exact path="/add/v2/:currencyIdA" component={AddLiquidityV2} />
|
<Route exact path="/add/v2/:currencyIdA" component={AddLiquidityV2} />
|
||||||
|
|
||||||
<Route exact path="/add/:currencyIdA" component={AddLiquidity} />
|
<Route exact path="/add/:currencyIdA" component={AddLiquidity} />
|
||||||
<Route exact strict path="/add/v2/:currencyIdA?/:currencyIdB?" component={RedirectDuplicateTokenIdsV2} />
|
<Route exact strict path="/add/v2/:currencyIdA?/:currencyIdB?" component={RedirectDuplicateTokenIdsV2} />
|
||||||
<Route
|
<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 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'
|
||||||
@ -6,14 +7,15 @@ import { SwapPoolTabs } from 'components/NavigationTabs'
|
|||||||
import PositionList from 'components/PositionList'
|
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/useV3PositionManager'
|
import { useV3Positions } from 'hooks/useV3Positions'
|
||||||
import React, { useContext, useMemo } from 'react'
|
|
||||||
import { BookOpen, ChevronDown, Download, Inbox, Info, PlusCircle } from 'react-feather'
|
import { BookOpen, ChevronDown, Download, Inbox, Info, PlusCircle } from 'react-feather'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled, { ThemeContext } from 'styled-components'
|
|
||||||
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 { HideSmall, MEDIA_WIDTHS, TYPE } from 'theme'
|
import { HideSmall, MEDIA_WIDTHS, TYPE } from 'theme'
|
||||||
|
import { PositionDetails } from 'types/position'
|
||||||
|
import { LoadingRows } from './styleds'
|
||||||
|
|
||||||
const PageWrapper = styled(AutoColumn)`
|
const PageWrapper = styled(AutoColumn)`
|
||||||
max-width: 870px;
|
max-width: 870px;
|
||||||
@ -91,24 +93,27 @@ const MainContentWrapper = styled.main`
|
|||||||
`
|
`
|
||||||
export default function Pool() {
|
export default function Pool() {
|
||||||
const { account } = useActiveWeb3React()
|
const { account } = useActiveWeb3React()
|
||||||
const { error, loading, positions } = useV3Positions(account)
|
|
||||||
const toggleWalletModal = useWalletModalToggle()
|
const toggleWalletModal = useWalletModalToggle()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const theme = useContext(ThemeContext)
|
const theme = useContext(ThemeContext)
|
||||||
|
|
||||||
|
const { error, positions } = useV3Positions(account)
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error('error fetching v3 positions', 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(() => {
|
const numInactivePositions = useMemo(() => {
|
||||||
return positions.reduce((acc: any, position: any) => {
|
return hasPositions && positions
|
||||||
const { tokenAmount0, tokenAmount1 } = position
|
? positions.reduce((acc: any, position: PositionDetails) => {
|
||||||
const limitCrossed = tokenAmount0.equalTo(BigInt(0)) || tokenAmount1.equalTo(BigInt(0))
|
const { tokensOwed0, tokensOwed1 } = position
|
||||||
return limitCrossed ? acc + 1 : acc
|
const limitCrossed = tokensOwed0.eq(0) || tokensOwed1.eq(0)
|
||||||
}, 0)
|
return limitCrossed ? acc + 1 : acc
|
||||||
}, [positions])
|
}, 0)
|
||||||
|
: 0
|
||||||
|
}, [positions, hasPositions])
|
||||||
|
|
||||||
const hasV2Liquidity = true
|
const hasV2Liquidity = true
|
||||||
const showMigrateHeaderLink = Boolean(hasV2Liquidity && hasPositions)
|
const showMigrateHeaderLink = Boolean(hasV2Liquidity && hasPositions)
|
||||||
@ -182,9 +187,9 @@ export default function Pool() {
|
|||||||
</TitleRow>
|
</TitleRow>
|
||||||
|
|
||||||
<MainContentWrapper>
|
<MainContentWrapper>
|
||||||
{hasPositions ? (
|
{hasPositions && positions ? (
|
||||||
<PositionList loading={loading} positions={positions} />
|
<PositionList positions={positions} />
|
||||||
) : (
|
) : positions && !hasPositions ? (
|
||||||
<NoLiquidity>
|
<NoLiquidity>
|
||||||
<TYPE.largeHeader color={theme.text3} textAlign="center">
|
<TYPE.largeHeader color={theme.text3} textAlign="center">
|
||||||
<Inbox />
|
<Inbox />
|
||||||
@ -208,6 +213,21 @@ export default function Pool() {
|
|||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
</NoLiquidity>
|
</NoLiquidity>
|
||||||
|
) : (
|
||||||
|
<LoadingRows>
|
||||||
|
<div />
|
||||||
|
<div />
|
||||||
|
<div />
|
||||||
|
<div />
|
||||||
|
<div />
|
||||||
|
<div />
|
||||||
|
<div />
|
||||||
|
<div />
|
||||||
|
<div />
|
||||||
|
<div />
|
||||||
|
<div />
|
||||||
|
<div />
|
||||||
|
</LoadingRows>
|
||||||
)}
|
)}
|
||||||
</MainContentWrapper>
|
</MainContentWrapper>
|
||||||
</AutoColumn>
|
</AutoColumn>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Text } from 'rebass'
|
import { Text } from 'rebass'
|
||||||
import styled from 'styled-components'
|
import styled, { keyframes } from 'styled-components'
|
||||||
|
|
||||||
export const Wrapper = styled.div`
|
export const Wrapper = styled.div`
|
||||||
position: relative;
|
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(() => {
|
const price = useMemo(() => {
|
||||||
// if no liquidity use typed value
|
// if no liquidity use typed value
|
||||||
if (noLiquidity) {
|
if (noLiquidity) {
|
||||||
const parsedAmount = tryParseAmount(startPriceTypedValue, tokenA)
|
const parsedAmount = tryParseAmount(startPriceTypedValue, tokenB)
|
||||||
if (parsedAmount && tokenA && tokenB) {
|
if (parsedAmount && tokenA && tokenB) {
|
||||||
const amountOne = tryParseAmount('1', tokenA)
|
const amountOne = tryParseAmount('1', tokenA)
|
||||||
return amountOne ? new Price(tokenA, tokenB, amountOne.raw, parsedAmount.raw) : undefined
|
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
|
readonly valid: boolean
|
||||||
// the result, or undefined if loading or errored/no data
|
// the result, or undefined if loading or errored/no data
|
||||||
readonly result: Result | undefined
|
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 PositionDetails {
|
||||||
|
nonce: BigNumber
|
||||||
export interface Position {
|
operator: string
|
||||||
feesEarned: Record<string, BigNumberish>
|
token0: string
|
||||||
feeLevel: FEE_BIPS
|
token1: string
|
||||||
tokenAmount0: TokenAmount
|
fee: number
|
||||||
tokenAmount1: TokenAmount
|
tickLower: number
|
||||||
tickLower: BigNumberish
|
tickUpper: number
|
||||||
tickUpper: BigNumberish
|
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