feat(weth): support WETH across the site and use sdk 3.0 (#947)

* first pass of sdk 3.0

* second pass using weth

* kill unused pool popup

* get it compiling again

* first pass of sdk 3.0

* switch to currencies

* get it compiling after the big move merge

* restore margin

* clean up add liquidity more

* fix a bunch of bugs

* todo trade on v1

* show eth in currency list

* allow selecting eth in the swap page

* fix unit tests for swap page

* test lint errors

* fix failing integration tests

* fix another couple of failing unit tests

* handle selecting currency b when no currency a

* improve the import pool page

* clean up add liquidity for invalid pairs

* bold

* first pass at swap arguments for v1, some unit tests

* fix some bugs in add liquidity, burn hook

* fix last of ts errors in remove liquidity

* support wrapping/unwrapping weth

* kill a bunch of code including the dummy pairs

* required pair prop in the position card

* tests for the v1 swap arguments

* do not say estimated on the wrap ui

* show ETH instead of WETH in the pool summaries

* small size socks

* fix lint error

* in burn, use currencies from the URL

* fix some integration tests

* both contain weth

* receive eth/weth link

* fix empty row

* show wrapped only if one currency is weth

* currency selects in the remove liquidity page
This commit is contained in:
Moody Salem 2020-07-20 06:48:42 -05:00 committed by GitHub
parent 6287b95b92
commit 19b1e9e399
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
76 changed files with 3731 additions and 3527 deletions

@ -32,13 +32,19 @@ describe('Add Liquidity', () => {
)
})
it('redirects /add/WETH-token to /add/ETH/token', () => {
it('redirects /add/WETH-token to /add/WETH-address/token', () => {
cy.visit('/add/0xc778417E063141139Fce010982780140Aa0cD5Ab-0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85')
cy.url().should('contain', '/add/ETH/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85')
cy.url().should(
'contain',
'/add/0xc778417E063141139Fce010982780140Aa0cD5Ab/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85'
)
})
it('redirects /add/token-WETH to /add/token/ETH', () => {
it('redirects /add/token-WETH to /add/token/WETH-address', () => {
cy.visit('/add/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85-0xc778417E063141139Fce010982780140Aa0cD5Ab')
cy.url().should('contain', '/add/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85/ETH')
cy.url().should(
'contain',
'/add/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85/0xc778417E063141139Fce010982780140Aa0cD5Ab'
)
})
})

@ -1,14 +1,34 @@
describe('Remove Liquidity', () => {
it('redirects', () => {
cy.visit('/remove/0xc778417E063141139Fce010982780140Aa0cD5Ab-0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85')
cy.url().should(
'contain',
'/remove/0xc778417E063141139Fce010982780140Aa0cD5Ab/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85'
)
})
it('eth remove', () => {
cy.visit('/remove/ETH/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85')
cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'ETH')
cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'MKR')
})
it('eth remove swap order', () => {
cy.visit('/remove/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85/ETH')
cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'MKR')
cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'ETH')
})
it('loads the two correct tokens', () => {
cy.visit('/remove/0xc778417E063141139Fce010982780140Aa0cD5Ab-0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85')
cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'ETH')
cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'WETH')
cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'MKR')
})
it('does not crash if ETH is duplicated', () => {
cy.visit('/remove/0xc778417E063141139Fce010982780140Aa0cD5Ab-0xc778417E063141139Fce010982780140Aa0cD5Ab')
cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'ETH')
cy.get('#remove-liquidity-tokenb-symbol').should('not.contain.text', 'ETH')
cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'WETH')
cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'WETH')
})
it('token not in storage is loaded', () => {

@ -4,15 +4,15 @@
"homepage": ".",
"private": true,
"devDependencies": {
"@ethersproject/address": "^5.0.0-beta.134",
"@ethersproject/bignumber": "^5.0.0-beta.138",
"@ethersproject/constants": "^5.0.0-beta.133",
"@ethersproject/contracts": "^5.0.0-beta.151",
"@ethersproject/experimental": "^5.0.0-beta.141",
"@ethersproject/providers": "5.0.0-beta.162",
"@ethersproject/strings": "^5.0.0-beta.136",
"@ethersproject/units": "^5.0.0-beta.132",
"@ethersproject/wallet": "^5.0.0-beta.141",
"@ethersproject/address": "^5.0.1",
"@ethersproject/bignumber": "^5.0.3",
"@ethersproject/constants": "^5.0.1",
"@ethersproject/contracts": "^5.0.1",
"@ethersproject/experimental": "^5.0.0",
"@ethersproject/providers": "^5.0.4",
"@ethersproject/strings": "^5.0.1",
"@ethersproject/units": "^5.0.1",
"@ethersproject/wallet": "^5.0.1",
"@popperjs/core": "^2.4.4",
"@reach/dialog": "^0.10.3",
"@reach/portal": "^0.10.3",
@ -27,11 +27,11 @@
"@types/react-router-dom": "^5.0.0",
"@types/react-window": "^1.8.2",
"@types/rebass": "^4.0.5",
"@types/styled-components": "^4.2.0",
"@types/styled-components": "^5.1.0",
"@types/testing-library__cypress": "^5.0.5",
"@typescript-eslint/eslint-plugin": "^2.31.0",
"@typescript-eslint/parser": "^2.31.0",
"@uniswap/sdk": "^2.0.5",
"@uniswap/sdk": "3.0.1",
"@uniswap/v2-core": "1.0.0",
"@uniswap/v2-periphery": "^1.1.0-beta.0",
"@web3-react/core": "^6.0.9",

@ -1,12 +1,11 @@
import { Pair, Token } from '@uniswap/sdk'
import { Currency, Pair } from '@uniswap/sdk'
import React, { useState, useContext, useCallback } from 'react'
import styled, { ThemeContext } from 'styled-components'
import { darken } from 'polished'
import { Field } from '../../state/swap/actions'
import { useTokenBalanceTreatingWETHasETH } from '../../state/wallet/hooks'
import TokenSearchModal from '../SearchModal/TokenSearchModal'
import TokenLogo from '../TokenLogo'
import DoubleLogo from '../DoubleLogo'
import { useCurrencyBalance } from '../../state/wallet/hooks'
import CurrencySearchModal from '../SearchModal/CurrencySearchModal'
import CurrencyLogo from '../CurrencyLogo'
import DoubleCurrencyLogo from '../DoubleLogo'
import { RowBetween } from '../Row'
import { TYPE, CursorPointer } from '../../theme'
import { Input as NumericalInput } from '../NumericalInput'
@ -117,40 +116,36 @@ const StyledBalanceMax = styled.button`
interface CurrencyInputPanelProps {
value: string
field: string
onUserInput: (field: string, val: string) => void
onUserInput: (value: string) => void
onMax?: () => void
showMaxButton: boolean
label?: string
onTokenSelection?: (tokenAddress: string) => void
token?: Token | null
disableTokenSelect?: boolean
onCurrencySelect?: (currency: Currency) => void
currency?: Currency | null
disableCurrencySelect?: boolean
hideBalance?: boolean
isExchange?: boolean
pair?: Pair | null
hideInput?: boolean
showSendWithSwap?: boolean
otherSelectedTokenAddress?: string | null
otherCurrency?: Currency | null
id: string
showCommonBases?: boolean
}
export default function CurrencyInputPanel({
value,
field,
onUserInput,
onMax,
showMaxButton,
label = 'Input',
onTokenSelection = null,
token = null,
disableTokenSelect = false,
onCurrencySelect = null,
currency = null,
disableCurrencySelect = false,
hideBalance = false,
isExchange = false,
pair = null, // used for double token logo
hideInput = false,
showSendWithSwap = false,
otherSelectedTokenAddress = null,
otherCurrency = null,
id,
showCommonBases
}: CurrencyInputPanelProps) {
@ -158,7 +153,7 @@ export default function CurrencyInputPanel({
const [modalOpen, setModalOpen] = useState(false)
const { account } = useActiveWeb3React()
const userTokenBalance = useTokenBalanceTreatingWETHasETH(account, token)
const selectedCurrencyBalance = useCurrencyBalance(account, currency)
const theme = useContext(ThemeContext)
const handleDismissSearch = useCallback(() => {
@ -183,8 +178,8 @@ export default function CurrencyInputPanel({
fontSize={14}
style={{ display: 'inline' }}
>
{!hideBalance && !!token && userTokenBalance
? 'Balance: ' + userTokenBalance?.toSignificant(6)
{!hideBalance && !!currency && selectedCurrencyBalance
? 'Balance: ' + selectedCurrencyBalance?.toSignificant(6)
: ' -'}
</TYPE.body>
</CursorPointer>
@ -192,63 +187,62 @@ export default function CurrencyInputPanel({
</RowBetween>
</LabelRow>
)}
<InputRow style={hideInput ? { padding: '0', borderRadius: '8px' } : {}} selected={disableTokenSelect}>
<InputRow style={hideInput ? { padding: '0', borderRadius: '8px' } : {}} selected={disableCurrencySelect}>
{!hideInput && (
<>
<NumericalInput
className="token-amount-input"
value={value}
onUserInput={val => {
onUserInput(field, val)
onUserInput(val)
}}
/>
{account && !!token?.address && showMaxButton && label !== 'To' && (
{account && currency && showMaxButton && label !== 'To' && (
<StyledBalanceMax onClick={onMax}>MAX</StyledBalanceMax>
)}
</>
)}
<CurrencySelect
selected={!!token}
selected={!!currency}
className="open-currency-select-button"
onClick={() => {
if (!disableTokenSelect) {
if (!disableCurrencySelect) {
setModalOpen(true)
}
}}
>
<Aligner>
{isExchange ? (
<DoubleLogo a0={pair?.token0.address} a1={pair?.token1.address} size={24} margin={true} />
) : token?.address ? (
<TokenLogo address={token?.address} size={'24px'} />
{pair ? (
<DoubleCurrencyLogo currency0={pair.token0} currency1={pair.token1} size={24} margin={true} />
) : currency ? (
<CurrencyLogo currency={currency} size={'24px'} />
) : null}
{isExchange ? (
{pair ? (
<StyledTokenName className="pair-name-container">
{pair?.token0.symbol}:{pair?.token1.symbol}
</StyledTokenName>
) : (
<StyledTokenName className="token-symbol-container" active={Boolean(token && token.symbol)}>
{(token && token.symbol && token.symbol.length > 20
? token.symbol.slice(0, 4) +
<StyledTokenName className="token-symbol-container" active={Boolean(currency && currency.symbol)}>
{(currency && currency.symbol && currency.symbol.length > 20
? currency.symbol.slice(0, 4) +
'...' +
token.symbol.slice(token.symbol.length - 5, token.symbol.length)
: token?.symbol) || t('selectToken')}
currency.symbol.slice(currency.symbol.length - 5, currency.symbol.length)
: currency?.symbol) || t('selectToken')}
</StyledTokenName>
)}
{!disableTokenSelect && <StyledDropDown selected={!!token?.address} />}
{!disableCurrencySelect && <StyledDropDown selected={!!currency} />}
</Aligner>
</CurrencySelect>
</InputRow>
</Container>
{!disableTokenSelect && (
<TokenSearchModal
{!disableCurrencySelect && (
<CurrencySearchModal
isOpen={modalOpen}
onDismiss={handleDismissSearch}
onTokenSelect={onTokenSelection}
onCurrencySelect={onCurrencySelect}
showSendWithSwap={showSendWithSwap}
hiddenToken={token?.address}
otherSelectedTokenAddress={otherSelectedTokenAddress}
otherSelectedText={field === Field.INPUT ? 'Selected as output' : 'Selected as input'}
hiddenCurrency={currency}
otherSelectedCurrency={otherCurrency}
showCommonBases={showCommonBases}
/>
)}

@ -1,8 +1,6 @@
import React, { useState } from 'react'
import styled from 'styled-components'
import { isAddress } from '../../utils'
import { useActiveWeb3React } from '../../hooks'
import { WETH } from '@uniswap/sdk'
import { Currency, Token } from '@uniswap/sdk'
import EthereumLogo from '../../assets/images/ethereum-logo.png'
@ -35,25 +33,21 @@ const StyledEthereumLogo = styled.img<{ size: string }>`
border-radius: 24px;
`
export default function TokenLogo({
address,
export default function CurrencyLogo({
currency,
size = '24px',
...rest
}: {
address?: string
currency?: Currency
size?: string
style?: React.CSSProperties
}) {
const [, refresh] = useState<number>(0)
const { chainId } = useActiveWeb3React()
if (currency instanceof Token) {
let path = ''
const validated = isAddress(address)
// hard code to show ETH instead of WETH in UI
if (validated === WETH[chainId].address) {
return <StyledEthereumLogo src={EthereumLogo} size={size} {...rest} />
} else if (!NO_LOGO_ADDRESSES[address] && validated) {
path = getTokenLogoURL(validated)
if (!NO_LOGO_ADDRESSES[currency.address]) {
path = getTokenLogoURL(currency.address)
} else {
return (
<Emoji {...rest} size={size}>
@ -67,13 +61,18 @@ export default function TokenLogo({
return (
<Image
{...rest}
// alt={address}
alt={`${currency.name} Logo`}
src={path}
size={size}
onError={() => {
NO_LOGO_ADDRESSES[address] = true
if (currency instanceof Token) {
NO_LOGO_ADDRESSES[currency.address] = true
}
refresh(i => i + 1)
}}
/>
)
} else {
return <StyledEthereumLogo src={EthereumLogo} size={size} {...rest} />
}
}

@ -1,34 +1,40 @@
import { Currency } from '@uniswap/sdk'
import React from 'react'
import styled from 'styled-components'
import TokenLogo from '../TokenLogo'
import CurrencyLogo from '../CurrencyLogo'
const TokenWrapper = styled.div<{ margin: boolean; sizeraw: number }>`
const Wrapper = styled.div<{ margin: boolean; sizeraw: number }>`
position: relative;
display: flex;
flex-direction: row;
margin-right: ${({ sizeraw, margin }) => margin && (sizeraw / 3 + 8).toString() + 'px'};
`
interface DoubleTokenLogoProps {
interface DoubleCurrencyLogoProps {
margin?: boolean
size?: number
a0?: string
a1?: string
currency0?: Currency
currency1?: Currency
}
const HigherLogo = styled(TokenLogo)`
const HigherLogo = styled(CurrencyLogo)`
z-index: 2;
`
const CoveredLogo = styled(TokenLogo)<{ sizeraw: number }>`
const CoveredLogo = styled(CurrencyLogo)<{ sizeraw: number }>`
position: absolute;
left: ${({ sizeraw }) => (sizeraw / 2).toString() + 'px'};
`
export default function DoubleTokenLogo({ a0, a1, size = 16, margin = false }: DoubleTokenLogoProps) {
export default function DoubleCurrencyLogo({
currency0,
currency1,
size = 16,
margin = false
}: DoubleCurrencyLogoProps) {
return (
<TokenWrapper sizeraw={size} margin={margin}>
{a0 && <HigherLogo address={a0} size={size.toString() + 'px'} />}
{a1 && <CoveredLogo address={a1} size={size.toString() + 'px'} sizeraw={size} />}
</TokenWrapper>
<Wrapper sizeraw={size} margin={margin}>
{currency0 && <HigherLogo currency={currency0} size={size.toString() + 'px'} />}
{currency1 && <CoveredLogo currency={currency1} size={size.toString() + 'px'} sizeraw={size} />}
</Wrapper>
)
}

@ -4,6 +4,7 @@ import { Link, useLocation } from 'react-router-dom'
import styled from 'styled-components'
import useParsedQueryString from '../../hooks/useParsedQueryString'
import useToggledVersion, { Version } from '../../hooks/useToggledVersion'
import { MouseoverTooltip } from '../Tooltip'
const VersionLabel = styled.span<{ enabled: boolean }>`
padding: 0.35rem 0.6rem;
@ -61,10 +62,15 @@ export default function VersionSwitch() {
[versionSwitchAvailable]
)
return (
const toggle = (
<VersionToggle enabled={versionSwitchAvailable} to={toggleDest} onClick={handleClick}>
<VersionLabel enabled={version === Version.v2 || !versionSwitchAvailable}>V2</VersionLabel>
<VersionLabel enabled={version === Version.v1 && versionSwitchAvailable}>V1</VersionLabel>
</VersionToggle>
)
return versionSwitchAvailable ? (
toggle
) : (
<MouseoverTooltip text="This page is only compatible with Uniswap V2.">{toggle}</MouseoverTooltip>
)
}

@ -1,4 +1,4 @@
import { ChainId, WETH } from '@uniswap/sdk'
import { ChainId } from '@uniswap/sdk'
import React from 'react'
import { isMobile } from 'react-device-detect'
import { Text } from 'rebass'
@ -11,7 +11,7 @@ import Wordmark from '../../assets/svg/wordmark.svg'
import WordmarkDark from '../../assets/svg/wordmark_white.svg'
import { useActiveWeb3React } from '../../hooks'
import { useDarkModeManager } from '../../state/user/hooks'
import { useTokenBalanceTreatingWETHasETH } from '../../state/wallet/hooks'
import { useETHBalances } from '../../state/wallet/hooks'
import { YellowCard } from '../Card'
import Settings from '../Settings'
@ -137,7 +137,7 @@ const NETWORK_LABELS: { [chainId in ChainId]: string | null } = {
export default function Header() {
const { account, chainId } = useActiveWeb3React()
const userEthBalance = useTokenBalanceTreatingWETHasETH(account, WETH[chainId])
const userEthBalance = useETHBalances([account])[account]
const [isDark] = useDarkModeManager()
return (

@ -66,20 +66,6 @@ export function SwapPoolTabs({ active }: { active: 'swap' | 'pool' }) {
)
}
export function CreatePoolTabs() {
return (
<Tabs>
<RowBetween style={{ padding: '1rem' }}>
<HistoryLink to="/pool">
<StyledArrowLeft />
</HistoryLink>
<ActiveText>Create Pool</ActiveText>
<QuestionHelper text={'Use this interface to create a new pool.'} />
</RowBetween>
</Tabs>
)
}
export function FindPoolTabs() {
return (
<Tabs>

@ -1,17 +1,12 @@
import { ChainId, Pair, Token } from '@uniswap/sdk'
import React, { useContext, useMemo } from 'react'
import React, { useContext } from 'react'
import styled, { ThemeContext } from 'styled-components'
import { useMediaLayout } from 'use-media'
import { X } from 'react-feather'
import { PopupContent } from '../../state/application/actions'
import { useActivePopups, useRemovePopup } from '../../state/application/hooks'
import { ExternalLink } from '../../theme'
import { AutoColumn } from '../Column'
import DoubleTokenLogo from '../DoubleLogo'
import Row from '../Row'
import TxnPopup from '../TxnPopup'
import { Text } from 'rebass'
const StyledClose = styled(X)`
position: absolute;
@ -72,52 +67,12 @@ const Popup = styled.div`
`}
`
function PoolPopup({
token0,
token1
}: {
token0: { address?: string; symbol?: string }
token1: { address?: string; symbol?: string }
}) {
const pairAddress: string | null = useMemo(() => {
if (!token0 || !token1) return null
// just mock it out
return Pair.getAddress(
new Token(ChainId.MAINNET, token0.address, 18),
new Token(ChainId.MAINNET, token1.address, 18)
)
}, [token0, token1])
return (
<AutoColumn gap={'10px'}>
<Text fontSize={20} fontWeight={500}>
Pool Imported
</Text>
<Row>
<DoubleTokenLogo a0={token0?.address ?? ''} a1={token1?.address ?? ''} margin={true} />
<Text fontSize={16} fontWeight={500}>
UNI {token0?.symbol} / {token1?.symbol}
</Text>
</Row>
{pairAddress ? (
<ExternalLink href={`https://uniswap.info/pair/${pairAddress}`}>View on Uniswap Info.</ExternalLink>
) : null}
</AutoColumn>
)
}
function PopupItem({ content, popKey }: { content: PopupContent; popKey: string }) {
if ('txn' in content) {
const {
txn: { hash, success, summary }
} = content
return <TxnPopup popKey={popKey} hash={hash} success={success} summary={summary} />
} else if ('poolAdded' in content) {
const {
poolAdded: { token0, token1 }
} = content
return <PoolPopup token0={token0} token1={token1} />
}
}

@ -7,7 +7,7 @@ import { AutoColumn } from '../Column'
import { ButtonSecondary } from '../Button'
import { RowBetween, RowFixed } from '../Row'
import { FixedHeightRow, HoverCard } from './index'
import DoubleTokenLogo from '../DoubleLogo'
import DoubleCurrencyLogo from '../DoubleLogo'
import { useActiveWeb3React } from '../../hooks'
import { ThemeContext } from 'styled-components'
@ -26,7 +26,7 @@ function V1PositionCard({ token, V1LiquidityBalance }: PositionCardProps) {
<AutoColumn gap="12px">
<FixedHeightRow>
<RowFixed>
<DoubleTokenLogo a0={token.address} margin={true} size={20} />
<DoubleCurrencyLogo currency0={token} margin={true} size={20} />
<Text fontWeight={500} fontSize={20} style={{ marginLeft: '' }}>
{`${token.equals(WETH[chainId]) ? 'WETH' : token.symbol}/ETH`}
</Text>

@ -1,23 +1,24 @@
import React, { useState } from 'react'
import styled from 'styled-components'
import { JSBI, Pair, Percent } from '@uniswap/sdk'
import { darken } from 'polished'
import React, { useState } from 'react'
import { ChevronDown, ChevronUp } from 'react-feather'
import { Link } from 'react-router-dom'
import { Percent, Pair, JSBI } from '@uniswap/sdk'
import { Text } from 'rebass'
import styled from 'styled-components'
import { useTotalSupply } from '../../data/TotalSupply'
import { useActiveWeb3React } from '../../hooks'
import { useTotalSupply } from '../../data/TotalSupply'
import { currencyId } from '../../pages/AddLiquidity/currencyId'
import { useTokenBalance } from '../../state/wallet/hooks'
import { ExternalLink } from '../../theme'
import { currencyId } from '../../utils/currencyId'
import { unwrappedToken } from '../../utils/wrappedCurrency'
import { ButtonSecondary } from '../Button'
import Card, { GreyCard } from '../Card'
import TokenLogo from '../TokenLogo'
import DoubleLogo from '../DoubleLogo'
import { Text } from 'rebass'
import { ExternalLink } from '../../theme'
import { AutoColumn } from '../Column'
import { ChevronDown, ChevronUp } from 'react-feather'
import { ButtonSecondary } from '../Button'
import { RowBetween, RowFixed, AutoRow } from '../Row'
import CurrencyLogo from '../CurrencyLogo'
import DoubleCurrencyLogo from '../DoubleLogo'
import { AutoRow, RowBetween, RowFixed } from '../Row'
import { Dots } from '../swap/styleds'
export const FixedHeightRow = styled(RowBetween)`
@ -32,20 +33,21 @@ export const HoverCard = styled(Card)`
`
interface PositionCardProps {
pair: Pair | undefined | null
pair: Pair
showUnwrapped?: boolean
border?: string
}
export function MinimalPositionCard({ pair, border }: PositionCardProps) {
export function MinimalPositionCard({ pair, showUnwrapped = false, border }: PositionCardProps) {
const { account } = useActiveWeb3React()
const token0 = pair?.token0
const token1 = pair?.token1
const currency0 = showUnwrapped ? pair.token0 : unwrappedToken(pair.token0)
const currency1 = showUnwrapped ? pair.token1 : unwrappedToken(pair.token1)
const [showMore, setShowMore] = useState(false)
const userPoolBalance = useTokenBalance(account, pair?.liquidityToken)
const totalPoolTokens = useTotalSupply(pair?.liquidityToken)
const userPoolBalance = useTokenBalance(account, pair.liquidityToken)
const totalPoolTokens = useTotalSupply(pair.liquidityToken)
const [token0Deposited, token1Deposited] =
!!pair &&
@ -54,8 +56,8 @@ export function MinimalPositionCard({ pair, border }: PositionCardProps) {
// this condition is a short-circuit in the case where useTokenBalance updates sooner than useTotalSupply
JSBI.greaterThanOrEqual(totalPoolTokens.raw, userPoolBalance.raw)
? [
pair.getLiquidityValue(token0, totalPoolTokens, userPoolBalance, false),
pair.getLiquidityValue(token1, totalPoolTokens, userPoolBalance, false)
pair.getLiquidityValue(pair.token0, totalPoolTokens, userPoolBalance, false),
pair.getLiquidityValue(pair.token1, totalPoolTokens, userPoolBalance, false)
]
: [undefined, undefined]
@ -73,9 +75,9 @@ export function MinimalPositionCard({ pair, border }: PositionCardProps) {
</FixedHeightRow>
<FixedHeightRow onClick={() => setShowMore(!showMore)}>
<RowFixed>
<DoubleLogo a0={token0?.address || ''} a1={token1?.address || ''} margin={true} size={20} />
<DoubleCurrencyLogo currency0={currency0} currency1={currency1} margin={true} size={20} />
<Text fontWeight={500} fontSize={20}>
{token0?.symbol}/{token1?.symbol}
{currency0.symbol}/{currency1.symbol}
</Text>
</RowFixed>
<RowFixed>
@ -87,7 +89,7 @@ export function MinimalPositionCard({ pair, border }: PositionCardProps) {
<AutoColumn gap="4px">
<FixedHeightRow>
<Text color="#888D9B" fontSize={16} fontWeight={500}>
{token0?.symbol}:
{currency0.symbol}:
</Text>
{token0Deposited ? (
<RowFixed>
@ -101,7 +103,7 @@ export function MinimalPositionCard({ pair, border }: PositionCardProps) {
</FixedHeightRow>
<FixedHeightRow>
<Text color="#888D9B" fontSize={16} fontWeight={500}>
{token1?.symbol}:
{currency1.symbol}:
</Text>
{token1Deposited ? (
<RowFixed>
@ -124,13 +126,13 @@ export function MinimalPositionCard({ pair, border }: PositionCardProps) {
export default function FullPositionCard({ pair, border }: PositionCardProps) {
const { account } = useActiveWeb3React()
const token0 = pair?.token0
const token1 = pair?.token1
const currency0 = unwrappedToken(pair.token0)
const currency1 = unwrappedToken(pair.token1)
const [showMore, setShowMore] = useState(false)
const userPoolBalance = useTokenBalance(account, pair?.liquidityToken)
const totalPoolTokens = useTotalSupply(pair?.liquidityToken)
const userPoolBalance = useTokenBalance(account, pair.liquidityToken)
const totalPoolTokens = useTotalSupply(pair.liquidityToken)
const poolTokenPercentage =
!!userPoolBalance && !!totalPoolTokens && JSBI.greaterThanOrEqual(totalPoolTokens.raw, userPoolBalance.raw)
@ -144,8 +146,8 @@ export default function FullPositionCard({ pair, border }: PositionCardProps) {
// this condition is a short-circuit in the case where useTokenBalance updates sooner than useTotalSupply
JSBI.greaterThanOrEqual(totalPoolTokens.raw, userPoolBalance.raw)
? [
pair.getLiquidityValue(token0, totalPoolTokens, userPoolBalance, false),
pair.getLiquidityValue(token1, totalPoolTokens, userPoolBalance, false)
pair.getLiquidityValue(pair.token0, totalPoolTokens, userPoolBalance, false),
pair.getLiquidityValue(pair.token1, totalPoolTokens, userPoolBalance, false)
]
: [undefined, undefined]
@ -154,9 +156,9 @@ export default function FullPositionCard({ pair, border }: PositionCardProps) {
<AutoColumn gap="12px">
<FixedHeightRow onClick={() => setShowMore(!showMore)} style={{ cursor: 'pointer' }}>
<RowFixed>
<DoubleLogo a0={token0?.address || ''} a1={token1?.address || ''} margin={true} size={20} />
<DoubleCurrencyLogo currency0={currency0} currency1={currency1} margin={true} size={20} />
<Text fontWeight={500} fontSize={20}>
{!token0 || !token1 ? <Dots>Loading</Dots> : `${token0.symbol}/${token1.symbol}`}
{!currency0 || !currency1 ? <Dots>Loading</Dots> : `${currency0.symbol}/${currency1.symbol}`}
</Text>
</RowFixed>
<RowFixed>
@ -172,7 +174,7 @@ export default function FullPositionCard({ pair, border }: PositionCardProps) {
<FixedHeightRow>
<RowFixed>
<Text fontSize={16} fontWeight={500}>
Pooled {token0?.symbol}:
Pooled {currency0.symbol}:
</Text>
</RowFixed>
{token0Deposited ? (
@ -180,7 +182,7 @@ export default function FullPositionCard({ pair, border }: PositionCardProps) {
<Text fontSize={16} fontWeight={500} marginLeft={'6px'}>
{token0Deposited?.toSignificant(6)}
</Text>
<TokenLogo size="20px" style={{ marginLeft: '8px' }} address={token0?.address} />
<CurrencyLogo size="20px" style={{ marginLeft: '8px' }} currency={currency0} />
</RowFixed>
) : (
'-'
@ -190,7 +192,7 @@ export default function FullPositionCard({ pair, border }: PositionCardProps) {
<FixedHeightRow>
<RowFixed>
<Text fontSize={16} fontWeight={500}>
Pooled {token1?.symbol}:
Pooled {currency1.symbol}:
</Text>
</RowFixed>
{token1Deposited ? (
@ -198,7 +200,7 @@ export default function FullPositionCard({ pair, border }: PositionCardProps) {
<Text fontSize={16} fontWeight={500} marginLeft={'6px'}>
{token1Deposited?.toSignificant(6)}
</Text>
<TokenLogo size="20px" style={{ marginLeft: '8px' }} address={token1?.address} />
<CurrencyLogo size="20px" style={{ marginLeft: '8px' }} currency={currency1} />
</RowFixed>
) : (
'-'
@ -222,15 +224,15 @@ export default function FullPositionCard({ pair, border }: PositionCardProps) {
</FixedHeightRow>
<AutoRow justify="center" marginTop={'10px'}>
<ExternalLink href={`https://uniswap.info/pair/${pair?.liquidityToken.address}`}>
<ExternalLink href={`https://uniswap.info/pair/${pair.liquidityToken.address}`}>
View pool information
</ExternalLink>
</AutoRow>
<RowBetween marginTop="10px">
<ButtonSecondary as={Link} to={`/add/${currencyId(token0)}/${currencyId(token1)}`} width="48%">
<ButtonSecondary as={Link} to={`/add/${currencyId(currency0)}/${currencyId(currency1)}`} width="48%">
Add
</ButtonSecondary>
<ButtonSecondary as={Link} width="48%" to={`/remove/${token0?.address}-${token1?.address}`}>
<ButtonSecondary as={Link} width="48%" to={`/remove/${currencyId(currency0)}/${currencyId(currency1)}`}>
Remove
</ButtonSecondary>
</RowBetween>

@ -5,14 +5,13 @@ const Row = styled(Box)<{ align?: string; padding?: string; border?: string; bor
width: 100%;
display: flex;
padding: 0;
align-items: center;
align-items: ${({ align }) => align && align};
align-items: ${({ align }) => (align ? align : 'center')};
padding: ${({ padding }) => padding};
border: ${({ border }) => border};
border-radius: ${({ borderRadius }) => borderRadius};
`
export const RowBetween = styled(Row)<{ align?: string; padding?: string; border?: string; borderRadius?: string }>`
export const RowBetween = styled(Row)`
justify-content: space-between;
`

@ -1,13 +1,13 @@
import React from 'react'
import { Text } from 'rebass'
import { ChainId, Token } from '@uniswap/sdk'
import { ChainId, Currency, currencyEquals, ETHER, Token } from '@uniswap/sdk'
import styled from 'styled-components'
import { SUGGESTED_BASES } from '../../constants'
import { AutoColumn } from '../Column'
import QuestionHelper from '../QuestionHelper'
import { AutoRow } from '../Row'
import TokenLogo from '../TokenLogo'
import CurrencyLogo from '../CurrencyLogo'
const BaseWrapper = styled.div<{ disable?: boolean }>`
border: 1px solid ${({ theme, disable }) => (disable ? 'transparent' : theme.bg3)};
@ -28,11 +28,11 @@ const BaseWrapper = styled.div<{ disable?: boolean }>`
export default function CommonBases({
chainId,
onSelect,
selectedTokenAddress
selectedCurrency
}: {
chainId: ChainId
selectedTokenAddress: string
onSelect: (tokenAddress: string) => void
selectedCurrency?: Currency
onSelect: (currency: Currency) => void
}) {
return (
<AutoColumn gap="md">
@ -43,14 +43,20 @@ export default function CommonBases({
<QuestionHelper text="These tokens are commonly paired with other tokens." />
</AutoRow>
<AutoRow gap="4px">
{(SUGGESTED_BASES[chainId as ChainId] ?? []).map((token: Token) => {
return (
<BaseWrapper
onClick={() => selectedTokenAddress !== token.address && onSelect(token.address)}
disable={selectedTokenAddress === token.address}
key={token.address}
onClick={() => !currencyEquals(selectedCurrency, ETHER) && onSelect(ETHER)}
disable={selectedCurrency === ETHER}
>
<TokenLogo address={token.address} style={{ marginRight: 8 }} />
<CurrencyLogo currency={ETHER} style={{ marginRight: 8 }} />
<Text fontWeight={500} fontSize={16}>
ETH
</Text>
</BaseWrapper>
{(SUGGESTED_BASES[chainId as ChainId] ?? []).map((token: Token) => {
const selected = currencyEquals(selectedCurrency, token)
return (
<BaseWrapper onClick={() => !selected && onSelect(token)} disable={selected} key={token.address}>
<CurrencyLogo currency={token} style={{ marginRight: 8 }} />
<Text fontWeight={500} fontSize={16}>
{token.symbol}
</Text>

@ -1,72 +1,72 @@
import { JSBI, Token, TokenAmount } from '@uniswap/sdk'
import { Currency, CurrencyAmount, currencyEquals, ETHER, JSBI, Token } from '@uniswap/sdk'
import React, { CSSProperties, memo, useContext, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { FixedSizeList } from 'react-window'
import { Text } from 'rebass'
import { ThemeContext } from 'styled-components'
import { useActiveWeb3React } from '../../hooks'
import { useAllTokens } from '../../hooks/Tokens'
import { useAddUserToken, useRemoveUserAddedToken } from '../../state/user/hooks'
import { useETHBalances } from '../../state/wallet/hooks'
import { LinkStyledButton, TYPE } from '../../theme'
import { ButtonSecondary } from '../Button'
import Column, { AutoColumn } from '../Column'
import { RowFixed } from '../Row'
import TokenLogo from '../TokenLogo'
import { FadedSpan, GreySpan, MenuItem, ModalInfo } from './styleds'
import CurrencyLogo from '../CurrencyLogo'
import { FadedSpan, MenuItem } from './styleds'
import Loader from '../Loader'
import { isDefaultToken, isCustomAddedToken } from '../../utils'
export default function TokenList({
tokens,
allTokenBalances,
selectedToken,
onTokenSelect,
otherToken,
showSendWithSwap,
otherSelectedText
function currencyKey(currency: Currency): string {
return currency instanceof Token ? currency.address : currency === ETHER ? 'ETHER' : ''
}
export default function CurrencyList({
currencies,
allBalances,
selectedCurrency,
onCurrencySelect,
otherCurrency,
showSendWithSwap
}: {
tokens: Token[]
selectedToken: string
allTokenBalances: { [tokenAddress: string]: TokenAmount }
onTokenSelect: (tokenAddress: string) => void
otherToken: string
currencies: Currency[]
selectedCurrency: Currency
allBalances: { [tokenAddress: string]: CurrencyAmount }
onCurrencySelect: (currency: Currency) => void
otherCurrency: Currency
showSendWithSwap?: boolean
otherSelectedText: string
}) {
const { t } = useTranslation()
const { account, chainId } = useActiveWeb3React()
const theme = useContext(ThemeContext)
const allTokens = useAllTokens()
const addToken = useAddUserToken()
const removeToken = useRemoveUserAddedToken()
const ETHBalance = useETHBalances([account])[account]
const TokenRow = useMemo(() => {
return memo(function TokenRow({ index, style }: { index: number; style: CSSProperties }) {
const token = tokens[index]
const { address, symbol } = token
const isDefault = isDefaultToken(token)
const customAdded = isCustomAddedToken(allTokens, token)
const balance = allTokenBalances[address]
const CurrencyRow = useMemo(() => {
return memo(function CurrencyRow({ index, style }: { index: number; style: CSSProperties }) {
const currency = index === 0 ? Currency.ETHER : currencies[index - 1]
const key = currencyKey(currency)
const isDefault = isDefaultToken(currency)
const customAdded = isCustomAddedToken(allTokens, currency)
const balance = currency === ETHER ? ETHBalance : allBalances[key]
const zeroBalance = balance && JSBI.equal(JSBI.BigInt(0), balance.raw)
const isSelected = Boolean(selectedCurrency && currencyEquals(currency, selectedCurrency))
const otherSelected = Boolean(otherCurrency && currencyEquals(otherCurrency, currency))
return (
<MenuItem
style={style}
key={address}
className={`token-item-${address}`}
onClick={() => (selectedToken && selectedToken === address ? null : onTokenSelect(address))}
disabled={selectedToken && selectedToken === address}
selected={otherToken === address}
className={`token-item-${key}`}
onClick={() => (isSelected ? null : onCurrencySelect(currency))}
disabled={isSelected}
selected={otherSelected}
>
<RowFixed>
<TokenLogo address={address} size={'24px'} style={{ marginRight: '14px' }} />
<CurrencyLogo currency={currency} size={'24px'} style={{ marginRight: '14px' }} />
<Column>
<Text fontWeight={500}>
{symbol}
{otherToken === address && <GreySpan> ({otherSelectedText})</GreySpan>}
</Text>
<Text fontWeight={500}>{currency.symbol}</Text>
<FadedSpan>
{customAdded ? (
<TYPE.main fontWeight={500}>
@ -74,7 +74,7 @@ export default function TokenList({
<LinkStyledButton
onClick={event => {
event.stopPropagation()
removeToken(chainId, address)
if (currency instanceof Token) removeToken(chainId, currency.address)
}}
>
(Remove)
@ -87,7 +87,7 @@ export default function TokenList({
<LinkStyledButton
onClick={event => {
event.stopPropagation()
addToken(token)
if (currency instanceof Token) addToken(currency)
}}
>
(Add)
@ -122,35 +122,31 @@ export default function TokenList({
)
})
}, [
ETHBalance,
account,
addToken,
allTokenBalances,
allBalances,
allTokens,
chainId,
onTokenSelect,
otherSelectedText,
otherToken,
currencies,
onCurrencySelect,
otherCurrency,
removeToken,
selectedToken,
selectedCurrency,
showSendWithSwap,
theme.primary1,
tokens
theme.primary1
])
if (tokens.length === 0) {
return <ModalInfo>{t('noToken')}</ModalInfo>
}
return (
<FixedSizeList
width="100%"
height={500}
itemCount={tokens.length}
itemCount={currencies.length + 1}
itemSize={56}
style={{ flex: '1' }}
itemKey={index => tokens[index].address}
itemKey={index => currencyKey(currencies[index])}
>
{TokenRow}
{CurrencyRow}
</FixedSizeList>
)
}

@ -1,4 +1,4 @@
import { Token } from '@uniswap/sdk'
import { Currency, Token } from '@uniswap/sdk'
import React, { KeyboardEvent, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
import { isMobile } from 'react-device-detect'
import { useTranslation } from 'react-i18next'
@ -8,7 +8,7 @@ import Card from '../../components/Card'
import { useActiveWeb3React } from '../../hooks'
import { useAllTokens, useToken } from '../../hooks/Tokens'
import useInterval from '../../hooks/useInterval'
import { useAllTokenBalancesTreatingWETHasETH, useTokenBalanceTreatingWETHasETH } from '../../state/wallet/hooks'
import { useAllTokenBalances, useTokenBalance } from '../../state/wallet/hooks'
import { CloseIcon, LinkStyledButton } from '../../theme'
import { isAddress } from '../../utils'
import Column from '../Column'
@ -20,30 +20,28 @@ import CommonBases from './CommonBases'
import { filterTokens } from './filtering'
import { useTokenComparator } from './sorting'
import { PaddedColumn, SearchInput } from './styleds'
import TokenList from './TokenList'
import CurrencyList from './CurrencyList'
import SortButton from './SortButton'
interface TokenSearchModalProps {
interface CurrencySearchModalProps {
isOpen?: boolean
onDismiss?: () => void
hiddenToken?: string
hiddenCurrency?: Currency
showSendWithSwap?: boolean
onTokenSelect?: (address: string) => void
otherSelectedTokenAddress?: string
otherSelectedText?: string
onCurrencySelect?: (currency: Currency) => void
otherSelectedCurrency?: Currency
showCommonBases?: boolean
}
export default function TokenSearchModal({
export default function CurrencySearchModal({
isOpen,
onDismiss,
onTokenSelect,
hiddenToken,
onCurrencySelect,
hiddenCurrency,
showSendWithSwap,
otherSelectedTokenAddress,
otherSelectedText,
otherSelectedCurrency,
showCommonBases = false
}: TokenSearchModalProps) {
}: CurrencySearchModalProps) {
const { t } = useTranslation()
const { account, chainId } = useActiveWeb3React()
const theme = useContext(ThemeContext)
@ -55,8 +53,8 @@ export default function TokenSearchModal({
// if the current input is an address, and we don't have the token in context, try to fetch it and import
const searchToken = useToken(searchQuery)
const searchTokenBalance = useTokenBalanceTreatingWETHasETH(account, searchToken)
const allTokenBalances_ = useAllTokenBalancesTreatingWETHasETH()
const searchTokenBalance = useTokenBalance(account, searchToken)
const allTokenBalances_ = useAllTokenBalances()
const allTokenBalances = searchToken
? {
[searchToken.address]: searchTokenBalance
@ -87,12 +85,12 @@ export default function TokenSearchModal({
]
}, [filteredTokens, searchQuery, searchToken, tokenComparator])
const handleTokenSelect = useCallback(
(address: string) => {
onTokenSelect(address)
const handleCurrencySelect = useCallback(
(currency: Currency) => {
onCurrencySelect(currency)
onDismiss()
},
[onDismiss, onTokenSelect]
[onDismiss, onCurrencySelect]
)
// clear the input on open
@ -129,11 +127,11 @@ export default function TokenSearchModal({
filteredSortedTokens[0].symbol.toLowerCase() === searchQuery.trim().toLowerCase() ||
filteredSortedTokens.length === 1
) {
handleTokenSelect(filteredSortedTokens[0].address)
handleCurrencySelect(filteredSortedTokens[0])
}
}
},
[filteredSortedTokens, handleTokenSelect, searchQuery]
[filteredSortedTokens, handleCurrencySelect, searchQuery]
)
return (
@ -174,7 +172,7 @@ export default function TokenSearchModal({
/>
</Tooltip>
{showCommonBases && (
<CommonBases chainId={chainId} onSelect={handleTokenSelect} selectedTokenAddress={hiddenToken} />
<CommonBases chainId={chainId} onSelect={handleCurrencySelect} selectedCurrency={hiddenCurrency} />
)}
<RowBetween>
<Text fontSize={14} fontWeight={500}>
@ -184,13 +182,12 @@ export default function TokenSearchModal({
</RowBetween>
</PaddedColumn>
<div style={{ width: '100%', height: '1px', backgroundColor: theme.bg2 }} />
<TokenList
tokens={filteredSortedTokens}
allTokenBalances={allTokenBalances}
onTokenSelect={handleTokenSelect}
otherSelectedText={otherSelectedText}
otherToken={otherSelectedTokenAddress}
selectedToken={hiddenToken}
<CurrencyList
currencies={filteredSortedTokens}
allBalances={allTokenBalances}
onCurrencySelect={handleCurrencySelect}
otherCurrency={otherSelectedCurrency}
selectedCurrency={hiddenCurrency}
showSendWithSwap={showSendWithSwap}
/>
<div style={{ width: '100%', height: '1px', backgroundColor: theme.bg2 }} />

@ -1,5 +1,5 @@
import { isAddress } from '../../utils'
import { Pair, Token } from '@uniswap/sdk'
import { Token } from '@uniswap/sdk'
export function filterTokens(tokens: Token[], search: string): Token[] {
if (search.length === 0) return tokens
@ -34,27 +34,3 @@ export function filterTokens(tokens: Token[], search: string): Token[] {
return matchesSearch(symbol) || matchesSearch(name)
})
}
export function filterPairs(pairs: Pair[], search: string): Pair[] {
if (search.trim().length === 0) return pairs
const addressSearch = isAddress(search)
if (addressSearch) {
return pairs.filter(p => {
return (
p.token0.address === addressSearch ||
p.token1.address === addressSearch ||
p.liquidityToken.address === addressSearch
)
})
}
const lowerSearch = search.toLowerCase()
return pairs.filter(pair => {
const pairExpressionA = `${pair.token0.symbol}/${pair.token1.symbol}`.toLowerCase()
if (pairExpressionA.startsWith(lowerSearch)) return true
const pairExpressionB = `${pair.token1.symbol}/${pair.token0.symbol}`.toLowerCase()
if (pairExpressionB.startsWith(lowerSearch)) return true
return filterTokens([pair.token0, pair.token1], search).length > 0
})
}

@ -1,8 +1,7 @@
import { Token, TokenAmount, WETH, Pair } from '@uniswap/sdk'
import { Token, TokenAmount, WETH } from '@uniswap/sdk'
import { useMemo } from 'react'
import { useActiveWeb3React } from '../../hooks'
import { useAllTokenBalancesTreatingWETHasETH } from '../../state/wallet/hooks'
import { DUMMY_PAIRS_TO_PIN } from '../../constants'
import { useAllTokenBalances } from '../../state/wallet/hooks'
// compare two token amounts with highest one coming first
function balanceComparator(balanceA?: TokenAmount, balanceB?: TokenAmount) {
@ -16,26 +15,6 @@ function balanceComparator(balanceA?: TokenAmount, balanceB?: TokenAmount) {
return 0
}
// compare two pairs, favoring "pinned" pairs, and falling back to balances
export function pairComparator(pairA: Pair, pairB: Pair, balanceA?: TokenAmount, balanceB?: TokenAmount) {
const aShouldBePinned =
DUMMY_PAIRS_TO_PIN[pairA?.token0?.chainId]?.some(
dummyPairToPin => dummyPairToPin.liquidityToken.address === pairA?.liquidityToken?.address
) ?? false
const bShouldBePinned =
DUMMY_PAIRS_TO_PIN[pairB?.token0?.chainId]?.some(
dummyPairToPin => dummyPairToPin.liquidityToken.address === pairB?.liquidityToken?.address
) ?? false
if (aShouldBePinned && !bShouldBePinned) {
return -1
} else if (!aShouldBePinned && bShouldBePinned) {
return 1
} else {
return balanceComparator(balanceA, balanceB)
}
}
function getTokenComparator(
weth: Token | undefined,
balances: { [tokenAddress: string]: TokenAmount }
@ -65,7 +44,7 @@ function getTokenComparator(
export function useTokenComparator(inverted: boolean): (tokenA: Token, tokenB: Token) => number {
const { chainId } = useActiveWeb3React()
const weth = WETH[chainId]
const balances = useAllTokenBalancesTreatingWETHasETH()
const balances = useAllTokenBalances()
const comparator = useMemo(() => getTokenComparator(weth, balances ?? {}), [balances, weth])
return useMemo(() => {
if (inverted) {

@ -1,4 +1,4 @@
import { Token } from '@uniswap/sdk'
import { Currency, Token } from '@uniswap/sdk'
import { transparentize } from 'polished'
import React, { useMemo } from 'react'
import styled from 'styled-components'
@ -11,7 +11,7 @@ import { ExternalLink, TYPE } from '../../theme'
import { getEtherscanLink, isDefaultToken } from '../../utils'
import PropsOfExcluding from '../../utils/props-of-excluding'
import QuestionHelper from '../QuestionHelper'
import TokenLogo from '../TokenLogo'
import CurrencyLogo from '../CurrencyLogo'
const Wrapper = styled.div<{ error: boolean }>`
background: ${({ theme, error }) => transparentize(0.9, error ? theme.red1 : theme.yellow1)};
@ -103,7 +103,7 @@ export default function TokenWarningCard({ token, ...rest }: TokenWarningCardPro
<QuestionHelper text={duplicateNameOrSymbol ? DUPLICATE_NAME_HELP_TEXT : HELP_TEXT} />
</Row>
<Row>
<TokenLogo address={token.address} />
<CurrencyLogo currency={token} />
<div style={{ fontWeight: 500 }}>
{token && token.name && token.symbol && token.name !== token.symbol
? `${token.name} (${token.symbol})`
@ -127,11 +127,13 @@ const WarningContainer = styled.div`
padding-right: 1rem;
`
export function TokenWarningCards({ tokens }: { tokens: { [field in Field]?: Token } }) {
export function TokenWarningCards({ currencies }: { currencies: { [field in Field]?: Currency } }) {
return (
<WarningContainer>
{Object.keys(tokens).map(field =>
tokens[field] ? <TokenWarningCard style={{ marginBottom: 14 }} key={field} token={tokens[field]} /> : null
{Object.keys(currencies).map(field =>
currencies[field] instanceof Token ? (
<TokenWarningCard style={{ marginBottom: 14 }} key={field} token={currencies[field]} />
) : null
)}
</WarningContainer>
)

@ -1,4 +1,4 @@
import React from 'react'
import React, { useCallback, useState } from 'react'
import styled from 'styled-components'
import Popover, { PopoverProps } from '../Popover'
@ -16,3 +16,16 @@ interface TooltipProps extends Omit<PopoverProps, 'content'> {
export default function Tooltip({ text, ...rest }: TooltipProps) {
return <Popover content={<TooltipContainer>{text}</TooltipContainer>} {...rest} />
}
export function MouseoverTooltip({ children, ...rest }: Omit<TooltipProps, 'show'>) {
const [show, setShow] = useState(false)
const open = useCallback(() => setShow(true), [setShow])
const close = useCallback(() => setShow(false), [setShow])
return (
<Tooltip {...rest} show={show}>
<div onMouseEnter={open} onMouseLeave={close}>
{children}
</div>
</Tooltip>
)
}

@ -126,6 +126,12 @@ function recentTransactionsOnly(a: TransactionDetails) {
return new Date().getTime() - a.addedTime < 86_400_000
}
const SOCK = (
<span role="img" aria-label="has socks emoji" style={{ marginTop: -4, marginBottom: -4 }}>
🧦
</span>
)
export default function Web3Status() {
const { t } = useTranslation()
const { active, account, connector, error } = useWeb3React()
@ -187,9 +193,10 @@ export default function Web3Status() {
<Text>{pending?.length} Pending</Text> <Loader stroke="white" />
</RowBetween>
) : (
<Text>
{hasSocks ? '🧦' : ''} {ENSName || shortenAddress(account)}
</Text>
<>
{hasSocks ? SOCK : null}
<Text>{ENSName || shortenAddress(account)}</Text>
</>
)}
{!hasPendingTransactions && getStatusIcon()}
</Web3StatusConnected>

@ -31,8 +31,10 @@ function TradeSummary({ trade, allowedSlippage }: { trade: Trade; allowedSlippag
<RowFixed>
<TYPE.black color={theme.text1} fontSize={14}>
{isExactIn
? `${slippageAdjustedAmounts[Field.OUTPUT]?.toSignificant(4)} ${trade.outputAmount.token.symbol}` ?? '-'
: `${slippageAdjustedAmounts[Field.INPUT]?.toSignificant(4)} ${trade.inputAmount.token.symbol}` ?? '-'}
? `${slippageAdjustedAmounts[Field.OUTPUT]?.toSignificant(4)} ${trade.outputAmount.currency.symbol}` ??
'-'
: `${slippageAdjustedAmounts[Field.INPUT]?.toSignificant(4)} ${trade.inputAmount.currency.symbol}` ??
'-'}
</TYPE.black>
</RowFixed>
</RowBetween>
@ -54,7 +56,7 @@ function TradeSummary({ trade, allowedSlippage }: { trade: Trade; allowedSlippag
<QuestionHelper text="A portion of each trade (0.30%) goes to liquidity providers as a protocol incentive." />
</RowFixed>
<TYPE.black fontSize={14} color={theme.text1}>
{realizedLPFee ? `${realizedLPFee.toSignificant(4)} ${trade.inputAmount.token.symbol}` : '-'}
{realizedLPFee ? `${realizedLPFee.toSignificant(4)} ${trade.inputAmount.currency.symbol}` : '-'}
</TYPE.black>
</RowBetween>
</AutoColumn>

@ -1,4 +1,4 @@
import { Percent, TokenAmount, Trade, TradeType } from '@uniswap/sdk'
import { CurrencyAmount, Percent, Trade, TradeType } from '@uniswap/sdk'
import React, { useContext } from 'react'
import { Repeat } from 'react-feather'
import { Text } from 'rebass'
@ -29,10 +29,10 @@ export default function SwapModalFooter({
showInverted: boolean
setShowInverted: (inverted: boolean) => void
severity: number
slippageAdjustedAmounts?: { [field in Field]?: TokenAmount }
slippageAdjustedAmounts?: { [field in Field]?: CurrencyAmount }
onSwap: () => any
parsedAmounts?: { [field in Field]?: TokenAmount }
realizedLPFee?: TokenAmount
parsedAmounts?: { [field in Field]?: CurrencyAmount }
realizedLPFee?: CurrencyAmount
priceImpactWithoutFee?: Percent
confirmText: string
}) {
@ -84,8 +84,8 @@ export default function SwapModalFooter({
{parsedAmounts[Field.OUTPUT] && parsedAmounts[Field.INPUT] && (
<TYPE.black fontSize={14} marginLeft={'4px'}>
{trade?.tradeType === TradeType.EXACT_INPUT
? parsedAmounts[Field.OUTPUT]?.token?.symbol
: parsedAmounts[Field.INPUT]?.token?.symbol}
? parsedAmounts[Field.OUTPUT]?.currency?.symbol
: parsedAmounts[Field.INPUT]?.currency?.symbol}
</TYPE.black>
)}
</RowFixed>
@ -107,7 +107,7 @@ export default function SwapModalFooter({
<QuestionHelper text="A portion of each trade (0.30%) goes to liquidity providers as a protocol incentive." />
</RowFixed>
<TYPE.black fontSize={14}>
{realizedLPFee ? realizedLPFee?.toSignificant(6) + ' ' + trade?.inputAmount?.token?.symbol : '-'}
{realizedLPFee ? realizedLPFee?.toSignificant(6) + ' ' + trade?.inputAmount?.currency?.symbol : '-'}
</TYPE.black>
</RowBetween>
</AutoColumn>

@ -1,4 +1,4 @@
import { Token, TokenAmount } from '@uniswap/sdk'
import { Currency, CurrencyAmount } from '@uniswap/sdk'
import React, { useContext } from 'react'
import { ArrowDown } from 'react-feather'
import { Text } from 'rebass'
@ -8,20 +8,20 @@ import { TYPE } from '../../theme'
import { isAddress, shortenAddress } from '../../utils'
import { AutoColumn } from '../Column'
import { RowBetween, RowFixed } from '../Row'
import TokenLogo from '../TokenLogo'
import CurrencyLogo from '../CurrencyLogo'
import { TruncatedText } from './styleds'
export default function SwapModalHeader({
tokens,
currencies,
formattedAmounts,
slippageAdjustedAmounts,
priceImpactSeverity,
independentField,
recipient
}: {
tokens: { [field in Field]?: Token }
currencies: { [field in Field]?: Currency }
formattedAmounts: { [field in Field]?: string }
slippageAdjustedAmounts: { [field in Field]?: TokenAmount }
slippageAdjustedAmounts: { [field in Field]?: CurrencyAmount }
priceImpactSeverity: number
independentField: Field
recipient: string | null
@ -35,9 +35,9 @@ export default function SwapModalHeader({
{formattedAmounts[Field.INPUT]}
</TruncatedText>
<RowFixed gap="4px">
<TokenLogo address={tokens[Field.INPUT]?.address} size={'24px'} />
<CurrencyLogo currency={currencies[Field.INPUT]} size={'24px'} />
<Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}>
{tokens[Field.INPUT]?.symbol}
{currencies[Field.INPUT]?.symbol}
</Text>
</RowFixed>
</RowBetween>
@ -49,9 +49,9 @@ export default function SwapModalHeader({
{formattedAmounts[Field.OUTPUT]}
</TruncatedText>
<RowFixed gap="4px">
<TokenLogo address={tokens[Field.OUTPUT]?.address} size={'24px'} />
<CurrencyLogo currency={currencies[Field.OUTPUT]} size={'24px'} />
<Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}>
{tokens[Field.OUTPUT]?.symbol}
{currencies[Field.OUTPUT]?.symbol}
</Text>
</RowFixed>
</RowBetween>
@ -60,7 +60,7 @@ export default function SwapModalHeader({
<TYPE.italic textAlign="left" style={{ width: '100%' }}>
{`Output is estimated. You will receive at least `}
<b>
{slippageAdjustedAmounts[Field.OUTPUT]?.toSignificant(6)} {tokens[Field.OUTPUT]?.symbol}
{slippageAdjustedAmounts[Field.OUTPUT]?.toSignificant(6)} {currencies[Field.OUTPUT]?.symbol}
</b>
{' or the transaction will revert.'}
</TYPE.italic>
@ -68,7 +68,7 @@ export default function SwapModalHeader({
<TYPE.italic textAlign="left" style={{ width: '100%' }}>
{`Input is estimated. You will sell at most `}
<b>
{slippageAdjustedAmounts[Field.INPUT]?.toSignificant(6)} {tokens[Field.INPUT]?.symbol}
{slippageAdjustedAmounts[Field.INPUT]?.toSignificant(6)} {currencies[Field.INPUT]?.symbol}
</b>
{' or the transaction will revert.'}
</TYPE.italic>

@ -4,7 +4,7 @@ import { ChevronRight } from 'react-feather'
import { Flex } from 'rebass'
import { ThemeContext } from 'styled-components'
import { TYPE } from '../../theme'
import TokenLogo from '../TokenLogo'
import CurrencyLogo from '../CurrencyLogo'
export default memo(function SwapRoute({ trade }: { trade: Trade }) {
const theme = useContext(ThemeContext)
@ -24,7 +24,7 @@ export default memo(function SwapRoute({ trade }: { trade: Trade }) {
return (
<Fragment key={i}>
<Flex my="0.5rem" alignItems="center" style={{ flexShrink: 0 }}>
<TokenLogo address={token.address} size="1.5rem" />
<CurrencyLogo currency={token} size="1.5rem" />
<TYPE.black fontSize={14} color={theme.text1} ml="0.5rem">
{token.symbol}
</TYPE.black>

@ -1,5 +1,5 @@
import React from 'react'
import { Price, Token } from '@uniswap/sdk'
import { Currency, Price } from '@uniswap/sdk'
import { useContext } from 'react'
import { Repeat } from 'react-feather'
import { Text } from 'rebass'
@ -8,21 +8,27 @@ import { StyledBalanceMaxMini } from './styleds'
interface TradePriceProps {
price?: Price
inputToken?: Token
outputToken?: Token
inputCurrency?: Currency
outputCurrency?: Currency
showInverted: boolean
setShowInverted: (showInverted: boolean) => void
}
export default function TradePrice({ price, inputToken, outputToken, showInverted, setShowInverted }: TradePriceProps) {
export default function TradePrice({
price,
inputCurrency,
outputCurrency,
showInverted,
setShowInverted
}: TradePriceProps) {
const theme = useContext(ThemeContext)
const formattedPrice = showInverted ? price?.toSignificant(6) : price?.invert()?.toSignificant(6)
const show = Boolean(inputToken && outputToken)
const show = Boolean(inputCurrency && outputCurrency)
const label = showInverted
? `${outputToken?.symbol} per ${inputToken?.symbol}`
: `${inputToken?.symbol} per ${outputToken?.symbol}`
? `${outputCurrency?.symbol} per ${inputCurrency?.symbol}`
: `${inputCurrency?.symbol} per ${outputCurrency?.symbol}`
return (
<Text

@ -46,7 +46,13 @@ class MiniRpcProvider implements AsyncSendable {
.catch(error => callback(error, null))
}
public readonly request = async (method: string, params?: unknown[] | object): Promise<unknown> => {
public readonly request = async (
method: string | { method: string; params: unknown[] },
params?: unknown[] | object
): Promise<unknown> => {
if (typeof method !== 'string') {
return this.request(method.method, method.params)
}
const response = await fetch(this.url, {
method: 'POST',
body: JSON.stringify({

@ -0,0 +1,279 @@
[
{
"constant": true,
"inputs": [],
"name": "name",
"outputs": [
{
"name": "",
"type": "string"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "guy",
"type": "address"
},
{
"name": "wad",
"type": "uint256"
}
],
"name": "approve",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "totalSupply",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "src",
"type": "address"
},
{
"name": "dst",
"type": "address"
},
{
"name": "wad",
"type": "uint256"
}
],
"name": "transferFrom",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "wad",
"type": "uint256"
}
],
"name": "withdraw",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "decimals",
"outputs": [
{
"name": "",
"type": "uint8"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "",
"type": "address"
}
],
"name": "balanceOf",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "symbol",
"outputs": [
{
"name": "",
"type": "string"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "dst",
"type": "address"
},
{
"name": "wad",
"type": "uint256"
}
],
"name": "transfer",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [],
"name": "deposit",
"outputs": [],
"payable": true,
"stateMutability": "payable",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "",
"type": "address"
},
{
"name": "",
"type": "address"
}
],
"name": "allowance",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"payable": true,
"stateMutability": "payable",
"type": "fallback"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "src",
"type": "address"
},
{
"indexed": true,
"name": "guy",
"type": "address"
},
{
"indexed": false,
"name": "wad",
"type": "uint256"
}
],
"name": "Approval",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "src",
"type": "address"
},
{
"indexed": true,
"name": "dst",
"type": "address"
},
{
"indexed": false,
"name": "wad",
"type": "uint256"
}
],
"name": "Transfer",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "dst",
"type": "address"
},
{
"indexed": false,
"name": "wad",
"type": "uint256"
}
],
"name": "Deposit",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "src",
"type": "address"
},
{
"indexed": false,
"name": "wad",
"type": "uint256"
}
],
"name": "Withdrawal",
"type": "event"
}
]

@ -1,4 +1,4 @@
import { ChainId, JSBI, Percent, Token, WETH, Pair, TokenAmount } from '@uniswap/sdk'
import { ChainId, JSBI, Percent, Token, WETH } from '@uniswap/sdk'
import { fortmatic, injected, portis, walletconnect, walletlink } from '../connectors'
import { COMP, DAI, MKR, USDC, USDT } from './tokens/mainnet'
@ -36,38 +36,14 @@ export const BASES_TO_TRACK_LIQUIDITY_FOR: ChainTokenList = {
[ChainId.MAINNET]: [...WETH_ONLY[ChainId.MAINNET], DAI, USDC, USDT]
}
export const DUMMY_PAIRS_TO_PIN: { readonly [chainId in ChainId]?: Pair[] } = {
export const PINNED_PAIRS: { readonly [chainId in ChainId]?: [Token, Token][] } = {
[ChainId.MAINNET]: [
new Pair(
new TokenAmount(
[
new Token(ChainId.MAINNET, '0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643', 8, 'cDAI', 'Compound Dai'),
'0'
),
new TokenAmount(
new Token(ChainId.MAINNET, '0x39AA39c021dfbaE8faC545936693aC917d5E7563', 8, 'cUSDC', 'Compound USD Coin'),
'0'
)
),
new Pair(
new TokenAmount(
new Token(ChainId.MAINNET, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 6, 'USDC', 'USD//C'),
'0'
),
new TokenAmount(
new Token(ChainId.MAINNET, '0xdAC17F958D2ee523a2206206994597C13D831ec7', 6, 'USDT', 'Tether USD'),
'0'
)
),
new Pair(
new TokenAmount(
new Token(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18, 'DAI', 'Dai Stablecoin'),
'0'
),
new TokenAmount(
new Token(ChainId.MAINNET, '0xdAC17F958D2ee523a2206206994597C13D831ec7', 6, 'USDT', 'Tether USD'),
'0'
)
)
new Token(ChainId.MAINNET, '0x39AA39c021dfbaE8faC545936693aC917d5E7563', 8, 'cUSDC', 'Compound USD Coin')
],
[USDC, USDT],
[DAI, USDT]
]
}

@ -14,14 +14,6 @@ export const ALL_TOKENS: AllTokens = [
...KOVAN_TOKENS,
...ROPSTEN_TOKENS
]
// remap WETH to ETH
.map(token => {
if (token.equals(WETH[token.chainId])) {
;(token as any).symbol = 'ETH'
;(token as any).name = 'Ether'
}
return token
})
// put into an object
.reduce<AllTokens>(
(tokenMap, token) => {

@ -1,18 +1,33 @@
import { Token, TokenAmount, Pair } from '@uniswap/sdk'
import { TokenAmount, Pair, Currency } from '@uniswap/sdk'
import { useMemo } from 'react'
import { abi as IUniswapV2PairABI } from '@uniswap/v2-core/build/IUniswapV2Pair.json'
import { Interface } from '@ethersproject/abi'
import { useActiveWeb3React } from '../hooks'
import { useMultipleContractSingleData } from '../state/multicall/hooks'
import { wrappedCurrency } from '../utils/wrappedCurrency'
const PAIR_INTERFACE = new Interface(IUniswapV2PairABI)
/*
* if loading, return undefined
* if no pair created yet, return null
* if pair already created (even if 0 reserves), return pair
*/
export function usePairs(tokens: [Token | undefined, Token | undefined][]): (undefined | Pair | null)[] {
export enum PairState {
LOADING,
NOT_EXISTS,
EXISTS,
INVALID
}
export function usePairs(currencies: [Currency | undefined, Currency | undefined][]): [PairState, Pair | null][] {
const { chainId } = useActiveWeb3React()
const tokens = useMemo(
() =>
currencies.map(([currencyA, currencyB]) => [
wrappedCurrency(currencyA, chainId),
wrappedCurrency(currencyB, chainId)
]),
[chainId, currencies]
)
const pairAddresses = useMemo(
() =>
tokens.map(([tokenA, tokenB]) => {
@ -29,15 +44,19 @@ export function usePairs(tokens: [Token | undefined, Token | undefined][]): (und
const tokenA = tokens[i][0]
const tokenB = tokens[i][1]
if (loading || !tokenA || !tokenB) return undefined
if (!reserves) return null
if (loading) return [PairState.LOADING, null]
if (!tokenA || !tokenB || tokenA.equals(tokenB)) return [PairState.INVALID, null]
if (!reserves) return [PairState.NOT_EXISTS, null]
const { reserve0, reserve1 } = reserves
const [token0, token1] = tokenA.sortsBefore(tokenB) ? [tokenA, tokenB] : [tokenB, tokenA]
return new Pair(new TokenAmount(token0, reserve0.toString()), new TokenAmount(token1, reserve1.toString()))
return [
PairState.EXISTS,
new Pair(new TokenAmount(token0, reserve0.toString()), new TokenAmount(token1, reserve1.toString()))
]
})
}, [results, tokens])
}
export function usePair(tokenA?: Token, tokenB?: Token): undefined | Pair | null {
export function usePair(tokenA?: Currency, tokenB?: Currency): [PairState, Pair | null] {
return usePairs([[tokenA, tokenB]])[0]
}

@ -1,4 +1,20 @@
import { JSBI, Pair, Percent, Route, Token, TokenAmount, Trade, TradeType, WETH } from '@uniswap/sdk'
import { AddressZero } from '@ethersproject/constants'
import {
BigintIsh,
Currency,
CurrencyAmount,
currencyEquals,
ETHER,
JSBI,
Pair,
Percent,
Route,
Token,
TokenAmount,
Trade,
TradeType,
WETH
} from '@uniswap/sdk'
import { useMemo } from 'react'
import { useActiveWeb3React } from '../hooks'
import { useAllTokens } from '../hooks/Tokens'
@ -6,7 +22,6 @@ import { useV1FactoryContract } from '../hooks/useContract'
import { Version } from '../hooks/useToggledVersion'
import { NEVER_RELOAD, useSingleCallResult, useSingleContractMultipleData } from '../state/multicall/hooks'
import { useETHBalances, useTokenBalance, useTokenBalances } from '../state/wallet/hooks'
import { AddressZero } from '@ethersproject/constants'
export function useV1ExchangeAddress(tokenAddress?: string): string | undefined {
const contract = useV1FactoryContract()
@ -15,18 +30,25 @@ export function useV1ExchangeAddress(tokenAddress?: string): string | undefined
return useSingleCallResult(contract, 'getExchange', inputs)?.result?.[0]
}
class MockV1Pair extends Pair {}
export class MockV1Pair extends Pair {
constructor(etherAmount: BigintIsh, tokenAmount: TokenAmount) {
super(tokenAmount, new TokenAmount(WETH[tokenAmount.token.chainId], etherAmount))
}
}
function useMockV1Pair(token?: Token): MockV1Pair | undefined {
const isWETH: boolean = token && WETH[token.chainId] ? token.equals(WETH[token.chainId]) : false
function useMockV1Pair(inputCurrency?: Currency): MockV1Pair | undefined {
const token = inputCurrency instanceof Token ? inputCurrency : undefined
const isWETH = Boolean(token && token.equals(WETH[token.chainId]))
const v1PairAddress = useV1ExchangeAddress(isWETH ? undefined : token?.address)
const tokenBalance = useTokenBalance(v1PairAddress, token)
const ETHBalance = useETHBalances([v1PairAddress])[v1PairAddress ?? '']
return tokenBalance && ETHBalance && token
? new MockV1Pair(tokenBalance, new TokenAmount(WETH[token.chainId], ETHBalance.toString()))
: undefined
return useMemo(
() =>
token && tokenBalance && ETHBalance && inputCurrency ? new MockV1Pair(ETHBalance.raw, tokenBalance) : undefined,
[ETHBalance, inputCurrency, token, tokenBalance]
)
}
// returns all v1 exchange addresses in the user's token list
@ -41,8 +63,7 @@ export function useAllTokenV1Exchanges(): { [exchangeAddress: string]: Token } {
() =>
data?.reduce<{ [exchangeAddress: string]: Token }>((memo, { result }, ix) => {
if (result?.[0] && result[0] !== AddressZero) {
const token = allTokens[args[ix][0]]
memo[result[0]] = token
memo[result[0]] = allTokens[args[ix][0]]
}
return memo
}, {}) ?? {},
@ -56,13 +77,13 @@ export function useUserHasLiquidityInAllTokens(): boolean | undefined {
const exchanges = useAllTokenV1Exchanges()
const fakeLiquidityTokens = useMemo(
const v1ExchangeLiquidityTokens = useMemo(
() =>
chainId ? Object.keys(exchanges).map(address => new Token(chainId, address, 18, 'UNI-V1', 'Uniswap V1')) : [],
[chainId, exchanges]
)
const balances = useTokenBalances(account ?? undefined, fakeLiquidityTokens)
const balances = useTokenBalances(account ?? undefined, v1ExchangeLiquidityTokens)
return useMemo(
() =>
@ -79,32 +100,30 @@ export function useUserHasLiquidityInAllTokens(): boolean | undefined {
*/
export function useV1Trade(
isExactIn?: boolean,
inputToken?: Token,
outputToken?: Token,
exactAmount?: TokenAmount
inputCurrency?: Currency,
outputCurrency?: Currency,
exactAmount?: CurrencyAmount
): Trade | undefined {
const { chainId } = useActiveWeb3React()
// get the mock v1 pairs
const inputPair = useMockV1Pair(inputToken)
const outputPair = useMockV1Pair(outputToken)
const inputPair = useMockV1Pair(inputCurrency)
const outputPair = useMockV1Pair(outputCurrency)
const inputIsWETH = (inputToken && chainId && WETH[chainId] && inputToken.equals(WETH[chainId])) ?? false
const outputIsWETH = (outputToken && chainId && WETH[chainId] && outputToken.equals(WETH[chainId])) ?? false
const inputIsETH = inputCurrency === ETHER
const outputIsETH = outputCurrency === ETHER
// construct a direct or through ETH v1 route
let pairs: Pair[] = []
if (inputIsWETH && outputPair) {
if (inputIsETH && outputPair) {
pairs = [outputPair]
} else if (outputIsWETH && inputPair) {
} else if (outputIsETH && inputPair) {
pairs = [inputPair]
}
// if neither are WETH, it's token-to-token (if they both exist)
// if neither are ETH, it's token-to-token (if they both exist)
else if (inputPair && outputPair) {
pairs = [inputPair, outputPair]
}
const route = inputToken && pairs && pairs.length > 0 && new Route(pairs, inputToken)
const route = inputCurrency && pairs && pairs.length > 0 && new Route(pairs, inputCurrency)
let v1Trade: Trade | undefined
try {
v1Trade =
@ -125,14 +144,13 @@ export function getTradeVersion(trade?: Trade): Version | undefined {
// returns the v1 exchange against which a trade should be executed
export function useV1TradeExchangeAddress(trade: Trade | undefined): string | undefined {
const tokenAddress: string | undefined = useMemo(() => {
const tradeVersion = getTradeVersion(trade)
const isV1 = tradeVersion === Version.v1
return isV1
? trade &&
WETH[trade.inputAmount.token.chainId] &&
trade.inputAmount.token.equals(WETH[trade.inputAmount.token.chainId])
if (!trade) return undefined
const isV1 = getTradeVersion(trade) === Version.v1
if (!isV1) return undefined
return trade.inputAmount instanceof TokenAmount
? trade.inputAmount.token.address
: trade.outputAmount instanceof TokenAmount
? trade.outputAmount.token.address
: trade?.inputAmount?.token?.address
: undefined
}, [trade])
return useV1ExchangeAddress(tokenAddress)
@ -140,7 +158,8 @@ export function useV1TradeExchangeAddress(trade: Trade | undefined): string | un
const ZERO_PERCENT = new Percent('0')
const ONE_HUNDRED_PERCENT = new Percent('1')
// returns whether tradeB is better than tradeA by at least a threshold
// returns whether tradeB is better than tradeA by at least a threshold percentage amount
export function isTradeBetter(
tradeA: Trade | undefined,
tradeB: Trade | undefined,
@ -150,8 +169,8 @@ export function isTradeBetter(
if (
tradeA.tradeType !== tradeB.tradeType ||
!tradeA.inputAmount.token.equals(tradeB.inputAmount.token) ||
!tradeB.outputAmount.token.equals(tradeB.outputAmount.token)
!currencyEquals(tradeA.inputAmount.currency, tradeB.inputAmount.currency) ||
!currencyEquals(tradeB.outputAmount.currency, tradeB.outputAmount.currency)
) {
throw new Error('Trades are not comparable')
}

@ -1,5 +1,5 @@
import { parseBytes32String } from '@ethersproject/strings'
import { ChainId, Token, WETH } from '@uniswap/sdk'
import { ChainId, Currency, ETHER, Token } from '@uniswap/sdk'
import { useMemo } from 'react'
import { ALL_TOKENS } from '../constants/tokens'
import { NEVER_RELOAD, useSingleCallResult } from '../state/multicall/hooks'
@ -15,7 +15,8 @@ export function useAllTokens(): { [address: string]: Token } {
return useMemo(() => {
if (!chainId) return {}
const tokens = userAddedTokens
return (
userAddedTokens
// reduce into all ALL_TOKENS filtered by the current chain
.reduce<{ [address: string]: Token }>(
(tokenMap, token) => {
@ -26,15 +27,7 @@ export function useAllTokens(): { [address: string]: Token } {
// want to make a copy in every iteration
{ ...ALL_TOKENS[chainId as ChainId] }
)
const weth = WETH[chainId as ChainId]
if (weth) {
// we have to replace it as a workaround because if it is automatically
// fetched by address it will cause an invariant when used in constructing
// pairs since we replace the name and symbol with 'ETH' and 'Ether'
tokens[weth.address] = weth
}
return tokens
)
}, [userAddedTokens, chainId])
}
@ -100,3 +93,9 @@ export function useToken(tokenAddress?: string): Token | undefined | null {
tokenNameBytes32.result
])
}
export function useCurrency(currencyId: string | undefined): Currency | null | undefined {
const isETH = currencyId?.toUpperCase() === 'ETH'
const token = useToken(isETH ? undefined : currencyId)
return isETH ? ETHER : token
}

@ -1,17 +1,22 @@
import { Pair, Token, TokenAmount, Trade } from '@uniswap/sdk'
import { Currency, CurrencyAmount, Pair, Token, Trade } from '@uniswap/sdk'
import flatMap from 'lodash.flatmap'
import { useMemo } from 'react'
import { BASES_TO_CHECK_TRADES_AGAINST } from '../constants'
import { usePairs } from '../data/Reserves'
import { PairState, usePairs } from '../data/Reserves'
import { wrappedCurrency } from '../utils/wrappedCurrency'
import { useActiveWeb3React } from './index'
function useAllCommonPairs(tokenA?: Token, tokenB?: Token): Pair[] {
function useAllCommonPairs(currencyA?: Currency, currencyB?: Currency): Pair[] {
const { chainId } = useActiveWeb3React()
const bases: Token[] = chainId ? BASES_TO_CHECK_TRADES_AGAINST[chainId] : []
const [tokenA, tokenB] = chainId
? [wrappedCurrency(currencyA, chainId), wrappedCurrency(currencyB, chainId)]
: [undefined, undefined]
const allPairCombinations: [Token | undefined, Token | undefined][] = useMemo(
() => [
// the direct pair
@ -34,9 +39,9 @@ function useAllCommonPairs(tokenA?: Token, tokenB?: Token): Pair[] {
Object.values(
allPairs
// filter out invalid pairs
.filter((p): p is Pair => !!p)
.filter((result): result is [PairState.EXISTS, Pair] => Boolean(result[0] === PairState.EXISTS && result[1]))
// filter out duplicated pairs
.reduce<{ [pairAddress: string]: Pair }>((memo, curr) => {
.reduce<{ [pairAddress: string]: Pair }>((memo, [, curr]) => {
memo[curr.liquidityToken.address] = memo[curr.liquidityToken.address] ?? curr
return memo
}, {})
@ -48,27 +53,32 @@ function useAllCommonPairs(tokenA?: Token, tokenB?: Token): Pair[] {
/**
* Returns the best trade for the exact amount of tokens in to the given token out
*/
export function useTradeExactIn(amountIn?: TokenAmount, tokenOut?: Token): Trade | null {
const allowedPairs = useAllCommonPairs(amountIn?.token, tokenOut)
export function useTradeExactIn(currencyAmountIn?: CurrencyAmount, currencyOut?: Currency): Trade | null {
const allowedPairs = useAllCommonPairs(currencyAmountIn?.currency, currencyOut)
return useMemo(() => {
if (amountIn && tokenOut && allowedPairs.length > 0) {
return Trade.bestTradeExactIn(allowedPairs, amountIn, tokenOut, { maxHops: 3, maxNumResults: 1 })[0] ?? null
if (currencyAmountIn && currencyOut && allowedPairs.length > 0) {
return (
Trade.bestTradeExactIn(allowedPairs, currencyAmountIn, currencyOut, { maxHops: 3, maxNumResults: 1 })[0] ?? null
)
}
return null
}, [allowedPairs, amountIn, tokenOut])
}, [allowedPairs, currencyAmountIn, currencyOut])
}
/**
* Returns the best trade for the token in to the exact amount of token out
*/
export function useTradeExactOut(tokenIn?: Token, amountOut?: TokenAmount): Trade | null {
const allowedPairs = useAllCommonPairs(tokenIn, amountOut?.token)
export function useTradeExactOut(currencyIn?: Currency, currencyAmountOut?: CurrencyAmount): Trade | null {
const allowedPairs = useAllCommonPairs(currencyIn, currencyAmountOut?.currency)
return useMemo(() => {
if (tokenIn && amountOut && allowedPairs.length > 0) {
return Trade.bestTradeExactOut(allowedPairs, tokenIn, amountOut, { maxHops: 3, maxNumResults: 1 })[0] ?? null
if (currencyIn && currencyAmountOut && allowedPairs.length > 0) {
return (
Trade.bestTradeExactOut(allowedPairs, currencyIn, currencyAmountOut, { maxHops: 3, maxNumResults: 1 })[0] ??
null
)
}
return null
}, [allowedPairs, tokenIn, amountOut])
}, [allowedPairs, currencyIn, currencyAmountOut])
}

@ -1,6 +1,6 @@
import { MaxUint256 } from '@ethersproject/constants'
import { TransactionResponse } from '@ethersproject/providers'
import { Trade, WETH, TokenAmount } from '@uniswap/sdk'
import { Trade, TokenAmount, CurrencyAmount, ETHER } from '@uniswap/sdk'
import { useCallback, useMemo } from 'react'
import { ROUTER_ADDRESS } from '../constants'
import { useTokenAllowance } from '../data/Allowances'
@ -22,19 +22,18 @@ export enum ApprovalState {
// returns a variable indicating the state of the approval and a function which approves if necessary or early returns
export function useApproveCallback(
amountToApprove?: TokenAmount,
amountToApprove?: CurrencyAmount,
spender?: string
): [ApprovalState, () => Promise<void>] {
const { account } = useActiveWeb3React()
const currentAllowance = useTokenAllowance(amountToApprove?.token, account ?? undefined, spender)
const pendingApproval = useHasPendingApproval(amountToApprove?.token?.address, spender)
const token = amountToApprove instanceof TokenAmount ? amountToApprove.token : undefined
const currentAllowance = useTokenAllowance(token, account ?? undefined, spender)
const pendingApproval = useHasPendingApproval(token?.address, spender)
// check the current approval status
const approvalState: ApprovalState = useMemo(() => {
if (!amountToApprove || !spender) return ApprovalState.UNKNOWN
// we treat WETH as ETH which requires no approvals
if (amountToApprove.token.equals(WETH[amountToApprove.token.chainId])) return ApprovalState.APPROVED
if (amountToApprove.currency === ETHER) return ApprovalState.APPROVED
// we might not have enough data to know whether or not we need to approve
if (!currentAllowance) return ApprovalState.UNKNOWN
@ -46,7 +45,7 @@ export function useApproveCallback(
: ApprovalState.APPROVED
}, [amountToApprove, currentAllowance, pendingApproval, spender])
const tokenContract = useTokenContract(amountToApprove?.token?.address)
const tokenContract = useTokenContract(token?.address)
const addTransaction = useTransactionAdder()
const approve = useCallback(async (): Promise<void> => {
@ -54,6 +53,10 @@ export function useApproveCallback(
console.error('approve was called unnecessarily')
return
}
if (!token) {
console.error('no token')
return
}
if (!tokenContract) {
console.error('tokenContract is null')
@ -83,15 +86,15 @@ export function useApproveCallback(
})
.then((response: TransactionResponse) => {
addTransaction(response, {
summary: 'Approve ' + amountToApprove.token.symbol,
approval: { tokenAddress: amountToApprove.token.address, spender: spender }
summary: 'Approve ' + amountToApprove.currency.symbol,
approval: { tokenAddress: token.address, spender: spender }
})
})
.catch((error: Error) => {
console.debug('Failed to approve token', error)
throw error
})
}, [approvalState, tokenContract, spender, amountToApprove, addTransaction])
}, [approvalState, token, tokenContract, amountToApprove, spender, addTransaction])
return [approvalState, approve]
}

@ -1,25 +0,0 @@
import { useCallback, useEffect } from 'react'
// modified from https://usehooks.com/useKeyPress/
export default function useBodyKeyDown(targetKey: string, onKeyDown: () => void, suppressOnKeyDown = false) {
const downHandler = useCallback(
event => {
const {
target: { tagName },
key
} = event
if (key === targetKey && tagName === 'BODY' && !suppressOnKeyDown) {
event.preventDefault()
onKeyDown()
}
},
[targetKey, onKeyDown, suppressOnKeyDown]
)
useEffect(() => {
window.addEventListener('keydown', downHandler)
return () => {
window.removeEventListener('keydown', downHandler)
}
}, [downHandler])
}

@ -1,10 +1,11 @@
import { Contract } from '@ethersproject/contracts'
import { ChainId } from '@uniswap/sdk'
import { ChainId, WETH } from '@uniswap/sdk'
import { abi as IUniswapV2PairABI } from '@uniswap/v2-core/build/IUniswapV2Pair.json'
import { useMemo } from 'react'
import { ERC20_BYTES32_ABI } from '../constants/abis/erc20'
import UNISOCKS_ABI from '../constants/abis/unisocks.json'
import ERC20_ABI from '../constants/abis/erc20.json'
import WETH_ABI from '../constants/abis/weth.json'
import { MIGRATOR_ABI, MIGRATOR_ADDRESS } from '../constants/abis/migrator'
import { MULTICALL_ABI, MULTICALL_NETWORKS } from '../constants/multicall'
import { V1_EXCHANGE_ABI, V1_FACTORY_ABI, V1_FACTORY_ADDRESSES } from '../constants/v1'
@ -43,6 +44,11 @@ export function useTokenContract(tokenAddress?: string, withSignerIfPossible?: b
return useContract(tokenAddress, ERC20_ABI, withSignerIfPossible)
}
export function useWETHContract(withSignerIfPossible?: boolean): Contract | null {
const { chainId } = useActiveWeb3React()
return useContract(chainId ? WETH[chainId].address : undefined, WETH_ABI, withSignerIfPossible)
}
export function useBytes32TokenContract(tokenAddress?: string, withSignerIfPossible?: boolean): Contract | null {
return useContract(tokenAddress, ERC20_BYTES32_ABI, withSignerIfPossible)
}

@ -1,6 +1,6 @@
import { JSBI } from '@uniswap/sdk'
import { useMemo } from 'react'
import { useSingleCallResult } from '../state/multicall/hooks'
import { NEVER_RELOAD, useSingleCallResult } from '../state/multicall/hooks'
import { useActiveWeb3React } from './index'
import { useSocksController } from './useContract'
@ -8,7 +8,7 @@ export default function useSocksBalance(): JSBI | undefined {
const { account } = useActiveWeb3React()
const socksContract = useSocksController()
const { result } = useSingleCallResult(socksContract, 'balanceOf', [account ?? undefined], { blocksPerFetch: 100 })
const { result } = useSingleCallResult(socksContract, 'balanceOf', [account ?? undefined], NEVER_RELOAD)
const data = result?.[0]
return data ? JSBI.BigInt(data.toString()) : undefined
}

@ -1,61 +1,17 @@
import { BigNumber } from '@ethersproject/bignumber'
import { MaxUint256 } from '@ethersproject/constants'
import { Contract } from '@ethersproject/contracts'
import { Trade, TradeType, WETH } from '@uniswap/sdk'
import { JSBI, Percent, Router, Trade, TradeType } from '@uniswap/sdk'
import { useMemo } from 'react'
import { DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE, ROUTER_ADDRESS } from '../constants'
import { useTokenAllowance } from '../data/Allowances'
import { BIPS_BASE, DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE } from '../constants'
import { getTradeVersion, useV1TradeExchangeAddress } from '../data/V1'
import { Field } from '../state/swap/actions'
import { useTransactionAdder } from '../state/transactions/hooks'
import { calculateGasMargin, getRouterContract, shortenAddress, isAddress } from '../utils'
import { computeSlippageAdjustedAmounts } from '../utils/prices'
import { calculateGasMargin, getRouterContract, isAddress, shortenAddress } from '../utils'
import v1SwapArguments from '../utils/v1SwapArguments'
import { useActiveWeb3React } from './index'
import { useV1ExchangeContract } from './useContract'
import useENS from './useENS'
import { Version } from './useToggledVersion'
enum SwapType {
EXACT_TOKENS_FOR_TOKENS,
EXACT_TOKENS_FOR_ETH,
EXACT_ETH_FOR_TOKENS,
TOKENS_FOR_EXACT_TOKENS,
TOKENS_FOR_EXACT_ETH,
ETH_FOR_EXACT_TOKENS,
V1_EXACT_ETH_FOR_TOKENS,
V1_EXACT_TOKENS_FOR_ETH,
V1_EXACT_TOKENS_FOR_TOKENS,
V1_ETH_FOR_EXACT_TOKENS,
V1_TOKENS_FOR_EXACT_ETH,
V1_TOKENS_FOR_EXACT_TOKENS
}
function getSwapType(trade: Trade | undefined): SwapType | undefined {
if (!trade) return undefined
const chainId = trade.inputAmount.token.chainId
const inputWETH = trade.inputAmount.token.equals(WETH[chainId])
const outputWETH = trade.outputAmount.token.equals(WETH[chainId])
const isExactIn = trade.tradeType === TradeType.EXACT_INPUT
const isV1 = getTradeVersion(trade) === Version.v1
if (isExactIn) {
if (inputWETH) {
return isV1 ? SwapType.V1_EXACT_ETH_FOR_TOKENS : SwapType.EXACT_ETH_FOR_TOKENS
} else if (outputWETH) {
return isV1 ? SwapType.V1_EXACT_TOKENS_FOR_ETH : SwapType.EXACT_TOKENS_FOR_ETH
} else {
return isV1 ? SwapType.V1_EXACT_TOKENS_FOR_TOKENS : SwapType.EXACT_TOKENS_FOR_TOKENS
}
} else {
if (inputWETH) {
return isV1 ? SwapType.V1_ETH_FOR_EXACT_TOKENS : SwapType.ETH_FOR_EXACT_TOKENS
} else if (outputWETH) {
return isV1 ? SwapType.V1_TOKENS_FOR_EXACT_ETH : SwapType.TOKENS_FOR_EXACT_ETH
} else {
return isV1 ? SwapType.V1_TOKENS_FOR_EXACT_TOKENS : SwapType.TOKENS_FOR_EXACT_TOKENS
}
}
}
// returns a function that will execute a swap, if the parameters are all valid
// and the user has approved the slippage adjusted input amount for the trade
export function useSwapCallback(
@ -72,31 +28,10 @@ export function useSwapCallback(
const tradeVersion = getTradeVersion(trade)
const v1Exchange = useV1ExchangeContract(useV1TradeExchangeAddress(trade), true)
const inputAllowance = useTokenAllowance(
trade?.inputAmount?.token,
account ?? undefined,
tradeVersion === Version.v1 ? v1Exchange?.address : ROUTER_ADDRESS
)
return useMemo(() => {
if (!trade || !recipient || !library || !account || !tradeVersion || !chainId) return null
// will always be defined
const {
[Field.INPUT]: slippageAdjustedInput,
[Field.OUTPUT]: slippageAdjustedOutput
} = computeSlippageAdjustedAmounts(trade, allowedSlippage)
if (!slippageAdjustedInput || !slippageAdjustedOutput) return null
// no allowance
if (
!trade.inputAmount.token.equals(WETH[chainId]) &&
(!inputAllowance || slippageAdjustedInput.greaterThan(inputAllowance))
) {
return null
}
return async function onSwap() {
const contract: Contract | null =
tradeVersion === Version.v2 ? getRouterContract(chainId, library, account) : v1Exchange
@ -104,123 +39,43 @@ export function useSwapCallback(
throw new Error('Failed to get a swap contract')
}
const path = trade.route.path.map(t => t.address)
const swapMethods = []
const deadlineFromNow: number = Math.ceil(Date.now() / 1000) + deadline
switch (tradeVersion) {
case Version.v2:
swapMethods.push(
Router.swapCallParameters(trade, {
feeOnTransfer: false,
allowedSlippage: new Percent(JSBI.BigInt(allowedSlippage), BIPS_BASE),
recipient,
ttl: deadline
})
)
const swapType = getSwapType(trade)
// let estimate: Function, method: Function,
let methodNames: string[],
args: Array<string | string[] | number>,
value: BigNumber | null = null
switch (swapType) {
case SwapType.EXACT_TOKENS_FOR_TOKENS:
methodNames = ['swapExactTokensForTokens', 'swapExactTokensForTokensSupportingFeeOnTransferTokens']
args = [
slippageAdjustedInput.raw.toString(),
slippageAdjustedOutput.raw.toString(),
path,
if (trade.tradeType === TradeType.EXACT_INPUT) {
swapMethods.push(
Router.swapCallParameters(trade, {
feeOnTransfer: true,
allowedSlippage: new Percent(JSBI.BigInt(allowedSlippage), BIPS_BASE),
recipient,
deadlineFromNow
]
ttl: deadline
})
)
}
break
case SwapType.TOKENS_FOR_EXACT_TOKENS:
methodNames = ['swapTokensForExactTokens']
args = [
slippageAdjustedOutput.raw.toString(),
slippageAdjustedInput.raw.toString(),
path,
case Version.v1:
swapMethods.push(
v1SwapArguments(trade, {
allowedSlippage: new Percent(JSBI.BigInt(allowedSlippage), BIPS_BASE),
recipient,
deadlineFromNow
]
ttl: deadline
})
)
break
case SwapType.EXACT_ETH_FOR_TOKENS:
methodNames = ['swapExactETHForTokens', 'swapExactETHForTokensSupportingFeeOnTransferTokens']
args = [slippageAdjustedOutput.raw.toString(), path, recipient, deadlineFromNow]
value = BigNumber.from(slippageAdjustedInput.raw.toString())
break
case SwapType.TOKENS_FOR_EXACT_ETH:
methodNames = ['swapTokensForExactETH']
args = [
slippageAdjustedOutput.raw.toString(),
slippageAdjustedInput.raw.toString(),
path,
recipient,
deadlineFromNow
]
break
case SwapType.EXACT_TOKENS_FOR_ETH:
methodNames = ['swapExactTokensForETH', 'swapExactTokensForETHSupportingFeeOnTransferTokens']
args = [
slippageAdjustedInput.raw.toString(),
slippageAdjustedOutput.raw.toString(),
path,
recipient,
deadlineFromNow
]
break
case SwapType.ETH_FOR_EXACT_TOKENS:
methodNames = ['swapETHForExactTokens']
args = [slippageAdjustedOutput.raw.toString(), path, recipient, deadlineFromNow]
value = BigNumber.from(slippageAdjustedInput.raw.toString())
break
case SwapType.V1_EXACT_ETH_FOR_TOKENS:
methodNames = ['ethToTokenTransferInput']
args = [slippageAdjustedOutput.raw.toString(), deadlineFromNow, recipient]
value = BigNumber.from(slippageAdjustedInput.raw.toString())
break
case SwapType.V1_EXACT_TOKENS_FOR_TOKENS:
methodNames = ['tokenToTokenTransferInput']
args = [
slippageAdjustedInput.raw.toString(),
slippageAdjustedOutput.raw.toString(),
1,
deadlineFromNow,
recipient,
trade.outputAmount.token.address
]
break
case SwapType.V1_EXACT_TOKENS_FOR_ETH:
methodNames = ['tokenToEthTransferOutput']
args = [
slippageAdjustedOutput.raw.toString(),
slippageAdjustedInput.raw.toString(),
deadlineFromNow,
recipient
]
break
case SwapType.V1_ETH_FOR_EXACT_TOKENS:
methodNames = ['ethToTokenTransferOutput']
args = [slippageAdjustedOutput.raw.toString(), deadlineFromNow, recipient]
value = BigNumber.from(slippageAdjustedInput.raw.toString())
break
case SwapType.V1_TOKENS_FOR_EXACT_ETH:
methodNames = ['tokenToEthTransferOutput']
args = [
slippageAdjustedOutput.raw.toString(),
slippageAdjustedInput.raw.toString(),
deadlineFromNow,
recipient
]
break
case SwapType.V1_TOKENS_FOR_EXACT_TOKENS:
methodNames = ['tokenToTokenTransferOutput']
args = [
slippageAdjustedOutput.raw.toString(),
slippageAdjustedInput.raw.toString(),
MaxUint256.toString(),
deadlineFromNow,
recipient,
trade.outputAmount.token.address
]
break
default:
throw new Error(`Unhandled swap type: ${swapType}`)
}
const safeGasEstimates: (BigNumber | undefined)[] = await Promise.all(
methodNames.map(methodName =>
swapMethods.map(({ args, methodName, value }) =>
contract.estimateGas[methodName](...args, value ? { value } : {})
.then(calculateGasMargin)
.catch(error => {
@ -251,7 +106,7 @@ export function useSwapCallback(
// if only 1 method exists, either:
// a) the token is doing something weird not related to FoT (e.g. enforcing a whitelist)
// b) the token is FoT and the user specified an exact output, which is not allowed
if (methodNames.length === 1) {
if (swapMethods.length === 1) {
throw Error(
`An error occurred. If either of the tokens you're swapping take a fee on transfer, you must specify an exact input amount.`
)
@ -259,7 +114,7 @@ export function useSwapCallback(
// if 2 methods exists, either:
// a) the token is doing something weird not related to FoT (e.g. enforcing a whitelist)
// b) the token is FoT and is taking more than the specified slippage
else if (methodNames.length === 2) {
else if (swapMethods.length === 2) {
throw Error(
`An error occurred. If either of the tokens you're swapping take a fee on transfer, you must specify a slippage tolerance higher than the fee.`
)
@ -267,7 +122,7 @@ export function useSwapCallback(
throw Error('This transaction would fail. Please contact support.')
}
} else {
const methodName = methodNames[indexOfSuccessfulEstimation]
const { methodName, args, value } = swapMethods[indexOfSuccessfulEstimation]
const safeGasEstimate = safeGasEstimates[indexOfSuccessfulEstimation]
return contract[methodName](...args, {
@ -275,10 +130,10 @@ export function useSwapCallback(
...(value ? { value } : {})
})
.then((response: any) => {
const inputSymbol = trade.inputAmount.token.symbol
const outputSymbol = trade.outputAmount.token.symbol
const inputAmount = slippageAdjustedInput.toSignificant(3)
const outputAmount = slippageAdjustedOutput.toSignificant(3)
const inputSymbol = trade.inputAmount.currency.symbol
const outputSymbol = trade.outputAmount.currency.symbol
const inputAmount = trade.inputAmount.toSignificant(3)
const outputAmount = trade.outputAmount.toSignificant(3)
const base = `Swap ${inputAmount} ${inputSymbol} for ${outputAmount} ${outputSymbol}`
const withRecipient =
@ -291,7 +146,7 @@ export function useSwapCallback(
}`
const withVersion =
tradeVersion === Version.v2 ? withRecipient : `${withRecipient} on ${tradeVersion.toUpperCase()}`
tradeVersion === Version.v2 ? withRecipient : `${withRecipient} on ${(tradeVersion as any).toUpperCase()}`
addTransaction(response, {
summary: withVersion
@ -320,7 +175,6 @@ export function useSwapCallback(
tradeVersion,
chainId,
allowedSlippage,
inputAllowance,
v1Exchange,
deadline,
recipientAddressOrName,

@ -0,0 +1,67 @@
import { Currency, currencyEquals, ETHER, WETH } from '@uniswap/sdk'
import { useMemo } from 'react'
import { tryParseAmount } from '../state/swap/hooks'
import { useTransactionAdder } from '../state/transactions/hooks'
import { useCurrencyBalance } from '../state/wallet/hooks'
import { useActiveWeb3React } from './index'
import { useWETHContract } from './useContract'
export enum WrapType {
NOT_APPLICABLE,
WRAP,
UNWRAP
}
const NOT_APPLICABLE = { wrapType: WrapType.NOT_APPLICABLE }
/**
* Given the selected input and output currency, return a wrap callback
* @param inputCurrency the selected input currency
* @param outputCurrency the selected output currency
* @param typedValue the user input value
*/
export default function useWrapCallback(
inputCurrency: Currency | undefined,
outputCurrency: Currency | undefined,
typedValue: string | undefined
): { wrapType: WrapType; execute?: undefined | (() => Promise<void>); error?: string } {
const { chainId, account } = useActiveWeb3React()
const wethContract = useWETHContract()
const balance = useCurrencyBalance(account ?? undefined, inputCurrency)
// we can always parse the amount typed as the input currency, since wrapping is 1:1
const inputAmount = useMemo(() => tryParseAmount(typedValue, inputCurrency), [inputCurrency, typedValue])
const addTransaction = useTransactionAdder()
return useMemo(() => {
if (!wethContract || !chainId || !inputCurrency || !outputCurrency) return NOT_APPLICABLE
const sufficientBalance = inputAmount && balance && !balance.lessThan(inputAmount)
if (inputCurrency === ETHER && currencyEquals(WETH[chainId], outputCurrency)) {
return {
wrapType: WrapType.WRAP,
execute:
sufficientBalance && inputAmount
? async () => {
const txReceipt = await wethContract.deposit({ value: `0x${inputAmount.raw.toString(16)}` })
addTransaction(txReceipt, { summary: `Wrap ${inputAmount.toSignificant(6)} ETH to WETH` })
}
: undefined,
error: sufficientBalance ? undefined : 'Insufficient ETH balance'
}
} else if (currencyEquals(WETH[chainId], inputCurrency) && outputCurrency === ETHER) {
return {
wrapType: WrapType.UNWRAP,
execute:
sufficientBalance && inputAmount
? async () => {
const txReceipt = await wethContract.withdraw(`0x${inputAmount.raw.toString(16)}`)
addTransaction(txReceipt, { summary: `Unwrap ${inputAmount.toSignificant(6)} WETH to ETH` })
}
: undefined,
error: sufficientBalance ? undefined : 'Insufficient WETH balance'
}
} else {
return NOT_APPLICABLE
}
}, [wethContract, chainId, inputCurrency, outputCurrency, inputAmount, balance, addTransaction])
}

@ -1,52 +1,56 @@
import { Fraction, Percent, Token, TokenAmount } from '@uniswap/sdk'
import { Currency, CurrencyAmount, Fraction, Percent } from '@uniswap/sdk'
import React from 'react'
import { Text } from 'rebass'
import { ButtonPrimary } from '../../components/Button'
import { RowBetween, RowFixed } from '../../components/Row'
import TokenLogo from '../../components/TokenLogo'
import CurrencyLogo from '../../components/CurrencyLogo'
import { Field } from '../../state/mint/actions'
import { TYPE } from '../../theme'
export function ConfirmAddModalBottom({
noLiquidity,
price,
tokens,
currencies,
parsedAmounts,
poolTokenPercentage,
onAdd
}: {
noLiquidity?: boolean
price?: Fraction
tokens: { [field in Field]?: Token }
parsedAmounts: { [field in Field]?: TokenAmount }
currencies: { [field in Field]?: Currency }
parsedAmounts: { [field in Field]?: CurrencyAmount }
poolTokenPercentage?: Percent
onAdd: () => void
}) {
return (
<>
<RowBetween>
<TYPE.body>{tokens[Field.TOKEN_A]?.symbol} Deposited</TYPE.body>
<TYPE.body>{currencies[Field.CURRENCY_A]?.symbol} Deposited</TYPE.body>
<RowFixed>
<TokenLogo address={tokens[Field.TOKEN_A]?.address} style={{ marginRight: '8px' }} />
<TYPE.body>{parsedAmounts[Field.TOKEN_A]?.toSignificant(6)}</TYPE.body>
<CurrencyLogo currency={currencies[Field.CURRENCY_A]} style={{ marginRight: '8px' }} />
<TYPE.body>{parsedAmounts[Field.CURRENCY_A]?.toSignificant(6)}</TYPE.body>
</RowFixed>
</RowBetween>
<RowBetween>
<TYPE.body>{tokens[Field.TOKEN_B]?.symbol} Deposited</TYPE.body>
<TYPE.body>{currencies[Field.CURRENCY_B]?.symbol} Deposited</TYPE.body>
<RowFixed>
<TokenLogo address={tokens[Field.TOKEN_B]?.address} style={{ marginRight: '8px' }} />
<TYPE.body>{parsedAmounts[Field.TOKEN_B]?.toSignificant(6)}</TYPE.body>
<CurrencyLogo currency={currencies[Field.CURRENCY_B]} style={{ marginRight: '8px' }} />
<TYPE.body>{parsedAmounts[Field.CURRENCY_B]?.toSignificant(6)}</TYPE.body>
</RowFixed>
</RowBetween>
<RowBetween>
<TYPE.body>Rates</TYPE.body>
<TYPE.body>
{`1 ${tokens[Field.TOKEN_A]?.symbol} = ${price?.toSignificant(4)} ${tokens[Field.TOKEN_B]?.symbol}`}
{`1 ${currencies[Field.CURRENCY_A]?.symbol} = ${price?.toSignificant(4)} ${
currencies[Field.CURRENCY_B]?.symbol
}`}
</TYPE.body>
</RowBetween>
<RowBetween style={{ justifyContent: 'flex-end' }}>
<TYPE.body>
{`1 ${tokens[Field.TOKEN_B]?.symbol} = ${price?.invert().toSignificant(4)} ${tokens[Field.TOKEN_A]?.symbol}`}
{`1 ${currencies[Field.CURRENCY_B]?.symbol} = ${price?.invert().toSignificant(4)} ${
currencies[Field.CURRENCY_A]?.symbol
}`}
</TYPE.body>
</RowBetween>
<RowBetween>

@ -1,4 +1,4 @@
import { Fraction, Percent, Token } from '@uniswap/sdk'
import { Currency, Fraction, Percent } from '@uniswap/sdk'
import React, { useContext } from 'react'
import { Text } from 'rebass'
import { ThemeContext } from 'styled-components'
@ -9,12 +9,12 @@ import { Field } from '../../state/mint/actions'
import { TYPE } from '../../theme'
export const PoolPriceBar = ({
tokens,
currencies,
noLiquidity,
poolTokenPercentage,
price
}: {
tokens: { [field in Field]?: Token }
currencies: { [field in Field]?: Currency }
noLiquidity?: boolean
poolTokenPercentage?: Percent
price?: Fraction
@ -26,13 +26,13 @@ export const PoolPriceBar = ({
<AutoColumn justify="center">
<TYPE.black>{price?.toSignificant(6) ?? '0'}</TYPE.black>
<Text fontWeight={500} fontSize={14} color={theme.text2} pt={1}>
{tokens[Field.TOKEN_B]?.symbol} per {tokens[Field.TOKEN_A]?.symbol}
{currencies[Field.CURRENCY_B]?.symbol} per {currencies[Field.CURRENCY_A]?.symbol}
</Text>
</AutoColumn>
<AutoColumn justify="center">
<TYPE.black>{price?.invert().toSignificant(6) ?? '0'}</TYPE.black>
<Text fontWeight={500} fontSize={14} color={theme.text2} pt={1}>
{tokens[Field.TOKEN_A]?.symbol} per {tokens[Field.TOKEN_B]?.symbol}
{currencies[Field.CURRENCY_A]?.symbol} per {currencies[Field.CURRENCY_B]?.symbol}
</Text>
</AutoColumn>
<AutoColumn justify="center">

@ -1,13 +0,0 @@
import { Token, ChainId, WETH } from '@uniswap/sdk'
export function currencyId(...args: [ChainId | undefined, string] | [Token]): string {
if (args.length === 2) {
const [chainId, tokenAddress] = args
return chainId && tokenAddress === WETH[chainId].address ? 'ETH' : tokenAddress
} else if (args.length === 1) {
const [token] = args
return currencyId(token.chainId, token.address)
} else {
throw new Error('unexpected call signature')
}
}

@ -1,6 +1,6 @@
import { BigNumber } from '@ethersproject/bignumber'
import { TransactionResponse } from '@ethersproject/providers'
import { ChainId, Token, TokenAmount, WETH } from '@uniswap/sdk'
import { Currency, currencyEquals, ETHER, TokenAmount, WETH } from '@uniswap/sdk'
import React, { useCallback, useContext, useState } from 'react'
import { Plus } from 'react-feather'
import ReactGA from 'react-ga'
@ -12,14 +12,15 @@ import { BlueCard, GreyCard, LightCard } from '../../components/Card'
import { AutoColumn, ColumnCenter } from '../../components/Column'
import ConfirmationModal from '../../components/ConfirmationModal'
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
import DoubleLogo from '../../components/DoubleLogo'
import DoubleCurrencyLogo from '../../components/DoubleLogo'
import { AddRemoveTabs } from '../../components/NavigationTabs'
import { MinimalPositionCard } from '../../components/PositionCard'
import Row, { RowBetween, RowFlat } from '../../components/Row'
import { ROUTER_ADDRESS } from '../../constants'
import { PairState } from '../../data/Reserves'
import { useActiveWeb3React } from '../../hooks'
import { useToken } from '../../hooks/Tokens'
import { useCurrency } from '../../hooks/Tokens'
import { ApprovalState, useApproveCallback } from '../../hooks/useApproveCallback'
import { useWalletModalToggle } from '../../state/application/hooks'
import { Field } from '../../state/mint/actions'
@ -30,18 +31,13 @@ import { useIsExpertMode, useUserDeadline, useUserSlippageTolerance } from '../.
import { TYPE } from '../../theme'
import { calculateGasMargin, calculateSlippageAmount, getRouterContract } from '../../utils'
import { maxAmountSpend } from '../../utils/maxAmountSpend'
import { wrappedCurrency } from '../../utils/wrappedCurrency'
import AppBody from '../AppBody'
import { Dots, Wrapper } from '../Pool/styleds'
import { ConfirmAddModalBottom } from './ConfirmAddModalBottom'
import { currencyId } from './currencyId'
import { currencyId } from '../../utils/currencyId'
import { PoolPriceBar } from './PoolPriceBar'
function useTokenByCurrencyId(chainId: ChainId | undefined, currencyId: string | undefined): Token | undefined {
const isETH = currencyId?.toUpperCase() === 'ETH'
const token = useToken(isETH ? undefined : currencyId)
return isETH && chainId ? WETH[chainId] : token ?? undefined
}
export default function AddLiquidity({
match: {
params: { currencyIdA, currencyIdB }
@ -51,11 +47,16 @@ export default function AddLiquidity({
const { account, chainId, library } = useActiveWeb3React()
const theme = useContext(ThemeContext)
const tokenA = useTokenByCurrencyId(chainId, currencyIdA)
const tokenB = useTokenByCurrencyId(chainId, currencyIdB)
const currencyA = useCurrency(currencyIdA)
const currencyB = useCurrency(currencyIdB)
// toggle wallet when disconnected
const toggleWalletModal = useWalletModalToggle()
const oneCurrencyIsWETH = Boolean(
chainId &&
((currencyA && currencyEquals(currencyA, WETH[chainId])) ||
(currencyB && currencyEquals(currencyB, WETH[chainId])))
)
const toggleWalletModal = useWalletModalToggle() // toggle wallet when disconnected
const expertMode = useIsExpertMode()
@ -63,30 +64,18 @@ export default function AddLiquidity({
const { independentField, typedValue, otherTypedValue } = useMintState()
const {
dependentField,
tokens,
currencies,
pair,
tokenBalances,
pairState,
currencyBalances,
parsedAmounts,
price,
noLiquidity,
liquidityMinted,
poolTokenPercentage,
error
} = useDerivedMintInfo(tokenA ?? undefined, tokenB ?? undefined)
const { onUserInput } = useMintActionHandlers(noLiquidity)
const handleTokenAInput = useCallback(
(field: string, value: string) => {
return onUserInput(Field.TOKEN_A, value)
},
[onUserInput]
)
const handleTokenBInput = useCallback(
(field: string, value: string) => {
return onUserInput(Field.TOKEN_B, value)
},
[onUserInput]
)
} = useDerivedMintInfo(currencyA ?? undefined, currencyB ?? undefined)
const { onFieldAInput, onFieldBInput } = useMintActionHandlers(noLiquidity)
const isValid = !error
@ -106,14 +95,17 @@ export default function AddLiquidity({
}
// get the max amounts user can add
const maxAmounts: { [field in Field]?: TokenAmount } = [Field.TOKEN_A, Field.TOKEN_B].reduce((accumulator, field) => {
const maxAmounts: { [field in Field]?: TokenAmount } = [Field.CURRENCY_A, Field.CURRENCY_B].reduce(
(accumulator, field) => {
return {
...accumulator,
[field]: maxAmountSpend(tokenBalances[field])
[field]: maxAmountSpend(currencyBalances[field])
}
}, {})
},
{}
)
const atMaxAmounts: { [field in Field]?: TokenAmount } = [Field.TOKEN_A, Field.TOKEN_B].reduce(
const atMaxAmounts: { [field in Field]?: TokenAmount } = [Field.CURRENCY_A, Field.CURRENCY_B].reduce(
(accumulator, field) => {
return {
...accumulator,
@ -124,8 +116,8 @@ export default function AddLiquidity({
)
// check whether the user has approved the router on the tokens
const [approvalA, approveACallback] = useApproveCallback(parsedAmounts[Field.TOKEN_A], ROUTER_ADDRESS)
const [approvalB, approveBCallback] = useApproveCallback(parsedAmounts[Field.TOKEN_B], ROUTER_ADDRESS)
const [approvalA, approveACallback] = useApproveCallback(parsedAmounts[Field.CURRENCY_A], ROUTER_ADDRESS)
const [approvalB, approveBCallback] = useApproveCallback(parsedAmounts[Field.CURRENCY_B], ROUTER_ADDRESS)
const addTransaction = useTransactionAdder()
@ -133,14 +125,14 @@ export default function AddLiquidity({
if (!chainId || !library || !account) return
const router = getRouterContract(chainId, library, account)
const { [Field.TOKEN_A]: parsedAmountA, [Field.TOKEN_B]: parsedAmountB } = parsedAmounts
if (!parsedAmountA || !parsedAmountB || !tokenA || !tokenB) {
const { [Field.CURRENCY_A]: parsedAmountA, [Field.CURRENCY_B]: parsedAmountB } = parsedAmounts
if (!parsedAmountA || !parsedAmountB || !currencyA || !currencyB) {
return
}
const amountsMin = {
[Field.TOKEN_A]: calculateSlippageAmount(parsedAmountA, noLiquidity ? 0 : allowedSlippage)[0],
[Field.TOKEN_B]: calculateSlippageAmount(parsedAmountB, noLiquidity ? 0 : allowedSlippage)[0]
[Field.CURRENCY_A]: calculateSlippageAmount(parsedAmountA, noLiquidity ? 0 : allowedSlippage)[0],
[Field.CURRENCY_B]: calculateSlippageAmount(parsedAmountB, noLiquidity ? 0 : allowedSlippage)[0]
}
const deadlineFromNow = Math.ceil(Date.now() / 1000) + deadline
@ -149,15 +141,15 @@ export default function AddLiquidity({
method: (...args: any) => Promise<TransactionResponse>,
args: Array<string | string[] | number>,
value: BigNumber | null
if (tokenA.equals(WETH[chainId]) || tokenB.equals(WETH[chainId])) {
const tokenBIsETH = tokenB.equals(WETH[chainId])
if (currencyA === ETHER || currencyB === ETHER) {
const tokenBIsETH = currencyB === ETHER
estimate = router.estimateGas.addLiquidityETH
method = router.addLiquidityETH
args = [
(tokenBIsETH ? tokenA : tokenB).address, // token
wrappedCurrency(tokenBIsETH ? currencyA : currencyB, chainId)?.address ?? '', // token
(tokenBIsETH ? parsedAmountA : parsedAmountB).raw.toString(), // token desired
amountsMin[tokenBIsETH ? Field.TOKEN_A : Field.TOKEN_B].toString(), // token min
amountsMin[tokenBIsETH ? Field.TOKEN_B : Field.TOKEN_A].toString(), // eth min
amountsMin[tokenBIsETH ? Field.CURRENCY_A : Field.CURRENCY_B].toString(), // token min
amountsMin[tokenBIsETH ? Field.CURRENCY_B : Field.CURRENCY_A].toString(), // eth min
account,
deadlineFromNow
]
@ -166,12 +158,12 @@ export default function AddLiquidity({
estimate = router.estimateGas.addLiquidity
method = router.addLiquidity
args = [
tokenA.address,
tokenB.address,
wrappedCurrency(currencyA, chainId)?.address ?? '',
wrappedCurrency(currencyB, chainId)?.address ?? '',
parsedAmountA.raw.toString(),
parsedAmountB.raw.toString(),
amountsMin[Field.TOKEN_A].toString(),
amountsMin[Field.TOKEN_B].toString(),
amountsMin[Field.CURRENCY_A].toString(),
amountsMin[Field.CURRENCY_B].toString(),
account,
deadlineFromNow
]
@ -190,13 +182,13 @@ export default function AddLiquidity({
addTransaction(response, {
summary:
'Add ' +
parsedAmounts[Field.TOKEN_A]?.toSignificant(3) +
parsedAmounts[Field.CURRENCY_A]?.toSignificant(3) +
' ' +
tokens[Field.TOKEN_A]?.symbol +
currencies[Field.CURRENCY_A]?.symbol +
' and ' +
parsedAmounts[Field.TOKEN_B]?.toSignificant(3) +
parsedAmounts[Field.CURRENCY_B]?.toSignificant(3) +
' ' +
tokens[Field.TOKEN_B]?.symbol
currencies[Field.CURRENCY_B]?.symbol
})
setTxHash(response.hash)
@ -204,7 +196,7 @@ export default function AddLiquidity({
ReactGA.event({
category: 'Liquidity',
action: 'Add',
label: [tokens[Field.TOKEN_A]?.symbol, tokens[Field.TOKEN_B]?.symbol].join('/')
label: [currencies[Field.CURRENCY_A]?.symbol, currencies[Field.CURRENCY_B]?.symbol].join('/')
})
})
)
@ -223,9 +215,13 @@ export default function AddLiquidity({
<LightCard mt="20px" borderRadius="20px">
<RowFlat>
<Text fontSize="48px" fontWeight={500} lineHeight="42px" marginRight={10}>
{tokens[Field.TOKEN_A]?.symbol + '/' + tokens[Field.TOKEN_B]?.symbol}
{currencies[Field.CURRENCY_A]?.symbol + '/' + currencies[Field.CURRENCY_B]?.symbol}
</Text>
<DoubleLogo a0={tokens[Field.TOKEN_A]?.address} a1={tokens[Field.TOKEN_B]?.address} size={30} />
<DoubleCurrencyLogo
currency0={currencies[Field.CURRENCY_A]}
currency1={currencies[Field.CURRENCY_B]}
size={30}
/>
</RowFlat>
</LightCard>
</AutoColumn>
@ -235,11 +231,15 @@ export default function AddLiquidity({
<Text fontSize="48px" fontWeight={500} lineHeight="42px" marginRight={10}>
{liquidityMinted?.toSignificant(6)}
</Text>
<DoubleLogo a0={tokens[Field.TOKEN_A]?.address} a1={tokens[Field.TOKEN_B]?.address} size={30} />
<DoubleCurrencyLogo
currency0={currencies[Field.CURRENCY_A]}
currency1={currencies[Field.CURRENCY_B]}
size={30}
/>
</RowFlat>
<Row>
<Text fontSize="24px">
{tokens[Field.TOKEN_A]?.symbol + '/' + tokens[Field.TOKEN_B]?.symbol + ' Pool Tokens'}
{currencies[Field.CURRENCY_A]?.symbol + '/' + currencies[Field.CURRENCY_B]?.symbol + ' Pool Tokens'}
</Text>
</Row>
<TYPE.italic fontSize={12} textAlign="left" padding={'8px 0 0 0 '}>
@ -254,7 +254,7 @@ export default function AddLiquidity({
return (
<ConfirmAddModalBottom
price={price}
tokens={tokens}
currencies={currencies}
parsedAmounts={parsedAmounts}
noLiquidity={noLiquidity}
onAdd={onAdd}
@ -263,37 +263,35 @@ export default function AddLiquidity({
)
}
const pendingText = `Supplying ${parsedAmounts[Field.TOKEN_A]?.toSignificant(6)} ${
tokens[Field.TOKEN_A]?.symbol
} and ${parsedAmounts[Field.TOKEN_B]?.toSignificant(6)} ${tokens[Field.TOKEN_B]?.symbol}`
const pendingText = `Supplying ${parsedAmounts[Field.CURRENCY_A]?.toSignificant(6)} ${
currencies[Field.CURRENCY_A]?.symbol
} and ${parsedAmounts[Field.CURRENCY_B]?.toSignificant(6)} ${currencies[Field.CURRENCY_B]?.symbol}`
const handleTokenASelect = useCallback(
(tokenAddress: string) => {
const [tokenAId, tokenBId] = [
currencyId(chainId, tokenAddress),
tokenB ? currencyId(chainId, tokenB.address) : undefined
]
if (tokenAId === tokenBId) {
history.push(`/add/${tokenAId}/${tokenA ? currencyId(chainId, tokenA.address) : ''}`)
const handleCurrencyASelect = useCallback(
(currencyA: Currency) => {
const newCurrencyIdA = currencyId(currencyA)
if (newCurrencyIdA === currencyIdB) {
history.push(`/add/${currencyIdB}/${currencyIdA}`)
} else {
history.push(`/add/${tokenAId}/${tokenBId}`)
history.push(`/add/${newCurrencyIdA}/${currencyIdB}`)
}
},
[chainId, tokenB, history, tokenA]
[currencyIdB, history, currencyIdA]
)
const handleTokenBSelect = useCallback(
(tokenAddress: string) => {
const [tokenAId, tokenBId] = [
tokenA ? currencyId(chainId, tokenA.address) : undefined,
currencyId(chainId, tokenAddress)
]
if (tokenAId === tokenBId) {
history.push(`/add/${tokenB ? currencyId(chainId, tokenB.address) : ''}/${tokenAId}`)
const handleCurrencyBSelect = useCallback(
(currencyB: Currency) => {
const newCurrencyIdB = currencyId(currencyB)
if (currencyIdA === newCurrencyIdB) {
if (currencyIdB) {
history.push(`/add/${currencyIdB}/${newCurrencyIdB}`)
} else {
history.push(`/add/${currencyIdA ? currencyIdA : 'ETH'}/${currencyId(chainId, tokenAddress)}`)
history.push(`/add/${newCurrencyIdB}`)
}
} else {
history.push(`/add/${currencyIdA ? currencyIdA : 'ETH'}/${newCurrencyIdB}`)
}
},
[tokenA, chainId, history, tokenB, currencyIdA]
[currencyIdA, history, currencyIdB]
)
return (
@ -307,7 +305,7 @@ export default function AddLiquidity({
setShowConfirm(false)
// if there was a tx hash, we want to clear the input
if (txHash) {
onUserInput(Field.TOKEN_A, '')
onFieldAInput('')
}
setTxHash('')
}}
@ -337,16 +335,14 @@ export default function AddLiquidity({
</ColumnCenter>
)}
<CurrencyInputPanel
field={Field.TOKEN_A}
value={formattedAmounts[Field.TOKEN_A]}
onUserInput={handleTokenAInput}
value={formattedAmounts[Field.CURRENCY_A]}
onUserInput={onFieldAInput}
onMax={() => {
onUserInput(Field.TOKEN_A, maxAmounts[Field.TOKEN_A]?.toExact() ?? '')
onFieldAInput(maxAmounts[Field.CURRENCY_A]?.toExact() ?? '')
}}
onTokenSelection={handleTokenASelect}
showMaxButton={!atMaxAmounts[Field.TOKEN_A]}
token={tokens[Field.TOKEN_A]}
pair={pair}
onCurrencySelect={handleCurrencyASelect}
showMaxButton={!atMaxAmounts[Field.CURRENCY_A]}
currency={currencies[Field.CURRENCY_A]}
id="add-liquidity-input-tokena"
showCommonBases
/>
@ -354,20 +350,18 @@ export default function AddLiquidity({
<Plus size="16" color={theme.text2} />
</ColumnCenter>
<CurrencyInputPanel
field={Field.TOKEN_B}
value={formattedAmounts[Field.TOKEN_B]}
onUserInput={handleTokenBInput}
onTokenSelection={handleTokenBSelect}
value={formattedAmounts[Field.CURRENCY_B]}
onUserInput={onFieldBInput}
onCurrencySelect={handleCurrencyBSelect}
onMax={() => {
onUserInput(Field.TOKEN_B, maxAmounts[Field.TOKEN_B]?.toExact() ?? '')
onFieldBInput(maxAmounts[Field.CURRENCY_B]?.toExact() ?? '')
}}
showMaxButton={!atMaxAmounts[Field.TOKEN_B]}
token={tokens[Field.TOKEN_B]}
pair={pair}
showMaxButton={!atMaxAmounts[Field.CURRENCY_B]}
currency={currencies[Field.CURRENCY_B]}
id="add-liquidity-input-tokenb"
showCommonBases
/>
{tokens[Field.TOKEN_A] && tokens[Field.TOKEN_B] && (
{currencies[Field.CURRENCY_A] && currencies[Field.CURRENCY_B] && pairState !== PairState.INVALID && (
<>
<GreyCard padding="0px" borderRadius={'20px'}>
<RowBetween padding="1rem">
@ -377,7 +371,7 @@ export default function AddLiquidity({
</RowBetween>{' '}
<LightCard padding="1rem" borderRadius={'20px'}>
<PoolPriceBar
tokens={tokens}
currencies={currencies}
poolTokenPercentage={poolTokenPercentage}
noLiquidity={noLiquidity}
price={price}
@ -404,9 +398,9 @@ export default function AddLiquidity({
width={approvalB !== ApprovalState.APPROVED ? '48%' : '100%'}
>
{approvalA === ApprovalState.PENDING ? (
<Dots>Approving {tokens[Field.TOKEN_A]?.symbol}</Dots>
<Dots>Approving {currencies[Field.CURRENCY_A]?.symbol}</Dots>
) : (
'Approve ' + tokens[Field.TOKEN_A]?.symbol
'Approve ' + currencies[Field.CURRENCY_A]?.symbol
)}
</ButtonPrimary>
)}
@ -417,9 +411,9 @@ export default function AddLiquidity({
width={approvalA !== ApprovalState.APPROVED ? '48%' : '100%'}
>
{approvalB === ApprovalState.PENDING ? (
<Dots>Approving {tokens[Field.TOKEN_B]?.symbol}</Dots>
<Dots>Approving {currencies[Field.CURRENCY_B]?.symbol}</Dots>
) : (
'Approve ' + tokens[Field.TOKEN_B]?.symbol
'Approve ' + currencies[Field.CURRENCY_B]?.symbol
)}
</ButtonPrimary>
)}
@ -430,7 +424,7 @@ export default function AddLiquidity({
expertMode ? onAdd() : setShowConfirm(true)
}}
disabled={!isValid || approvalA !== ApprovalState.APPROVED || approvalB !== ApprovalState.APPROVED}
error={!isValid && !!parsedAmounts[Field.TOKEN_A] && !!parsedAmounts[Field.TOKEN_B]}
error={!isValid && !!parsedAmounts[Field.CURRENCY_A] && !!parsedAmounts[Field.CURRENCY_B]}
>
<Text fontSize={20} fontWeight={500}>
{error ?? 'Supply'}
@ -442,9 +436,9 @@ export default function AddLiquidity({
</Wrapper>
</AppBody>
{pair && !noLiquidity ? (
{pair && !noLiquidity && pairState !== PairState.INVALID ? (
<AutoColumn style={{ minWidth: '20rem', marginTop: '1rem' }}>
<MinimalPositionCard pair={pair} />
<MinimalPositionCard showUnwrapped={oneCurrencyIsWETH} pair={pair} />
</AutoColumn>
) : null}
</>

@ -1,4 +1,3 @@
import { WETH } from '@uniswap/sdk'
import React from 'react'
import { Redirect, RouteComponentProps } from 'react-router-dom'
import AddLiquidity from './index'
@ -7,13 +6,6 @@ export function RedirectToAddLiquidity() {
return <Redirect to="/add/" />
}
function convertToCurrencyIds(address: string): string {
if (Object.values(WETH).some(weth => weth.address === address)) {
return 'ETH'
}
return address
}
const OLD_PATH_STRUCTURE = /^(0x[a-fA-F0-9]{40})-(0x[a-fA-F0-9]{40})$/
export function RedirectOldAddLiquidityPathStructure(props: RouteComponentProps<{ currencyIdA: string }>) {
const {
@ -23,7 +15,7 @@ export function RedirectOldAddLiquidityPathStructure(props: RouteComponentProps<
} = props
const match = currencyIdA.match(OLD_PATH_STRUCTURE)
if (match?.length) {
return <Redirect to={`/add/${convertToCurrencyIds(match[1])}/${convertToCurrencyIds(match[2])}`} />
return <Redirect to={`/add/${match[1]}/${match[2]}`} />
}
return <AddLiquidity {...props} />

@ -18,6 +18,7 @@ import RemoveV1Exchange from './MigrateV1/RemoveV1Exchange'
import Pool from './Pool'
import PoolFinder from './PoolFinder'
import RemoveLiquidity from './RemoveLiquidity'
import { RedirectOldRemoveLiquidityPathStructure } from './RemoveLiquidity/redirects'
import Swap from './Swap'
import { RedirectPathToSwapOnly, RedirectToSwap } from './Swap/redirects'
@ -79,7 +80,8 @@ export default function App() {
<Route exact path="/add" component={AddLiquidity} />
<Route exact path="/add/:currencyIdA" component={RedirectOldAddLiquidityPathStructure} />
<Route exact path="/add/:currencyIdA/:currencyIdB" component={RedirectDuplicateTokenIds} />
<Route exact strict path="/remove/:tokens" component={RemoveLiquidity} />
<Route exact strict path="/remove/:tokens" component={RedirectOldRemoveLiquidityPathStructure} />
<Route exact strict path="/remove/:currencyIdA/:currencyIdB" component={RemoveLiquidity} />
<Route exact strict path="/migrate/v1" component={MigrateV1} />
<Route exact strict path="/migrate/v1/:address" component={MigrateV1Exchange} />
<Route exact strict path="/remove/v1/:address" component={RemoveV1Exchange} />

@ -1,17 +1,20 @@
import { TransactionResponse } from '@ethersproject/abstract-provider'
import { ChainId, Fraction, JSBI, Percent, Token, TokenAmount, WETH } from '@uniswap/sdk'
import { AddressZero } from '@ethersproject/constants'
import { Currency, Fraction, JSBI, Percent, Token, TokenAmount, WETH } from '@uniswap/sdk'
import React, { useCallback, useMemo, useState } from 'react'
import ReactGA from 'react-ga'
import { Redirect, RouteComponentProps } from 'react-router'
import { Text } from 'rebass'
import { ButtonConfirmed } from '../../components/Button'
import { PinkCard, YellowCard, LightCard } from '../../components/Card'
import { LightCard, PinkCard, YellowCard } from '../../components/Card'
import { AutoColumn } from '../../components/Column'
import CurrencyLogo from '../../components/CurrencyLogo'
import QuestionHelper from '../../components/QuestionHelper'
import { AutoRow, RowBetween, RowFixed } from '../../components/Row'
import { Dots } from '../../components/swap/styleds'
import { DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE } from '../../constants'
import { MIGRATOR_ADDRESS } from '../../constants/abis/migrator'
import { usePair } from '../../data/Reserves'
import { PairState, usePair } from '../../data/Reserves'
import { useTotalSupply } from '../../data/TotalSupply'
import { useActiveWeb3React } from '../../hooks'
import { useToken } from '../../hooks/Tokens'
@ -20,13 +23,10 @@ import { useV1ExchangeContract, useV2MigratorContract } from '../../hooks/useCon
import { NEVER_RELOAD, useSingleCallResult } from '../../state/multicall/hooks'
import { useIsTransactionPending, useTransactionAdder } from '../../state/transactions/hooks'
import { useETHBalances, useTokenBalance } from '../../state/wallet/hooks'
import { TYPE, ExternalLink, BackArrow } from '../../theme'
import { isAddress, getEtherscanLink } from '../../utils'
import { BackArrow, ExternalLink, TYPE } from '../../theme'
import { getEtherscanLink, isAddress } from '../../utils'
import { BodyWrapper } from '../AppBody'
import { EmptyState } from './EmptyState'
import TokenLogo from '../../components/TokenLogo'
import { AddressZero } from '@ethersproject/constants'
import { Text } from 'rebass'
const POOL_TOKEN_AMOUNT_MIN = new Fraction(JSBI.BigInt(1), JSBI.BigInt(1000000))
const WEI_DENOM = JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(18))
@ -63,7 +63,7 @@ export function V1LiquidityInfo({
return (
<>
<AutoRow style={{ justifyContent: 'flex-start', width: 'fit-content' }}>
<TokenLogo size="24px" address={token.address} />
<CurrencyLogo size="24px" currency={token} />
<div style={{ marginLeft: '.75rem' }}>
<TYPE.mediumHeader>
{<FormattedPoolTokenAmount tokenAmount={liquidityTokenAmount} />}{' '}
@ -80,7 +80,7 @@ export function V1LiquidityInfo({
<Text fontSize={16} fontWeight={500} marginLeft={'6px'}>
{tokenWorth.toSignificant(4)}
</Text>
<TokenLogo size="20px" style={{ marginLeft: '8px' }} address={token.address} />
<CurrencyLogo size="20px" style={{ marginLeft: '8px' }} currency={token} />
</RowFixed>
</RowBetween>
<RowBetween mb="1rem">
@ -91,7 +91,7 @@ export function V1LiquidityInfo({
<Text fontSize={16} fontWeight={500} marginLeft={'6px'}>
{ethWorth.toSignificant(4)}
</Text>
<TokenLogo size="20px" style={{ marginLeft: '8px' }} address={WETH[chainId].address} />
<CurrencyLogo size="20px" style={{ marginLeft: '8px' }} currency={Currency.ETHER} />
</RowFixed>
</RowBetween>
</>
@ -104,10 +104,10 @@ function V1PairMigration({ liquidityTokenAmount, token }: { liquidityTokenAmount
const exchangeETHBalance = useETHBalances([liquidityTokenAmount.token.address])?.[liquidityTokenAmount.token.address]
const exchangeTokenBalance = useTokenBalance(liquidityTokenAmount.token.address, token)
const v2Pair = usePair(WETH[chainId as ChainId], token)
const isFirstLiquidityProvider: boolean = v2Pair === null
const [v2PairState, v2Pair] = usePair(chainId ? WETH[chainId] : undefined, token)
const isFirstLiquidityProvider: boolean = v2PairState === PairState.NOT_EXISTS
const v2SpotPrice = v2Pair?.reserveOf(token)?.divide(v2Pair?.reserveOf(WETH[chainId as ChainId]))
const v2SpotPrice = v2Pair?.reserveOf(token)?.divide(v2Pair?.reserveOf(WETH[chainId]))
const [confirmingMigration, setConfirmingMigration] = useState<boolean>(false)
const [pendingMigrationHash, setPendingMigrationHash] = useState<string | null>(null)
@ -126,7 +126,7 @@ function V1PairMigration({ liquidityTokenAmount, token }: { liquidityTokenAmount
const v1SpotPrice =
exchangeTokenBalance && exchangeETHBalance
? exchangeTokenBalance.divide(new Fraction(exchangeETHBalance, WEI_DENOM))
? exchangeTokenBalance.divide(new Fraction(exchangeETHBalance.raw, WEI_DENOM))
: null
const priceDifferenceFraction: Fraction | undefined =

@ -1,4 +1,4 @@
import React, { useContext } from 'react'
import React, { useContext, useMemo } from 'react'
import { ThemeContext } from 'styled-components'
import { Pair } from '@uniswap/sdk'
import { Link } from 'react-router-dom'
@ -17,7 +17,7 @@ import { AutoColumn } from '../../components/Column'
import { useActiveWeb3React } from '../../hooks'
import { usePairs } from '../../data/Reserves'
import { useAllDummyPairs } from '../../state/user/hooks'
import { toV2LiquidityToken, useTrackedTokenPairs } from '../../state/user/hooks'
import AppBody from '../AppBody'
import { Dots } from '../../components/swap/styleds'
@ -26,27 +26,33 @@ export default function Pool() {
const { account } = useActiveWeb3React()
// fetch the user's balances of all tracked V2 LP tokens
const v2DummyPairs = useAllDummyPairs()
const trackedTokenPairs = useTrackedTokenPairs()
const tokenPairsWithLiquidityTokens = useMemo(
() => trackedTokenPairs.map(tokens => ({ liquidityToken: toV2LiquidityToken(tokens), tokens })),
[trackedTokenPairs]
)
const liquidityTokens = useMemo(() => tokenPairsWithLiquidityTokens.map(tpwlt => tpwlt.liquidityToken), [
tokenPairsWithLiquidityTokens
])
const [v2PairsBalances, fetchingV2PairBalances] = useTokenBalancesWithLoadingIndicator(
account ?? undefined,
v2DummyPairs?.map(p => p.liquidityToken)
liquidityTokens
)
// fetch the reserves for all V2 pools in which the user has a balance
const v2DummyPairsWithABalance = v2DummyPairs.filter(dummyPair =>
v2PairsBalances[dummyPair.liquidityToken.address]?.greaterThan('0')
)
const v2Pairs = usePairs(
v2DummyPairsWithABalance.map(V2DummyPairWithABalance => [
V2DummyPairWithABalance.token0,
V2DummyPairWithABalance.token1
])
)
const v2IsLoading =
fetchingV2PairBalances || v2Pairs?.length < v2DummyPairsWithABalance.length || v2Pairs?.some(V2Pair => !V2Pair)
const allV2PairsWithLiquidity = v2Pairs
.filter((v2Pair): v2Pair is Pair => Boolean(v2Pair))
.map(V2Pair => <FullPositionCard key={V2Pair.liquidityToken.address} pair={V2Pair} />)
// fetch the reserves for all V2 pools in which the user has a balance
const liquidityTokensWithBalances = useMemo(
() =>
tokenPairsWithLiquidityTokens.filter(({ liquidityToken }) =>
v2PairsBalances[liquidityToken.address]?.greaterThan('0')
),
[tokenPairsWithLiquidityTokens, v2PairsBalances]
)
const v2Pairs = usePairs(liquidityTokensWithBalances.map(({ tokens }) => tokens))
const v2IsLoading =
fetchingV2PairBalances || v2Pairs?.length < liquidityTokensWithBalances.length || v2Pairs?.some(V2Pair => !V2Pair)
const allV2PairsWithLiquidity = v2Pairs.map(([, pair]) => pair).filter((v2Pair): v2Pair is Pair => Boolean(v2Pair))
const hasV1Liquidity = useUserHasLiquidityInAllTokens()
@ -82,7 +88,11 @@ export default function Pool() {
</TYPE.body>
</LightCard>
) : allV2PairsWithLiquidity?.length > 0 ? (
<>{allV2PairsWithLiquidity}</>
<>
{allV2PairsWithLiquidity.map(v2Pair => (
<FullPositionCard key={v2Pair.liquidityToken.address} pair={v2Pair} />
))}
</>
) : (
<LightCard padding="40px">
<TYPE.body color={theme.text3} textAlign="center">

@ -1,22 +1,23 @@
import { JSBI, Pair, Token, TokenAmount, WETH } from '@uniswap/sdk'
import { Currency, ETHER, JSBI, TokenAmount } from '@uniswap/sdk'
import React, { useCallback, useEffect, useState } from 'react'
import { Plus } from 'react-feather'
import { Text } from 'rebass'
import { ButtonDropdownLight } from '../../components/Button'
import { LightCard } from '../../components/Card'
import { AutoColumn, ColumnCenter } from '../../components/Column'
import CurrencyLogo from '../../components/CurrencyLogo'
import { FindPoolTabs } from '../../components/NavigationTabs'
import { MinimalPositionCard } from '../../components/PositionCard'
import Row from '../../components/Row'
import TokenSearchModal from '../../components/SearchModal/TokenSearchModal'
import TokenLogo from '../../components/TokenLogo'
import { usePair } from '../../data/Reserves'
import CurrencySearchModal from '../../components/SearchModal/CurrencySearchModal'
import { PairState, usePair } from '../../data/Reserves'
import { useActiveWeb3React } from '../../hooks'
import { useToken } from '../../hooks/Tokens'
import { usePairAdder } from '../../state/user/hooks'
import { useTokenBalanceTreatingWETHasETH } from '../../state/wallet/hooks'
import { useTokenBalance } from '../../state/wallet/hooks'
import { StyledInternalLink } from '../../theme'
import { currencyId } from '../../utils/currencyId'
import AppBody from '../AppBody'
import { Dots } from '../Pool/styleds'
enum Fields {
TOKEN0 = 0,
@ -24,17 +25,15 @@ enum Fields {
}
export default function PoolFinder() {
const { account, chainId } = useActiveWeb3React()
const { account } = useActiveWeb3React()
const [showSearch, setShowSearch] = useState<boolean>(false)
const [activeField, setActiveField] = useState<number>(Fields.TOKEN1)
const [token0Address, setToken0Address] = useState<string>(chainId ? WETH[chainId].address : '')
const [token1Address, setToken1Address] = useState<string>()
const token0: Token | null | undefined = useToken(token0Address)
const token1: Token | null | undefined = useToken(token1Address)
const [currency0, setCurrency0] = useState<Currency | null>(ETHER)
const [currency1, setCurrency1] = useState<Currency | null>(null)
const pair: Pair | null | undefined = usePair(token0 ?? undefined, token1 ?? undefined)
const [pairState, pair] = usePair(currency0 ?? undefined, currency1 ?? undefined)
const addPair = usePairAdder()
useEffect(() => {
if (pair) {
@ -42,16 +41,25 @@ export default function PoolFinder() {
}
}, [pair, addPair])
const newPair: boolean =
pair === null ||
(!!pair && JSBI.equal(pair.reserve0.raw, JSBI.BigInt(0)) && JSBI.equal(pair.reserve1.raw, JSBI.BigInt(0)))
const validPairNoLiquidity: boolean =
pairState === PairState.NOT_EXISTS ||
Boolean(
pairState === PairState.EXISTS &&
pair &&
JSBI.equal(pair.reserve0.raw, JSBI.BigInt(0)) &&
JSBI.equal(pair.reserve1.raw, JSBI.BigInt(0))
)
const position: TokenAmount | undefined = useTokenBalanceTreatingWETHasETH(account ?? undefined, pair?.liquidityToken)
const poolImported: boolean = !!position && JSBI.greaterThan(position.raw, JSBI.BigInt(0))
const position: TokenAmount | undefined = useTokenBalance(account ?? undefined, pair?.liquidityToken)
const hasPosition = Boolean(position && JSBI.greaterThan(position.raw, JSBI.BigInt(0)))
const handleTokenSelect = useCallback(
(address: string) => {
activeField === Fields.TOKEN0 ? setToken0Address(address) : setToken1Address(address)
const handleCurrencySelect = useCallback(
(currency: Currency) => {
if (activeField === Fields.TOKEN0) {
setCurrency0(currency)
} else {
setCurrency1(currency)
}
},
[activeField]
)
@ -60,6 +68,14 @@ export default function PoolFinder() {
setShowSearch(false)
}, [setShowSearch])
const prerequisiteMessage = (
<LightCard padding="45px 10px">
<Text textAlign="center">
{!account ? 'Connect to a wallet to find pools' : 'Select a token to find your liquidity.'}
</Text>
</LightCard>
)
return (
<AppBody>
<FindPoolTabs />
@ -70,11 +86,11 @@ export default function PoolFinder() {
setActiveField(Fields.TOKEN0)
}}
>
{token0 ? (
{currency0 ? (
<Row>
<TokenLogo address={token0Address} />
<CurrencyLogo currency={currency0} />
<Text fontWeight={500} fontSize={20} marginLeft={'12px'}>
{token0.symbol}
{currency0.symbol}
</Text>
</Row>
) : (
@ -94,11 +110,11 @@ export default function PoolFinder() {
setActiveField(Fields.TOKEN1)
}}
>
{token1 ? (
{currency1 ? (
<Row>
<TokenLogo address={token1Address} />
<CurrencyLogo currency={currency1} />
<Text fontWeight={500} fontSize={20} marginLeft={'12px'}>
{token1.symbol}
{currency1.symbol}
</Text>
</Row>
) : (
@ -108,51 +124,68 @@ export default function PoolFinder() {
)}
</ButtonDropdownLight>
{poolImported && (
{hasPosition && (
<ColumnCenter
style={{ justifyItems: 'center', backgroundColor: '', padding: '12px 0px', borderRadius: '12px' }}
>
<Text textAlign="center" fontWeight={500} color="">
<Text textAlign="center" fontWeight={500}>
Pool Found!
</Text>
</ColumnCenter>
)}
{position ? (
poolImported ? (
{currency0 && currency1 ? (
pairState === PairState.EXISTS ? (
hasPosition && pair ? (
<MinimalPositionCard pair={pair} border="1px solid #CED0D9" />
) : (
<LightCard padding="45px 10px">
<AutoColumn gap="sm" justify="center">
<Text textAlign="center">You dont have liquidity in this pool yet.</Text>
<StyledInternalLink to={`/add/${token0?.address}/${token1?.address}`}>
<StyledInternalLink to={`/add/${currencyId(currency0)}/${currencyId(currency1)}`}>
<Text textAlign="center">Add liquidity.</Text>
</StyledInternalLink>
</AutoColumn>
</LightCard>
)
) : newPair ? (
) : validPairNoLiquidity ? (
<LightCard padding="45px 10px">
<AutoColumn gap="sm" justify="center">
<Text textAlign="center">No pool found.</Text>
<StyledInternalLink to={`/add/${token0Address}/${token1Address}`}>Create pool?</StyledInternalLink>
<StyledInternalLink to={`/add/${currencyId(currency0)}/${currencyId(currency1)}`}>
Create pool.
</StyledInternalLink>
</AutoColumn>
</LightCard>
) : (
) : pairState === PairState.INVALID ? (
<LightCard padding="45px 10px">
<Text textAlign="center">
{!account ? 'Connect to a wallet to find pools' : 'Select a token to find your liquidity.'}
<AutoColumn gap="sm" justify="center">
<Text textAlign="center" fontWeight={500}>
Invalid pair.
</Text>
</AutoColumn>
</LightCard>
) : pairState === PairState.LOADING ? (
<LightCard padding="45px 10px">
<AutoColumn gap="sm" justify="center">
<Text textAlign="center">
Loading
<Dots />
</Text>
</AutoColumn>
</LightCard>
) : null
) : (
prerequisiteMessage
)}
</AutoColumn>
<TokenSearchModal
<CurrencySearchModal
isOpen={showSearch}
onTokenSelect={handleTokenSelect}
onCurrencySelect={handleCurrencySelect}
onDismiss={handleSearchDismiss}
showCommonBases
hiddenToken={activeField === Fields.TOKEN0 ? token1Address : token0Address}
hiddenCurrency={(activeField === Fields.TOKEN0 ? currency1 : currency0) ?? undefined}
/>
</AppBody>
)

@ -1,7 +1,8 @@
import { splitSignature } from '@ethersproject/bytes'
import { Contract } from '@ethersproject/contracts'
import { Percent, WETH } from '@uniswap/sdk'
import React, { useCallback, useContext, useState } from 'react'
import { TransactionResponse } from '@ethersproject/providers'
import { Currency, currencyEquals, ETHER, Percent, WETH } from '@uniswap/sdk'
import React, { useCallback, useContext, useMemo, useState } from 'react'
import { ArrowDown, Plus } from 'react-feather'
import ReactGA from 'react-ga'
import { RouteComponentProps } from 'react-router'
@ -12,35 +13,48 @@ import { LightCard } from '../../components/Card'
import { AutoColumn, ColumnCenter } from '../../components/Column'
import ConfirmationModal from '../../components/ConfirmationModal'
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
import DoubleLogo from '../../components/DoubleLogo'
import DoubleCurrencyLogo from '../../components/DoubleLogo'
import { AddRemoveTabs } from '../../components/NavigationTabs'
import { MinimalPositionCard } from '../../components/PositionCard'
import Row, { RowBetween, RowFixed } from '../../components/Row'
import Slider from '../../components/Slider'
import TokenLogo from '../../components/TokenLogo'
import CurrencyLogo from '../../components/CurrencyLogo'
import { ROUTER_ADDRESS } from '../../constants'
import { useActiveWeb3React } from '../../hooks'
import { useCurrency } from '../../hooks/Tokens'
import { usePairContract } from '../../hooks/useContract'
import { useTransactionAdder } from '../../state/transactions/hooks'
import { TYPE } from '../../theme'
import { StyledInternalLink, TYPE } from '../../theme'
import { calculateGasMargin, calculateSlippageAmount, getRouterContract } from '../../utils'
import { currencyId } from '../../utils/currencyId'
import { wrappedCurrency } from '../../utils/wrappedCurrency'
import AppBody from '../AppBody'
import { ClickableText, MaxButton, Wrapper } from '../Pool/styleds'
import { useApproveCallback, ApprovalState } from '../../hooks/useApproveCallback'
import { Dots } from '../../components/swap/styleds'
import { useDefaultsFromURLMatchParams, useBurnActionHandlers } from '../../state/burn/hooks'
import { useBurnActionHandlers } from '../../state/burn/hooks'
import { useDerivedBurnInfo, useBurnState } from '../../state/burn/hooks'
import { Field } from '../../state/burn/actions'
import { useWalletModalToggle } from '../../state/application/hooks'
import { useUserDeadline, useUserSlippageTolerance } from '../../state/user/hooks'
import { BigNumber } from '@ethersproject/bignumber'
export default function RemoveLiquidity({ match: { params } }: RouteComponentProps<{ tokens: string }>) {
useDefaultsFromURLMatchParams(params)
export default function RemoveLiquidity({
history,
match: {
params: { currencyIdA, currencyIdB }
}
}: RouteComponentProps<{ currencyIdA: string; currencyIdB: string }>) {
const [currencyA, currencyB] = [useCurrency(currencyIdA) ?? undefined, useCurrency(currencyIdB) ?? undefined]
const { account, chainId, library } = useActiveWeb3React()
const [tokenA, tokenB] = useMemo(() => [wrappedCurrency(currencyA, chainId), wrappedCurrency(currencyB, chainId)], [
currencyA,
currencyB,
chainId
])
const theme = useContext(ThemeContext)
// toggle wallet when disconnected
@ -48,7 +62,7 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
// burn state
const { independentField, typedValue } = useBurnState()
const { tokens, pair, route, parsedAmounts, error } = useDerivedBurnInfo()
const { pair, parsedAmounts, error } = useDerivedBurnInfo(currencyA ?? undefined, currencyB ?? undefined)
const { onUserInput: _onUserInput } = useBurnActionHandlers()
const isValid = !error
@ -70,23 +84,27 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
: parsedAmounts[Field.LIQUIDITY_PERCENT].toFixed(0),
[Field.LIQUIDITY]:
independentField === Field.LIQUIDITY ? typedValue : parsedAmounts[Field.LIQUIDITY]?.toSignificant(6) ?? '',
[Field.TOKEN_A]:
independentField === Field.TOKEN_A ? typedValue : parsedAmounts[Field.TOKEN_A]?.toSignificant(6) ?? '',
[Field.TOKEN_B]:
independentField === Field.TOKEN_B ? typedValue : parsedAmounts[Field.TOKEN_B]?.toSignificant(6) ?? ''
[Field.CURRENCY_A]:
independentField === Field.CURRENCY_A ? typedValue : parsedAmounts[Field.CURRENCY_A]?.toSignificant(6) ?? '',
[Field.CURRENCY_B]:
independentField === Field.CURRENCY_B ? typedValue : parsedAmounts[Field.CURRENCY_B]?.toSignificant(6) ?? ''
}
const atMaxAmount = parsedAmounts[Field.LIQUIDITY_PERCENT]?.equalTo(new Percent('1'))
// pair contract
const pairContract: Contract = usePairContract(pair?.liquidityToken?.address)
const pairContract: Contract | null = usePairContract(pair?.liquidityToken?.address)
// allowance handling
const [signatureData, setSignatureData] = useState<{ v: number; r: string; s: string; deadline: number }>(null)
const [signatureData, setSignatureData] = useState<{ v: number; r: string; s: string; deadline: number } | null>(null)
const [approval, approveCallback] = useApproveCallback(parsedAmounts[Field.LIQUIDITY], ROUTER_ADDRESS)
async function onAttemptToApprove() {
if (!pairContract || !pair || !library) throw new Error('missing dependencies')
const liquidityAmount = parsedAmounts[Field.LIQUIDITY]
if (!liquidityAmount) throw new Error('missing liquidity amount')
// try to gather a signature for permission
const nonce = await pairContract.nonces(account)
const deadlineForSignature: number = Math.ceil(Date.now() / 1000) + deadline
const EIP712Domain = [
@ -111,7 +129,7 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
const message = {
owner: account,
spender: ROUTER_ADDRESS,
value: parsedAmounts[Field.LIQUIDITY].raw.toString(),
value: liquidityAmount.raw.toString(),
nonce: nonce.toHexString(),
deadline: deadlineForSignature
}
@ -153,32 +171,52 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
[_onUserInput]
)
const onLiquidityInput = useCallback((typedValue: string): void => onUserInput(Field.LIQUIDITY, typedValue), [
onUserInput
])
const onCurrencyAInput = useCallback((typedValue: string): void => onUserInput(Field.CURRENCY_A, typedValue), [
onUserInput
])
const onCurrencyBInput = useCallback((typedValue: string): void => onUserInput(Field.CURRENCY_B, typedValue), [
onUserInput
])
// tx sending
const addTransaction = useTransactionAdder()
async function onRemove() {
if (!chainId || !library || !account) throw new Error('missing dependencies')
const { [Field.CURRENCY_A]: currencyAmountA, [Field.CURRENCY_B]: currencyAmountB } = parsedAmounts
if (!currencyAmountA || !currencyAmountB) {
throw new Error('missing currency amounts')
}
const router = getRouterContract(chainId, library, account)
const amountsMin = {
[Field.TOKEN_A]: calculateSlippageAmount(parsedAmounts[Field.TOKEN_A], allowedSlippage)[0],
[Field.TOKEN_B]: calculateSlippageAmount(parsedAmounts[Field.TOKEN_B], allowedSlippage)[0]
[Field.CURRENCY_A]: calculateSlippageAmount(currencyAmountA, allowedSlippage)[0],
[Field.CURRENCY_B]: calculateSlippageAmount(currencyAmountB, allowedSlippage)[0]
}
const tokenBIsETH = tokens[Field.TOKEN_B].equals(WETH[chainId])
const oneTokenIsETH = tokens[Field.TOKEN_A].equals(WETH[chainId]) || tokenBIsETH
if (!currencyA || !currencyB) throw new Error('missing tokens')
const liquidityAmount = parsedAmounts[Field.LIQUIDITY]
if (!liquidityAmount) throw new Error('missing liquidity amount')
const currencyBIsETH = currencyB === ETHER
const oneCurrencyIsETH = currencyA === ETHER || currencyBIsETH
const deadlineFromNow = Math.ceil(Date.now() / 1000) + deadline
if (!tokenA || !tokenB) throw new Error('could not wrap')
let methodNames: string[], args: Array<string | string[] | number | boolean>
// we have approval, use normal remove liquidity
if (approval === ApprovalState.APPROVED) {
// removeLiquidityETH
if (oneTokenIsETH) {
if (oneCurrencyIsETH) {
methodNames = ['removeLiquidityETH', 'removeLiquidityETHSupportingFeeOnTransferTokens']
args = [
tokens[tokenBIsETH ? Field.TOKEN_A : Field.TOKEN_B].address,
parsedAmounts[Field.LIQUIDITY].raw.toString(),
amountsMin[tokenBIsETH ? Field.TOKEN_A : Field.TOKEN_B].toString(),
amountsMin[tokenBIsETH ? Field.TOKEN_B : Field.TOKEN_A].toString(),
currencyBIsETH ? tokenA.address : tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_A : Field.CURRENCY_B].toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_B : Field.CURRENCY_A].toString(),
account,
deadlineFromNow
]
@ -187,11 +225,11 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
else {
methodNames = ['removeLiquidity']
args = [
tokens[Field.TOKEN_A].address,
tokens[Field.TOKEN_B].address,
parsedAmounts[Field.LIQUIDITY].raw.toString(),
amountsMin[Field.TOKEN_A].toString(),
amountsMin[Field.TOKEN_B].toString(),
tokenA.address,
tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[Field.CURRENCY_A].toString(),
amountsMin[Field.CURRENCY_B].toString(),
account,
deadlineFromNow
]
@ -200,13 +238,13 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
// we have a signataure, use permit versions of remove liquidity
else if (signatureData !== null) {
// removeLiquidityETHWithPermit
if (oneTokenIsETH) {
if (oneCurrencyIsETH) {
methodNames = ['removeLiquidityETHWithPermit', 'removeLiquidityETHWithPermitSupportingFeeOnTransferTokens']
args = [
tokens[tokenBIsETH ? Field.TOKEN_A : Field.TOKEN_B].address,
parsedAmounts[Field.LIQUIDITY].raw.toString(),
amountsMin[tokenBIsETH ? Field.TOKEN_A : Field.TOKEN_B].toString(),
amountsMin[tokenBIsETH ? Field.TOKEN_B : Field.TOKEN_A].toString(),
currencyBIsETH ? tokenA.address : tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_A : Field.CURRENCY_B].toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_B : Field.CURRENCY_A].toString(),
account,
signatureData.deadline,
false,
@ -219,11 +257,11 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
else {
methodNames = ['removeLiquidityWithPermit']
args = [
tokens[Field.TOKEN_A].address,
tokens[Field.TOKEN_B].address,
parsedAmounts[Field.LIQUIDITY].raw.toString(),
amountsMin[Field.TOKEN_A].toString(),
amountsMin[Field.TOKEN_B].toString(),
tokenA.address,
tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[Field.CURRENCY_A].toString(),
amountsMin[Field.CURRENCY_B].toString(),
account,
signatureData.deadline,
false,
@ -233,7 +271,7 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
]
}
} else {
console.error('Attempting to confirm without approval or a signature. Please contact support.')
throw new Error('Attempting to confirm without approval or a signature. Please contact support.')
}
const safeGasEstimates = await Promise.all(
@ -261,19 +299,19 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
await router[methodName](...args, {
gasLimit: safeGasEstimate
})
.then(response => {
.then((response: TransactionResponse) => {
setAttemptingTxn(false)
addTransaction(response, {
summary:
'Remove ' +
parsedAmounts[Field.TOKEN_A]?.toSignificant(3) +
parsedAmounts[Field.CURRENCY_A]?.toSignificant(3) +
' ' +
tokens[Field.TOKEN_A]?.symbol +
currencyA?.symbol +
' and ' +
parsedAmounts[Field.TOKEN_B]?.toSignificant(3) +
parsedAmounts[Field.CURRENCY_B]?.toSignificant(3) +
' ' +
tokens[Field.TOKEN_B]?.symbol
currencyB?.symbol
})
setTxHash(response.hash)
@ -281,15 +319,13 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
ReactGA.event({
category: 'Liquidity',
action: 'Remove',
label: [tokens[Field.TOKEN_A]?.symbol, tokens[Field.TOKEN_B]?.symbol].join('/')
label: [currencyA?.symbol, currencyB?.symbol].join('/')
})
})
.catch(error => {
.catch((error: Error) => {
setAttemptingTxn(false)
// we only care if the error is something _other_ than the user rejected the tx
if (error?.code !== 4001) {
console.error(error)
}
})
}
}
@ -299,12 +335,12 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
<AutoColumn gap={'md'} style={{ marginTop: '20px' }}>
<RowBetween align="flex-end">
<Text fontSize={24} fontWeight={500}>
{parsedAmounts[Field.TOKEN_A]?.toSignificant(6)}
{parsedAmounts[Field.CURRENCY_A]?.toSignificant(6)}
</Text>
<RowFixed gap="4px">
<TokenLogo address={tokens[Field.TOKEN_A]?.address} size={'24px'} />
<CurrencyLogo currency={currencyA} size={'24px'} />
<Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}>
{tokens[Field.TOKEN_A]?.symbol}
{currencyA?.symbol}
</Text>
</RowFixed>
</RowBetween>
@ -313,12 +349,12 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
</RowFixed>
<RowBetween align="flex-end">
<Text fontSize={24} fontWeight={500}>
{parsedAmounts[Field.TOKEN_B]?.toSignificant(6)}
{parsedAmounts[Field.CURRENCY_B]?.toSignificant(6)}
</Text>
<RowFixed gap="4px">
<TokenLogo address={tokens[Field.TOKEN_B]?.address} size={'24px'} />
<CurrencyLogo currency={currencyB} size={'24px'} />
<Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}>
{tokens[Field.TOKEN_B]?.symbol}
{currencyB?.symbol}
</Text>
</RowFixed>
</RowBetween>
@ -336,34 +372,29 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
<>
<RowBetween>
<Text color={theme.text2} fontWeight={500} fontSize={16}>
{'UNI ' + tokens[Field.TOKEN_A]?.symbol + '/' + tokens[Field.TOKEN_B]?.symbol} Burned
{'UNI ' + currencyA?.symbol + '/' + currencyB?.symbol} Burned
</Text>
<RowFixed>
<DoubleLogo
a0={tokens[Field.TOKEN_A]?.address || ''}
a1={tokens[Field.TOKEN_B]?.address || ''}
margin={true}
/>
<DoubleCurrencyLogo currency0={currencyA} currency1={currencyB} margin={true} />
<Text fontWeight={500} fontSize={16}>
{parsedAmounts[Field.LIQUIDITY]?.toSignificant(6)}
</Text>
</RowFixed>
</RowBetween>
{route && (
{pair && (
<>
<RowBetween>
<Text color={theme.text2} fontWeight={500} fontSize={16}>
Price
</Text>
<Text fontWeight={500} fontSize={16} color={theme.text1}>
1 {tokens[Field.TOKEN_A]?.symbol} = {route.midPrice.toSignificant(6)} {tokens[Field.TOKEN_B]?.symbol}
1 {currencyA?.symbol} = {tokenA ? pair.priceOf(tokenA).toSignificant(6) : '-'} {currencyB?.symbol}
</Text>
</RowBetween>
<RowBetween>
<div />
<Text fontWeight={500} fontSize={16} color={theme.text1}>
1 {tokens[Field.TOKEN_B]?.symbol} = {route.midPrice.invert().toSignificant(6)}{' '}
{tokens[Field.TOKEN_A]?.symbol}
1 {currencyB?.symbol} = {tokenB ? pair.priceOf(tokenB).toSignificant(6) : '-'} {currencyA?.symbol}
</Text>
</RowBetween>
</>
@ -377,9 +408,9 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
)
}
const pendingText = `Removing ${parsedAmounts[Field.TOKEN_A]?.toSignificant(6)} ${
tokens[Field.TOKEN_A]?.symbol
} and ${parsedAmounts[Field.TOKEN_B]?.toSignificant(6)} ${tokens[Field.TOKEN_B]?.symbol}`
const pendingText = `Removing ${parsedAmounts[Field.CURRENCY_A]?.toSignificant(6)} ${
currencyA?.symbol
} and ${parsedAmounts[Field.CURRENCY_B]?.toSignificant(6)} ${currencyB?.symbol}`
const liquidityPercentChangeCallback = useCallback(
(value: number) => {
@ -388,6 +419,34 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
[onUserInput]
)
const oneCurrencyIsETH = currencyA === ETHER || currencyB === ETHER
const oneCurrencyIsWETH = Boolean(
chainId &&
((currencyA && currencyEquals(WETH[chainId], currencyA)) ||
(currencyB && currencyEquals(WETH[chainId], currencyB)))
)
const handleSelectCurrencyA = useCallback(
(currency: Currency) => {
if (currencyIdB && currencyId(currency) === currencyIdB) {
history.push(`/remove/${currencyId(currency)}/${currencyIdA}`)
} else {
history.push(`/remove/${currencyId(currency)}/${currencyIdB}`)
}
},
[currencyIdA, currencyIdB, history]
)
const handleSelectCurrencyB = useCallback(
(currency: Currency) => {
if (currencyIdA && currencyId(currency) === currencyIdA) {
history.push(`/remove/${currencyIdB}/${currencyId(currency)}`)
} else {
history.push(`/remove/${currencyIdA}/${currencyId(currency)}`)
}
},
[currencyIdA, currencyIdB, history]
)
return (
<>
<AppBody>
@ -463,26 +522,47 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
<AutoColumn gap="10px">
<RowBetween>
<Text fontSize={24} fontWeight={500}>
{formattedAmounts[Field.TOKEN_A] || '-'}
{formattedAmounts[Field.CURRENCY_A] || '-'}
</Text>
<RowFixed>
<TokenLogo address={tokens[Field.TOKEN_A]?.address} style={{ marginRight: '12px' }} />
<CurrencyLogo currency={currencyA} style={{ marginRight: '12px' }} />
<Text fontSize={24} fontWeight={500} id="remove-liquidity-tokena-symbol">
{tokens[Field.TOKEN_A]?.symbol}
{currencyA?.symbol}
</Text>
</RowFixed>
</RowBetween>
<RowBetween>
<Text fontSize={24} fontWeight={500}>
{formattedAmounts[Field.TOKEN_B] || '-'}
{formattedAmounts[Field.CURRENCY_B] || '-'}
</Text>
<RowFixed>
<TokenLogo address={tokens[Field.TOKEN_B]?.address} style={{ marginRight: '12px' }} />
<CurrencyLogo currency={currencyB} style={{ marginRight: '12px' }} />
<Text fontSize={24} fontWeight={500} id="remove-liquidity-tokenb-symbol">
{tokens[Field.TOKEN_B]?.symbol}
{currencyB?.symbol}
</Text>
</RowFixed>
</RowBetween>
{chainId && (oneCurrencyIsWETH || oneCurrencyIsETH) ? (
<RowBetween style={{ justifyContent: 'flex-end' }}>
{oneCurrencyIsETH ? (
<StyledInternalLink
to={`/remove/${currencyA === ETHER ? WETH[chainId].address : currencyIdA}/${
currencyB === ETHER ? WETH[chainId].address : currencyIdB
}`}
>
Receive WETH
</StyledInternalLink>
) : oneCurrencyIsWETH ? (
<StyledInternalLink
to={`/remove/${
currencyA && currencyEquals(currencyA, WETH[chainId]) ? 'ETH' : currencyIdA
}/${currencyB && currencyEquals(currencyB, WETH[chainId]) ? 'ETH' : currencyIdB}`}
>
Receive ETH
</StyledInternalLink>
) : null}
</RowBetween>
) : null}
</AutoColumn>
</LightCard>
</>
@ -491,16 +571,14 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
{showDetailed && (
<>
<CurrencyInputPanel
field={Field.LIQUIDITY}
value={formattedAmounts[Field.LIQUIDITY]}
onUserInput={onUserInput}
onUserInput={onLiquidityInput}
onMax={() => {
onUserInput(Field.LIQUIDITY_PERCENT, '100')
}}
showMaxButton={!atMaxAmount}
disableTokenSelect
token={pair?.liquidityToken}
isExchange={true}
disableCurrencySelect
currency={pair?.liquidityToken}
pair={pair}
id="liquidity-amount"
/>
@ -509,14 +587,13 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
</ColumnCenter>
<CurrencyInputPanel
hideBalance={true}
field={Field.TOKEN_A}
value={formattedAmounts[Field.TOKEN_A]}
onUserInput={onUserInput}
value={formattedAmounts[Field.CURRENCY_A]}
onUserInput={onCurrencyAInput}
onMax={() => onUserInput(Field.LIQUIDITY_PERCENT, '100')}
showMaxButton={!atMaxAmount}
token={tokens[Field.TOKEN_A]}
currency={currencyA}
label={'Output'}
disableTokenSelect
onCurrencySelect={handleSelectCurrencyA}
id="remove-liquidity-tokena"
/>
<ColumnCenter>
@ -524,32 +601,36 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
</ColumnCenter>
<CurrencyInputPanel
hideBalance={true}
field={Field.TOKEN_B}
value={formattedAmounts[Field.TOKEN_B]}
onUserInput={onUserInput}
value={formattedAmounts[Field.CURRENCY_B]}
onUserInput={onCurrencyBInput}
onMax={() => onUserInput(Field.LIQUIDITY_PERCENT, '100')}
showMaxButton={!atMaxAmount}
token={tokens[Field.TOKEN_B]}
currency={currencyB}
label={'Output'}
disableTokenSelect
onCurrencySelect={handleSelectCurrencyB}
id="remove-liquidity-tokenb"
/>
</>
)}
{route && (
{pair && (
<div style={{ padding: '10px 20px' }}>
<RowBetween>
Price:
<div>
1 {tokens[Field.TOKEN_A]?.symbol} = {route.midPrice.toSignificant(6)}{' '}
{tokens[Field.TOKEN_B]?.symbol}
1 {currencyA?.symbol} = {tokenA ? pair.priceOf(tokenA).toSignificant(6) : '-'} {currencyB?.symbol}
</div>
</RowBetween>
<RowBetween>
<div />
<div>
1 {tokens[Field.TOKEN_B]?.symbol} = {route.midPrice.invert().toSignificant(6)}{' '}
{tokens[Field.TOKEN_A]?.symbol}
1 {currencyB?.symbol} ={' '}
{tokenB
? pair
.priceOf(tokenB)
.invert()
.toSignificant(6)
: '-'}{' '}
{currencyA?.symbol}
</div>
</RowBetween>
</div>
@ -580,7 +661,7 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
setShowConfirm(true)
}}
disabled={!isValid || (signatureData === null && approval !== ApprovalState.APPROVED)}
error={!isValid && !!parsedAmounts[Field.TOKEN_A] && !!parsedAmounts[Field.TOKEN_B]}
error={!isValid && !!parsedAmounts[Field.CURRENCY_A] && !!parsedAmounts[Field.CURRENCY_B]}
>
<Text fontSize={16} fontWeight={500}>
{error || 'Remove'}
@ -595,7 +676,7 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
{pair ? (
<AutoColumn style={{ minWidth: '20rem', marginTop: '1rem' }}>
<MinimalPositionCard pair={pair} />
<MinimalPositionCard showUnwrapped={oneCurrencyIsWETH} pair={pair} />
</AutoColumn>
) : null}
</>

@ -0,0 +1,17 @@
import React from 'react'
import { RouteComponentProps, Redirect } from 'react-router-dom'
const OLD_PATH_STRUCTURE = /^(0x[a-fA-F0-9]{40})-(0x[a-fA-F0-9]{40})$/
export function RedirectOldRemoveLiquidityPathStructure({
match: {
params: { tokens }
}
}: RouteComponentProps<{ tokens: string }>) {
if (!OLD_PATH_STRUCTURE.test(tokens)) {
return <Redirect to="/pool" />
}
const [currency0, currency1] = tokens.split('-')
return <Redirect to={`/remove/${currency0}/${currency1}`} />
}

@ -0,0 +1,7 @@
{
"extends": "../../../tsconfig.strict.json",
"include": [
"**/*",
"../../../node_modules/eslint-plugin-react/lib/types.d.ts"
]
}

@ -1,5 +1,5 @@
import { JSBI, TokenAmount } from '@uniswap/sdk'
import React, { useContext, useState, useEffect, useCallback } from 'react'
import { CurrencyAmount, JSBI } from '@uniswap/sdk'
import React, { useCallback, useContext, useEffect, useState } from 'react'
import { ArrowDown } from 'react-feather'
import ReactGA from 'react-ga'
import { Text } from 'rebass'
@ -13,23 +13,23 @@ import CurrencyInputPanel from '../../components/CurrencyInputPanel'
import { SwapPoolTabs } from '../../components/NavigationTabs'
import { AutoRow, RowBetween } from '../../components/Row'
import AdvancedSwapDetailsDropdown from '../../components/swap/AdvancedSwapDetailsDropdown'
import BetterTradeLink from '../../components/swap/BetterTradeLink'
import confirmPriceImpactWithoutFee from '../../components/swap/confirmPriceImpactWithoutFee'
import { ArrowWrapper, BottomGrouping, Dots, Wrapper } from '../../components/swap/styleds'
import SwapModalFooter from '../../components/swap/SwapModalFooter'
import SwapModalHeader from '../../components/swap/SwapModalHeader'
import TradePrice from '../../components/swap/TradePrice'
import BetterTradeLink from '../../components/swap/BetterTradeLink'
import { TokenWarningCards } from '../../components/TokenWarningCard'
import { BETTER_TRADE_LINK_THRESHOLD, INITIAL_ALLOWED_SLIPPAGE } from '../../constants'
import { getTradeVersion, isTradeBetter } from '../../data/V1'
import { useActiveWeb3React } from '../../hooks'
import { useApproveCallbackFromTrade, ApprovalState } from '../../hooks/useApproveCallback'
import { ApprovalState, useApproveCallbackFromTrade } from '../../hooks/useApproveCallback'
import useENSAddress from '../../hooks/useENSAddress'
import { useSwapCallback } from '../../hooks/useSwapCallback'
import { useWalletModalToggle, useToggleSettingsMenu } from '../../state/application/hooks'
import { useExpertModeManager, useUserSlippageTolerance, useUserDeadline } from '../../state/user/hooks'
import { INITIAL_ALLOWED_SLIPPAGE, BETTER_TRADE_LINK_THRESHOLD } from '../../constants'
import { getTradeVersion, isTradeBetter } from '../../data/V1'
import useToggledVersion, { Version } from '../../hooks/useToggledVersion'
import useWrapCallback, { WrapType } from '../../hooks/useWrapCallback'
import { useToggleSettingsMenu, useWalletModalToggle } from '../../state/application/hooks'
import { Field } from '../../state/swap/actions'
import {
useDefaultsFromURLSearch,
@ -37,6 +37,7 @@ import {
useSwapActionHandlers,
useSwapState
} from '../../state/swap/hooks'
import { useExpertModeManager, useUserDeadline, useUserSlippageTolerance } from '../../state/user/hooks'
import { CursorPointer, LinkStyledButton, TYPE } from '../../theme'
import { maxAmountSpend } from '../../utils/maxAmountSpend'
import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown, warningSeverity } from '../../utils/prices'
@ -62,14 +63,21 @@ export default function Swap() {
// swap state
const { independentField, typedValue, recipient } = useSwapState()
const { v1Trade, v2Trade, tokenBalances, parsedAmount, tokens, error } = useDerivedSwapInfo()
const { v1Trade, v2Trade, currencyBalances, parsedAmount, currencies, error } = useDerivedSwapInfo()
const { wrapType, execute: onWrap, error: wrapError } = useWrapCallback(
currencies[Field.INPUT],
currencies[Field.OUTPUT],
typedValue
)
const showWrap: boolean = wrapType !== WrapType.NOT_APPLICABLE
const { address: recipientAddress } = useENSAddress(recipient)
const toggledVersion = useToggledVersion()
const trade =
{
const trade = showWrap
? undefined
: {
[Version.v1]: v1Trade,
[Version.v2]: v2Trade
}[toggledVersion] ?? undefined
}[toggledVersion]
const betterTradeLinkVersion: Version | undefined =
toggledVersion === Version.v2 && isTradeBetter(v2Trade, v1Trade, BETTER_TRADE_LINK_THRESHOLD)
@ -78,23 +86,28 @@ export default function Swap() {
? Version.v2
: undefined
const parsedAmounts = {
const parsedAmounts = showWrap
? {
[Field.INPUT]: parsedAmount,
[Field.OUTPUT]: parsedAmount
}
: {
[Field.INPUT]: independentField === Field.INPUT ? parsedAmount : trade?.inputAmount,
[Field.OUTPUT]: independentField === Field.OUTPUT ? parsedAmount : trade?.outputAmount
}
const { onSwitchTokens, onTokenSelection, onUserInput, onChangeRecipient } = useSwapActionHandlers()
const { onSwitchTokens, onCurrencySelection, onUserInput, onChangeRecipient } = useSwapActionHandlers()
const isValid = !error
const dependentField: Field = independentField === Field.INPUT ? Field.OUTPUT : Field.INPUT
const handleTypeInput = useCallback(
(field, value) => {
(value: string) => {
onUserInput(Field.INPUT, value)
},
[onUserInput]
)
const handleTypeOutput = useCallback(
(field, value) => {
(value: string) => {
onUserInput(Field.OUTPUT, value)
},
[onUserInput]
@ -107,12 +120,14 @@ export default function Swap() {
const formattedAmounts = {
[independentField]: typedValue,
[dependentField]: parsedAmounts[dependentField]?.toSignificant(6) ?? ''
[dependentField]: showWrap
? parsedAmounts[independentField]?.toExact() ?? ''
: parsedAmounts[dependentField]?.toSignificant(6) ?? ''
}
const route = trade?.route
const userHasSpecifiedInputOutput = Boolean(
tokens[Field.INPUT] && tokens[Field.OUTPUT] && parsedAmounts[independentField]?.greaterThan(JSBI.BigInt(0))
currencies[Field.INPUT] && currencies[Field.OUTPUT] && parsedAmounts[independentField]?.greaterThan(JSBI.BigInt(0))
)
const noRoute = !route
@ -129,7 +144,7 @@ export default function Swap() {
}
}, [approval, approvalSubmitted])
const maxAmountInput: TokenAmount | undefined = maxAmountSpend(tokenBalances[Field.INPUT])
const maxAmountInput: CurrencyAmount | undefined = maxAmountSpend(currencyBalances[Field.INPUT])
const atMaxAmountInput = Boolean(maxAmountInput && parsedAmounts[Field.INPUT]?.equalTo(maxAmountInput))
const slippageAdjustedAmounts = computeSlippageAdjustedAmounts(trade, allowedSlippage)
@ -160,9 +175,11 @@ export default function Swap() {
: (recipientAddress ?? recipient) === account
? 'Swap w/o Send + recipient'
: 'Swap w/ Send',
label: [trade?.inputAmount?.token?.symbol, trade?.outputAmount?.token?.symbol, getTradeVersion(trade)].join(
'/'
)
label: [
trade?.inputAmount?.currency?.symbol,
trade?.outputAmount?.currency?.symbol,
getTradeVersion(trade)
].join('/')
})
})
.catch(error => {
@ -192,7 +209,7 @@ export default function Swap() {
function modalHeader() {
return (
<SwapModalHeader
tokens={tokens}
currencies={currencies}
formattedAmounts={formattedAmounts}
slippageAdjustedAmounts={slippageAdjustedAmounts}
priceImpactSeverity={priceImpactSeverity}
@ -221,12 +238,12 @@ export default function Swap() {
// text to show while loading
const pendingText = `Swapping ${parsedAmounts[Field.INPUT]?.toSignificant(6)} ${
tokens[Field.INPUT]?.symbol
} for ${parsedAmounts[Field.OUTPUT]?.toSignificant(6)} ${tokens[Field.OUTPUT]?.symbol}`
currencies[Field.INPUT]?.symbol
} for ${parsedAmounts[Field.OUTPUT]?.toSignificant(6)} ${currencies[Field.OUTPUT]?.symbol}`
return (
<>
<TokenWarningCards tokens={tokens} />
<TokenWarningCards currencies={currencies} />
<AppBody>
<SwapPoolTabs active={'swap'} />
<Wrapper id="swap-page">
@ -250,20 +267,19 @@ export default function Swap() {
<AutoColumn gap={'md'}>
<CurrencyInputPanel
field={Field.INPUT}
label={independentField === Field.OUTPUT ? 'From (estimated)' : 'From'}
label={independentField === Field.OUTPUT && !showWrap ? 'From (estimated)' : 'From'}
value={formattedAmounts[Field.INPUT]}
showMaxButton={!atMaxAmountInput}
token={tokens[Field.INPUT]}
currency={currencies[Field.INPUT]}
onUserInput={handleTypeInput}
onMax={() => {
maxAmountInput && onUserInput(Field.INPUT, maxAmountInput.toExact())
}}
onTokenSelection={address => {
onCurrencySelect={currency => {
setApprovalSubmitted(false) // reset 2 step UI for approvals
onTokenSelection(Field.INPUT, address)
onCurrencySelection(Field.INPUT, currency)
}}
otherSelectedTokenAddress={tokens[Field.OUTPUT]?.address}
otherCurrency={currencies[Field.OUTPUT]}
id="swap-currency-input"
/>
@ -277,10 +293,10 @@ export default function Swap() {
setApprovalSubmitted(false) // reset 2 step UI for approvals
onSwitchTokens()
}}
color={tokens[Field.INPUT] && tokens[Field.OUTPUT] ? theme.primary1 : theme.text2}
color={currencies[Field.INPUT] && currencies[Field.OUTPUT] ? theme.primary1 : theme.text2}
/>
</ArrowWrapper>
{recipient === null ? (
{recipient === null && !showWrap ? (
<LinkStyledButton id="add-recipient-button" onClick={() => onChangeRecipient('')}>
+ Add a send (optional)
</LinkStyledButton>
@ -289,18 +305,17 @@ export default function Swap() {
</AutoColumn>
</CursorPointer>
<CurrencyInputPanel
field={Field.OUTPUT}
value={formattedAmounts[Field.OUTPUT]}
onUserInput={handleTypeOutput}
label={independentField === Field.INPUT ? 'To (estimated)' : 'To'}
label={independentField === Field.INPUT && !showWrap ? 'To (estimated)' : 'To'}
showMaxButton={false}
token={tokens[Field.OUTPUT]}
onTokenSelection={address => onTokenSelection(Field.OUTPUT, address)}
otherSelectedTokenAddress={tokens[Field.INPUT]?.address}
currency={currencies[Field.OUTPUT]}
onCurrencySelect={address => onCurrencySelection(Field.OUTPUT, address)}
otherCurrency={currencies[Field.INPUT]}
id="swap-currency-output"
/>
{recipient !== null ? (
{recipient !== null && !showWrap ? (
<>
<AutoRow justify="space-between" style={{ padding: '0 1rem' }}>
<ArrowWrapper clickable={false}>
@ -314,6 +329,7 @@ export default function Swap() {
</>
) : null}
{showWrap ? null : (
<Card padding={'.25rem .75rem 0 .75rem'} borderRadius={'20px'}>
<AutoColumn gap="4px">
<RowBetween align="center">
@ -321,8 +337,8 @@ export default function Swap() {
Price
</Text>
<TradePrice
inputToken={tokens[Field.INPUT]}
outputToken={tokens[Field.OUTPUT]}
inputCurrency={currencies[Field.INPUT]}
outputCurrency={currencies[Field.OUTPUT]}
price={trade?.executionPrice}
showInverted={showInverted}
setShowInverted={setShowInverted}
@ -341,10 +357,15 @@ export default function Swap() {
)}
</AutoColumn>
</Card>
)}
</AutoColumn>
<BottomGrouping>
{!account ? (
<ButtonLight onClick={toggleWalletModal}>Connect Wallet</ButtonLight>
) : showWrap ? (
<ButtonPrimary disabled={Boolean(wrapError)} onClick={onWrap}>
{wrapError ?? (wrapType === WrapType.WRAP ? 'Wrap' : wrapType === WrapType.UNWRAP ? 'Unwrap' : null)}
</ButtonPrimary>
) : noRoute && userHasSpecifiedInputOutput ? (
<GreyCard style={{ textAlign: 'center' }}>
<TYPE.main mb="4px">Insufficient liquidity for this trade.</TYPE.main>
@ -362,7 +383,7 @@ export default function Swap() {
) : approvalSubmitted && approval === ApprovalState.APPROVED ? (
'Approved'
) : (
'Approve ' + tokens[Field.INPUT]?.symbol
'Approve ' + currencies[Field.INPUT]?.symbol
)}
</ButtonPrimary>
<ButtonError

@ -1,25 +1,12 @@
import { createAction } from '@reduxjs/toolkit'
export type PopupContent =
| {
export type PopupContent = {
txn: {
hash: string
success?: boolean
success: boolean
summary?: string
}
}
| {
poolAdded: {
token0?: {
address?: string
symbol?: string
}
token1: {
address?: string
symbol?: string
}
}
}
}
export const updateBlockNumber = createAction<{ chainId: number; blockNumber: number }>('updateBlockNumber')
export const toggleWalletModal = createAction<void>('toggleWalletModal')

@ -1,15 +1,10 @@
import { createAction } from '@reduxjs/toolkit'
import { ChainId } from '@uniswap/sdk'
export enum Field {
LIQUIDITY_PERCENT = 'LIQUIDITY_PERCENT',
LIQUIDITY = 'LIQUIDITY',
TOKEN_A = 'TOKEN_A',
TOKEN_B = 'TOKEN_B'
CURRENCY_A = 'CURRENCY_A',
CURRENCY_B = 'CURRENCY_B'
}
export const typeInput = createAction<{ field: Field; typedValue: string }>('typeInputBurn')
export const setBurnDefaultsFromURLMatchParams = createAction<{
chainId: ChainId
params: { tokens: string }
}>('setBurnDefaultsFromURLMatchParams')

@ -1,94 +1,74 @@
import { useEffect, useCallback, useMemo } from 'react'
import { Currency, CurrencyAmount, JSBI, Pair, Percent, TokenAmount } from '@uniswap/sdk'
import { useCallback } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { useActiveWeb3React } from '../../hooks'
import { AppDispatch, AppState } from '../index'
import { Field, setBurnDefaultsFromURLMatchParams, typeInput } from './actions'
import { useToken } from '../../hooks/Tokens'
import { Token, Pair, TokenAmount, Percent, JSBI, Route } from '@uniswap/sdk'
import { usePair } from '../../data/Reserves'
import { useTokenBalances } from '../wallet/hooks'
import { tryParseAmount } from '../swap/hooks'
import { useTotalSupply } from '../../data/TotalSupply'
const ZERO = JSBI.BigInt(0)
import { useActiveWeb3React } from '../../hooks'
import { wrappedCurrency } from '../../utils/wrappedCurrency'
import { AppDispatch, AppState } from '../index'
import { tryParseAmount } from '../swap/hooks'
import { useTokenBalances } from '../wallet/hooks'
import { Field, typeInput } from './actions'
export function useBurnState(): AppState['burn'] {
return useSelector<AppState, AppState['burn']>(state => state.burn)
}
export function useDerivedBurnInfo(): {
tokens: { [field in Extract<Field, Field.TOKEN_A | Field.TOKEN_B>]?: Token }
export function useDerivedBurnInfo(
currencyA: Currency | undefined,
currencyB: Currency | undefined
): {
pair?: Pair | null
route?: Route
parsedAmounts: {
[Field.LIQUIDITY_PERCENT]: Percent
[Field.LIQUIDITY]?: TokenAmount
[Field.TOKEN_A]?: TokenAmount
[Field.TOKEN_B]?: TokenAmount
[Field.CURRENCY_A]?: CurrencyAmount
[Field.CURRENCY_B]?: CurrencyAmount
}
error?: string
} {
const { account } = useActiveWeb3React()
const { account, chainId } = useActiveWeb3React()
const {
independentField,
typedValue,
[Field.TOKEN_A]: { address: tokenAAddress },
[Field.TOKEN_B]: { address: tokenBAddress }
} = useBurnState()
// tokens
const tokenA = useToken(tokenAAddress)
const tokenB = useToken(tokenBAddress)
const tokens: { [field in Extract<Field, Field.TOKEN_A | Field.TOKEN_B>]?: Token } = useMemo(
() => ({
[Field.TOKEN_A]: tokenA ?? undefined,
[Field.TOKEN_B]: tokenB ?? undefined
}),
[tokenA, tokenB]
)
const { independentField, typedValue } = useBurnState()
// pair + totalsupply
const pair = usePair(tokens[Field.TOKEN_A], tokens[Field.TOKEN_B])
const noLiquidity =
pair === null || (!!pair && JSBI.equal(pair.reserve0.raw, ZERO) && JSBI.equal(pair.reserve1.raw, ZERO))
// route
const route =
!noLiquidity && pair && tokens[Field.TOKEN_A] ? new Route([pair], tokens[Field.TOKEN_A] as Token) : undefined
const [, pair] = usePair(currencyA, currencyB)
// balances
const relevantTokenBalances = useTokenBalances(account ?? undefined, [pair?.liquidityToken])
const userLiquidity: undefined | TokenAmount = relevantTokenBalances?.[pair?.liquidityToken?.address ?? '']
const [tokenA, tokenB] = [wrappedCurrency(currencyA, chainId), wrappedCurrency(currencyB, chainId)]
const tokens = {
[Field.CURRENCY_A]: tokenA,
[Field.CURRENCY_B]: tokenB,
[Field.LIQUIDITY]: pair?.liquidityToken
}
// liquidity values
const totalSupply = useTotalSupply(pair?.liquidityToken)
const liquidityValues: { [field in Extract<Field, Field.TOKEN_A | Field.TOKEN_B>]?: TokenAmount } = {
[Field.TOKEN_A]:
const liquidityValueA =
pair &&
tokens[Field.TOKEN_A] &&
totalSupply &&
userLiquidity &&
tokenA &&
// this condition is a short-circuit in the case where useTokenBalance updates sooner than useTotalSupply
JSBI.greaterThanOrEqual(totalSupply.raw, userLiquidity.raw)
? new TokenAmount(
tokens[Field.TOKEN_A] as Token,
pair.getLiquidityValue(tokens[Field.TOKEN_A] as Token, totalSupply, userLiquidity, false).raw
)
: undefined,
[Field.TOKEN_B]:
pair &&
tokens[Field.TOKEN_B] &&
totalSupply &&
userLiquidity &&
// this condition is a short-circuit in the case where useTokenBalance updates sooner than useTotalSupply
JSBI.greaterThanOrEqual(totalSupply.raw, userLiquidity.raw)
? new TokenAmount(
tokens[Field.TOKEN_B] as Token,
pair.getLiquidityValue(tokens[Field.TOKEN_B] as Token, totalSupply, userLiquidity, false).raw
)
? new TokenAmount(tokenA, pair.getLiquidityValue(tokenA, totalSupply, userLiquidity, false).raw)
: undefined
const liquidityValueB =
pair &&
totalSupply &&
userLiquidity &&
tokenB &&
// this condition is a short-circuit in the case where useTokenBalance updates sooner than useTotalSupply
JSBI.greaterThanOrEqual(totalSupply.raw, userLiquidity.raw)
? new TokenAmount(tokenB, pair.getLiquidityValue(tokenB, totalSupply, userLiquidity, false).raw)
: undefined
const liquidityValues: { [Field.CURRENCY_A]?: TokenAmount; [Field.CURRENCY_B]?: TokenAmount } = {
[Field.CURRENCY_A]: liquidityValueA,
[Field.CURRENCY_B]: liquidityValueB
}
let percentToRemove: Percent = new Percent('0', '100')
@ -109,12 +89,9 @@ export function useDerivedBurnInfo(): {
else {
if (tokens[independentField]) {
const independentAmount = tryParseAmount(typedValue, tokens[independentField])
if (
independentAmount &&
liquidityValues[independentField] &&
!independentAmount.greaterThan(liquidityValues[independentField] as TokenAmount)
) {
percentToRemove = new Percent(independentAmount.raw, (liquidityValues[independentField] as TokenAmount).raw)
const liquidityValue = liquidityValues[independentField]
if (independentAmount && liquidityValue && !independentAmount.greaterThan(liquidityValue)) {
percentToRemove = new Percent(independentAmount.raw, liquidityValue.raw)
}
}
}
@ -122,27 +99,21 @@ export function useDerivedBurnInfo(): {
const parsedAmounts: {
[Field.LIQUIDITY_PERCENT]: Percent
[Field.LIQUIDITY]?: TokenAmount
[Field.TOKEN_A]?: TokenAmount
[Field.TOKEN_B]?: TokenAmount
[Field.CURRENCY_A]?: TokenAmount
[Field.CURRENCY_B]?: TokenAmount
} = {
[Field.LIQUIDITY_PERCENT]: percentToRemove,
[Field.LIQUIDITY]:
userLiquidity && percentToRemove && percentToRemove.greaterThan('0')
? new TokenAmount(userLiquidity.token, percentToRemove.multiply(userLiquidity.raw).quotient)
: undefined,
[Field.TOKEN_A]:
tokens[Field.TOKEN_A] && percentToRemove && percentToRemove.greaterThan('0') && liquidityValues[Field.TOKEN_A]
? new TokenAmount(
tokens[Field.TOKEN_A] as Token,
percentToRemove.multiply((liquidityValues[Field.TOKEN_A] as TokenAmount).raw).quotient
)
[Field.CURRENCY_A]:
tokenA && percentToRemove && percentToRemove.greaterThan('0') && liquidityValueA
? new TokenAmount(tokenA, percentToRemove.multiply(liquidityValueA.raw).quotient)
: undefined,
[Field.TOKEN_B]:
tokens[Field.TOKEN_B] && percentToRemove && percentToRemove.greaterThan('0') && liquidityValues[Field.TOKEN_B]
? new TokenAmount(
tokens[Field.TOKEN_B] as Token,
percentToRemove.multiply((liquidityValues[Field.TOKEN_B] as TokenAmount).raw).quotient
)
[Field.CURRENCY_B]:
tokenB && percentToRemove && percentToRemove.greaterThan('0') && liquidityValueB
? new TokenAmount(tokenB, percentToRemove.multiply(liquidityValueB.raw).quotient)
: undefined
}
@ -151,11 +122,11 @@ export function useDerivedBurnInfo(): {
error = 'Connect Wallet'
}
if (!parsedAmounts[Field.LIQUIDITY] || !parsedAmounts[Field.TOKEN_A] || !parsedAmounts[Field.TOKEN_B]) {
if (!parsedAmounts[Field.LIQUIDITY] || !parsedAmounts[Field.CURRENCY_A] || !parsedAmounts[Field.CURRENCY_B]) {
error = error ?? 'Enter an amount'
}
return { tokens, pair, route, parsedAmounts, error }
return { pair, parsedAmounts, error }
}
export function useBurnActionHandlers(): {
@ -174,13 +145,3 @@ export function useBurnActionHandlers(): {
onUserInput
}
}
// updates the burn state to use the appropriate tokens, given the route
export function useDefaultsFromURLMatchParams(params: { tokens: string }) {
const { chainId } = useActiveWeb3React()
const dispatch = useDispatch<AppDispatch>()
useEffect(() => {
if (!chainId) return
dispatch(setBurnDefaultsFromURLMatchParams({ chainId, params }))
}, [dispatch, chainId, params])
}

@ -1,65 +1,18 @@
import { createReducer } from '@reduxjs/toolkit'
import { ChainId, WETH } from '@uniswap/sdk'
import { isAddress } from '../../utils'
import { Field, setBurnDefaultsFromURLMatchParams, typeInput } from './actions'
import { Field, typeInput } from './actions'
export interface BurnState {
readonly independentField: Field
readonly typedValue: string
readonly [Field.TOKEN_A]: {
readonly address: string
}
readonly [Field.TOKEN_B]: {
readonly address: string
}
}
const initialState: BurnState = {
independentField: Field.LIQUIDITY_PERCENT,
typedValue: '0',
[Field.TOKEN_A]: {
address: ''
},
[Field.TOKEN_B]: {
address: ''
}
}
export function parseTokens(chainId: ChainId, tokens: string): string[] {
return (
tokens
// split by '-'
.split('-')
// map to addresses
.map((token): string =>
isAddress(token) ? token : token.toLowerCase() === 'ETH'.toLowerCase() ? WETH[chainId]?.address ?? '' : ''
)
//remove duplicates
.filter((token, i, array) => array.indexOf(token) === i)
// add two empty elements for cases where the array is length 0
.concat(['', ''])
// only consider the first 2 elements
.slice(0, 2)
)
typedValue: '0'
}
export default createReducer<BurnState>(initialState, builder =>
builder
.addCase(setBurnDefaultsFromURLMatchParams, (state, { payload: { chainId, params } }) => {
const tokens = parseTokens(chainId, params?.tokens ?? '')
return {
independentField: Field.LIQUIDITY_PERCENT,
typedValue: '0',
[Field.TOKEN_A]: {
address: tokens[0]
},
[Field.TOKEN_B]: {
address: tokens[1]
}
}
})
.addCase(typeInput, (state, { payload: { field, typedValue } }) => {
builder.addCase(typeInput, (state, { payload: { field, typedValue } }) => {
return {
...state,
independentField: field,

@ -1,8 +1,8 @@
import { createAction } from '@reduxjs/toolkit'
export enum Field {
TOKEN_A = 'TOKEN_A',
TOKEN_B = 'TOKEN_B'
CURRENCY_A = 'CURRENCY_A',
CURRENCY_B = 'CURRENCY_B'
}
export const typeInput = createAction<{ field: Field; typedValue: string; noLiquidity: boolean }>('typeInputMint')

@ -1,14 +1,15 @@
import { Currency, CurrencyAmount, JSBI, Pair, Percent, Price, TokenAmount } from '@uniswap/sdk'
import { useCallback, useMemo } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { Token, TokenAmount, Route, JSBI, Price, Percent, Pair } from '@uniswap/sdk'
import { PairState, usePair } from '../../data/Reserves'
import { useTotalSupply } from '../../data/TotalSupply'
import { useActiveWeb3React } from '../../hooks'
import { wrappedCurrency, wrappedCurrencyAmount } from '../../utils/wrappedCurrency'
import { AppDispatch, AppState } from '../index'
import { Field, typeInput } from './actions'
import { useTokenBalancesTreatWETHAsETH } from '../wallet/hooks'
import { usePair } from '../../data/Reserves'
import { useTotalSupply } from '../../data/TotalSupply'
import { tryParseAmount } from '../swap/hooks'
import { useCurrencyBalances } from '../wallet/hooks'
import { Field, typeInput } from './actions'
const ZERO = JSBI.BigInt(0)
@ -17,109 +18,98 @@ export function useMintState(): AppState['mint'] {
}
export function useDerivedMintInfo(
tokenA: Token | undefined,
tokenB: Token | undefined
currencyA: Currency | undefined,
currencyB: Currency | undefined
): {
dependentField: Field
tokens: { [field in Field]?: Token }
currencies: { [field in Field]?: Currency }
pair?: Pair | null
tokenBalances: { [field in Field]?: TokenAmount }
parsedAmounts: { [field in Field]?: TokenAmount }
pairState: PairState
currencyBalances: { [field in Field]?: CurrencyAmount }
parsedAmounts: { [field in Field]?: CurrencyAmount }
price?: Price
noLiquidity?: boolean
liquidityMinted?: TokenAmount
poolTokenPercentage?: Percent
error?: string
} {
const { account } = useActiveWeb3React()
const { account, chainId } = useActiveWeb3React()
const { independentField, typedValue, otherTypedValue } = useMintState()
const dependentField = independentField === Field.TOKEN_A ? Field.TOKEN_B : Field.TOKEN_A
const dependentField = independentField === Field.CURRENCY_A ? Field.CURRENCY_B : Field.CURRENCY_A
// tokens
const tokens: { [field in Field]?: Token } = useMemo(
const currencies: { [field in Field]?: Currency } = useMemo(
() => ({
[Field.TOKEN_A]: tokenA,
[Field.TOKEN_B]: tokenB
[Field.CURRENCY_A]: currencyA ?? undefined,
[Field.CURRENCY_B]: currencyB ?? undefined
}),
[tokenA, tokenB]
[currencyA, currencyB]
)
// pair
const pair = usePair(tokens[Field.TOKEN_A], tokens[Field.TOKEN_B])
const noLiquidity =
pair === null || (!!pair && JSBI.equal(pair.reserve0.raw, ZERO) && JSBI.equal(pair.reserve1.raw, ZERO))
// route
const route = useMemo(
() =>
!noLiquidity && pair && tokens[independentField] ? new Route([pair], tokens[Field.TOKEN_A] as Token) : undefined,
[noLiquidity, pair, tokens, independentField]
)
const [pairState, pair] = usePair(currencies[Field.CURRENCY_A], currencies[Field.CURRENCY_B])
const noLiquidity: boolean =
pairState === PairState.NOT_EXISTS ||
Boolean(pair && JSBI.equal(pair.reserve0.raw, ZERO) && JSBI.equal(pair.reserve1.raw, ZERO))
// balances
const relevantTokenBalances = useTokenBalancesTreatWETHAsETH(account ?? undefined, [
tokens[Field.TOKEN_A],
tokens[Field.TOKEN_B]
const balances = useCurrencyBalances(account ?? undefined, [
currencies[Field.CURRENCY_A],
currencies[Field.CURRENCY_B]
])
const tokenBalances: { [field in Field]?: TokenAmount } = {
[Field.TOKEN_A]: relevantTokenBalances?.[tokens[Field.TOKEN_A]?.address ?? ''],
[Field.TOKEN_B]: relevantTokenBalances?.[tokens[Field.TOKEN_B]?.address ?? '']
const currencyBalances: { [field in Field]?: CurrencyAmount } = {
[Field.CURRENCY_A]: balances[0],
[Field.CURRENCY_B]: balances[1]
}
// amounts
const independentAmount = tryParseAmount(typedValue, tokens[independentField])
const independentAmount = tryParseAmount(typedValue, currencies[independentField])
const dependentAmount = useMemo(() => {
if (noLiquidity && otherTypedValue && tokens[dependentField]) {
return tryParseAmount(otherTypedValue, tokens[dependentField])
} else if (route && independentAmount) {
return dependentField === Field.TOKEN_B
? route.midPrice.quote(independentAmount)
: route.midPrice.invert().quote(independentAmount)
if (noLiquidity && otherTypedValue && currencies[dependentField]) {
return tryParseAmount(otherTypedValue, currencies[dependentField])
} else if (independentAmount) {
const wrappedIndependentAmount = wrappedCurrencyAmount(independentAmount, chainId)
const [tokenA, tokenB] = [wrappedCurrency(currencyA, chainId), wrappedCurrency(currencyB, chainId)]
if (tokenA && tokenB && wrappedIndependentAmount && pair) {
return dependentField === Field.CURRENCY_B
? pair.priceOf(tokenA).quote(wrappedIndependentAmount)
: pair.priceOf(tokenB).quote(wrappedIndependentAmount)
}
return
} else {
return
}
}, [noLiquidity, otherTypedValue, tokens, dependentField, independentAmount, route])
const parsedAmounts = {
[Field.TOKEN_A]: independentField === Field.TOKEN_A ? independentAmount : dependentAmount,
[Field.TOKEN_B]: independentField === Field.TOKEN_A ? dependentAmount : independentAmount
}, [noLiquidity, otherTypedValue, currencies, dependentField, independentAmount, currencyA, chainId, currencyB, pair])
const parsedAmounts: { [field in Field]: CurrencyAmount | undefined } = {
[Field.CURRENCY_A]: independentField === Field.CURRENCY_A ? independentAmount : dependentAmount,
[Field.CURRENCY_B]: independentField === Field.CURRENCY_A ? dependentAmount : independentAmount
}
const price = useMemo(() => {
if (
noLiquidity &&
tokens[Field.TOKEN_A] &&
tokens[Field.TOKEN_B] &&
parsedAmounts[Field.TOKEN_A] &&
parsedAmounts[Field.TOKEN_B]
) {
return new Price(
tokens[Field.TOKEN_A] as Token,
tokens[Field.TOKEN_B] as Token,
(parsedAmounts[Field.TOKEN_A] as TokenAmount).raw,
(parsedAmounts[Field.TOKEN_B] as TokenAmount).raw
)
} else if (route) {
return route.midPrice
const { [Field.CURRENCY_A]: currencyAAmount, [Field.CURRENCY_B]: currencyBAmount } = parsedAmounts
if (noLiquidity && currencyAAmount && currencyBAmount) {
return new Price(currencyAAmount.currency, currencyBAmount.currency, currencyAAmount.raw, currencyBAmount.raw)
} else {
return
}
}, [noLiquidity, tokens, parsedAmounts, route])
}, [noLiquidity, parsedAmounts])
// liquidity minted
const totalSupply = useTotalSupply(pair?.liquidityToken)
const liquidityMinted = useMemo(() => {
if (pair && totalSupply && parsedAmounts[Field.TOKEN_A] && parsedAmounts[Field.TOKEN_B]) {
return pair.getLiquidityMinted(
totalSupply,
parsedAmounts[Field.TOKEN_A] as TokenAmount,
parsedAmounts[Field.TOKEN_B] as TokenAmount
)
const { [Field.CURRENCY_A]: currencyAAmount, [Field.CURRENCY_B]: currencyBAmount } = parsedAmounts
const [tokenAmountA, tokenAmountB] = [
wrappedCurrencyAmount(currencyAAmount, chainId),
wrappedCurrencyAmount(currencyBAmount, chainId)
]
if (pair && totalSupply && tokenAmountA && tokenAmountB) {
return pair.getLiquidityMinted(totalSupply, tokenAmountA, tokenAmountB)
} else {
return
}
}, [pair, totalSupply, parsedAmounts])
}, [parsedAmounts, chainId, pair, totalSupply])
const poolTokenPercentage = useMemo(() => {
if (liquidityMinted && totalSupply) {
@ -134,29 +124,30 @@ export function useDerivedMintInfo(
error = 'Connect Wallet'
}
if (!parsedAmounts[Field.TOKEN_A] || !parsedAmounts[Field.TOKEN_B]) {
if (pairState === PairState.INVALID) {
error = error ?? 'Invalid pair'
}
if (!parsedAmounts[Field.CURRENCY_A] || !parsedAmounts[Field.CURRENCY_B]) {
error = error ?? 'Enter an amount'
}
if (
parsedAmounts[Field.TOKEN_A] &&
tokenBalances?.[Field.TOKEN_A]?.lessThan(parsedAmounts[Field.TOKEN_A] as TokenAmount)
) {
error = 'Insufficient ' + tokens[Field.TOKEN_A]?.symbol + ' balance'
const { [Field.CURRENCY_A]: currencyAAmount, [Field.CURRENCY_B]: currencyBAmount } = parsedAmounts
if (currencyAAmount && currencyBalances?.[Field.CURRENCY_A]?.lessThan(currencyAAmount)) {
error = 'Insufficient ' + currencies[Field.CURRENCY_A]?.symbol + ' balance'
}
if (
parsedAmounts[Field.TOKEN_B] &&
tokenBalances?.[Field.TOKEN_B]?.lessThan(parsedAmounts[Field.TOKEN_B] as TokenAmount)
) {
error = 'Insufficient ' + tokens[Field.TOKEN_B]?.symbol + ' balance'
if (currencyBAmount && currencyBalances?.[Field.CURRENCY_B]?.lessThan(currencyBAmount)) {
error = 'Insufficient ' + currencies[Field.CURRENCY_B]?.symbol + ' balance'
}
return {
dependentField,
tokens,
currencies,
pair,
tokenBalances,
pairState,
currencyBalances,
parsedAmounts,
price,
noLiquidity,
@ -169,18 +160,26 @@ export function useDerivedMintInfo(
export function useMintActionHandlers(
noLiquidity: boolean | undefined
): {
onUserInput: (field: Field, typedValue: string) => void
onFieldAInput: (typedValue: string) => void
onFieldBInput: (typedValue: string) => void
} {
const dispatch = useDispatch<AppDispatch>()
const onUserInput = useCallback(
(field: Field, typedValue: string) => {
dispatch(typeInput({ field, typedValue, noLiquidity: noLiquidity === true }))
const onFieldAInput = useCallback(
(typedValue: string) => {
dispatch(typeInput({ field: Field.CURRENCY_A, typedValue, noLiquidity: noLiquidity === true }))
},
[dispatch, noLiquidity]
)
const onFieldBInput = useCallback(
(typedValue: string) => {
dispatch(typeInput({ field: Field.CURRENCY_B, typedValue, noLiquidity: noLiquidity === true }))
},
[dispatch, noLiquidity]
)
return {
onUserInput
onFieldAInput,
onFieldBInput
}
}

@ -8,7 +8,7 @@ describe('mint reducer', () => {
beforeEach(() => {
store = createStore(reducer, {
independentField: Field.TOKEN_A,
independentField: Field.CURRENCY_A,
typedValue: '',
otherTypedValue: ''
})
@ -16,13 +16,13 @@ describe('mint reducer', () => {
describe('typeInput', () => {
it('sets typed value', () => {
store.dispatch(typeInput({ field: Field.TOKEN_A, typedValue: '1.0', noLiquidity: false }))
expect(store.getState()).toEqual({ independentField: Field.TOKEN_A, typedValue: '1.0', otherTypedValue: '' })
store.dispatch(typeInput({ field: Field.CURRENCY_A, typedValue: '1.0', noLiquidity: false }))
expect(store.getState()).toEqual({ independentField: Field.CURRENCY_A, typedValue: '1.0', otherTypedValue: '' })
})
it('clears other value', () => {
store.dispatch(typeInput({ field: Field.TOKEN_A, typedValue: '1.0', noLiquidity: false }))
store.dispatch(typeInput({ field: Field.TOKEN_B, typedValue: '1.0', noLiquidity: false }))
expect(store.getState()).toEqual({ independentField: Field.TOKEN_B, typedValue: '1.0', otherTypedValue: '' })
store.dispatch(typeInput({ field: Field.CURRENCY_A, typedValue: '1.0', noLiquidity: false }))
store.dispatch(typeInput({ field: Field.CURRENCY_B, typedValue: '1.0', noLiquidity: false }))
expect(store.getState()).toEqual({ independentField: Field.CURRENCY_B, typedValue: '1.0', otherTypedValue: '' })
})
})
})

@ -8,7 +8,7 @@ export interface MintState {
}
const initialState: MintState = {
independentField: Field.TOKEN_A,
independentField: Field.CURRENCY_A,
typedValue: '',
otherTypedValue: ''
}

@ -5,14 +5,14 @@ export enum Field {
OUTPUT = 'OUTPUT'
}
export const selectToken = createAction<{ field: Field; address: string }>('selectToken')
export const switchTokens = createAction<void>('switchTokens')
export const selectCurrency = createAction<{ field: Field; currencyId: string }>('selectCurrency')
export const switchCurrencies = createAction<void>('switchCurrencies')
export const typeInput = createAction<{ field: Field; typedValue: string }>('typeInput')
export const replaceSwapState = createAction<{
field: Field
typedValue: string
inputTokenAddress?: string
outputTokenAddress?: string
inputCurrencyId?: string
outputCurrencyId?: string
recipient: string | null
}>('replaceSwapState')
export const setRecipient = createAction<{ recipient: string | null }>('setRecipient')

@ -1,4 +1,3 @@
import { ChainId, WETH } from '@uniswap/sdk'
import { parse } from 'qs'
import { Field } from './actions'
import { queryParametersToSwapState } from './hooks'
@ -11,12 +10,11 @@ describe('hooks', () => {
parse(
'?inputCurrency=ETH&outputCurrency=0x6b175474e89094c44da98b954eedeac495271d0f&exactAmount=20.5&exactField=outPUT',
{ parseArrays: false, ignoreQueryPrefix: true }
),
ChainId.MAINNET
)
)
).toEqual({
[Field.OUTPUT]: { address: '0x6B175474E89094C44Da98b954EedeAC495271d0F' },
[Field.INPUT]: { address: WETH[ChainId.MAINNET].address },
[Field.OUTPUT]: { currencyId: '0x6B175474E89094C44Da98b954EedeAC495271d0F' },
[Field.INPUT]: { currencyId: 'ETH' },
typedValue: '20.5',
independentField: Field.OUTPUT,
recipient: null
@ -25,13 +23,10 @@ describe('hooks', () => {
test('does not duplicate eth for invalid output token', () => {
expect(
queryParametersToSwapState(
parse('?outputCurrency=invalid', { parseArrays: false, ignoreQueryPrefix: true }),
ChainId.MAINNET
)
queryParametersToSwapState(parse('?outputCurrency=invalid', { parseArrays: false, ignoreQueryPrefix: true }))
).toEqual({
[Field.INPUT]: { address: '' },
[Field.OUTPUT]: { address: WETH[ChainId.MAINNET].address },
[Field.INPUT]: { currencyId: '' },
[Field.OUTPUT]: { currencyId: 'ETH' },
typedValue: '',
independentField: Field.INPUT,
recipient: null
@ -41,12 +36,11 @@ describe('hooks', () => {
test('output ETH only', () => {
expect(
queryParametersToSwapState(
parse('?outputCurrency=eth&exactAmount=20.5', { parseArrays: false, ignoreQueryPrefix: true }),
ChainId.MAINNET
parse('?outputCurrency=eth&exactAmount=20.5', { parseArrays: false, ignoreQueryPrefix: true })
)
).toEqual({
[Field.OUTPUT]: { address: WETH[ChainId.MAINNET].address },
[Field.INPUT]: { address: '' },
[Field.OUTPUT]: { currencyId: 'ETH' },
[Field.INPUT]: { currencyId: '' },
typedValue: '20.5',
independentField: Field.INPUT,
recipient: null
@ -56,12 +50,11 @@ describe('hooks', () => {
test('invalid recipient', () => {
expect(
queryParametersToSwapState(
parse('?outputCurrency=eth&exactAmount=20.5&recipient=abc', { parseArrays: false, ignoreQueryPrefix: true }),
ChainId.MAINNET
parse('?outputCurrency=eth&exactAmount=20.5&recipient=abc', { parseArrays: false, ignoreQueryPrefix: true })
)
).toEqual({
[Field.OUTPUT]: { address: WETH[ChainId.MAINNET].address },
[Field.INPUT]: { address: '' },
[Field.OUTPUT]: { currencyId: 'ETH' },
[Field.INPUT]: { currencyId: '' },
typedValue: '20.5',
independentField: Field.INPUT,
recipient: null
@ -74,12 +67,11 @@ describe('hooks', () => {
parse('?outputCurrency=eth&exactAmount=20.5&recipient=0x0fF2D1eFd7A57B7562b2bf27F3f37899dB27F4a5', {
parseArrays: false,
ignoreQueryPrefix: true
}),
ChainId.MAINNET
})
)
).toEqual({
[Field.OUTPUT]: { address: WETH[ChainId.MAINNET].address },
[Field.INPUT]: { address: '' },
[Field.OUTPUT]: { currencyId: 'ETH' },
[Field.INPUT]: { currencyId: '' },
typedValue: '20.5',
independentField: Field.INPUT,
recipient: '0x0fF2D1eFd7A57B7562b2bf27F3f37899dB27F4a5'
@ -91,12 +83,11 @@ describe('hooks', () => {
parse('?outputCurrency=eth&exactAmount=20.5&recipient=bob.argent.xyz', {
parseArrays: false,
ignoreQueryPrefix: true
}),
ChainId.MAINNET
})
)
).toEqual({
[Field.OUTPUT]: { address: WETH[ChainId.MAINNET].address },
[Field.INPUT]: { address: '' },
[Field.OUTPUT]: { currencyId: 'ETH' },
[Field.INPUT]: { currencyId: '' },
typedValue: '20.5',
independentField: Field.INPUT,
recipient: 'bob.argent.xyz'

@ -1,19 +1,19 @@
import useENS from '../../hooks/useENS'
import { Version } from '../../hooks/useToggledVersion'
import { parseUnits } from '@ethersproject/units'
import { ChainId, JSBI, Token, TokenAmount, Trade, WETH } from '@uniswap/sdk'
import { Currency, CurrencyAmount, ETHER, JSBI, Token, TokenAmount, Trade } from '@uniswap/sdk'
import { ParsedQs } from 'qs'
import { useCallback, useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { useV1Trade } from '../../data/V1'
import { useActiveWeb3React } from '../../hooks'
import { useToken } from '../../hooks/Tokens'
import { useCurrency } from '../../hooks/Tokens'
import { useTradeExactIn, useTradeExactOut } from '../../hooks/Trades'
import useParsedQueryString from '../../hooks/useParsedQueryString'
import { isAddress } from '../../utils'
import { AppDispatch, AppState } from '../index'
import { useTokenBalancesTreatWETHAsETH } from '../wallet/hooks'
import { Field, replaceSwapState, selectToken, setRecipient, switchTokens, typeInput } from './actions'
import { useCurrencyBalances } from '../wallet/hooks'
import { Field, replaceSwapState, selectCurrency, setRecipient, switchCurrencies, typeInput } from './actions'
import { SwapState } from './reducer'
import useToggledVersion from '../../hooks/useToggledVersion'
import { useUserSlippageTolerance } from '../user/hooks'
@ -24,18 +24,18 @@ export function useSwapState(): AppState['swap'] {
}
export function useSwapActionHandlers(): {
onTokenSelection: (field: Field, address: string) => void
onCurrencySelection: (field: Field, currency: Currency) => void
onSwitchTokens: () => void
onUserInput: (field: Field, typedValue: string) => void
onChangeRecipient: (recipient: string | null) => void
} {
const dispatch = useDispatch<AppDispatch>()
const onTokenSelection = useCallback(
(field: Field, address: string) => {
const onCurrencySelection = useCallback(
(field: Field, currency: Currency) => {
dispatch(
selectToken({
selectCurrency({
field,
address
currencyId: currency instanceof Token ? currency.address : currency === ETHER ? 'ETH' : ''
})
)
},
@ -43,7 +43,7 @@ export function useSwapActionHandlers(): {
)
const onSwitchTokens = useCallback(() => {
dispatch(switchTokens())
dispatch(switchCurrencies())
}, [dispatch])
const onUserInput = useCallback(
@ -62,21 +62,23 @@ export function useSwapActionHandlers(): {
return {
onSwitchTokens,
onTokenSelection,
onCurrencySelection,
onUserInput,
onChangeRecipient
}
}
// try to parse a user entered amount for a given token
export function tryParseAmount(value?: string, token?: Token): TokenAmount | undefined {
if (!value || !token) {
export function tryParseAmount(value?: string, currency?: Currency): CurrencyAmount | undefined {
if (!value || !currency) {
return
}
try {
const typedValueParsed = parseUnits(value, token.decimals).toString()
const typedValueParsed = parseUnits(value, currency.decimals).toString()
if (typedValueParsed !== '0') {
return new TokenAmount(token, JSBI.BigInt(typedValueParsed))
return currency instanceof Token
? new TokenAmount(currency, JSBI.BigInt(typedValueParsed))
: CurrencyAmount.ether(JSBI.BigInt(typedValueParsed))
}
} catch (error) {
// should fail if the user specifies too many decimal places of precision (or maybe exceed max uint?)
@ -88,9 +90,9 @@ export function tryParseAmount(value?: string, token?: Token): TokenAmount | und
// from the current swap inputs, compute the best trade and return it.
export function useDerivedSwapInfo(): {
tokens: { [field in Field]?: Token }
tokenBalances: { [field in Field]?: TokenAmount }
parsedAmount: TokenAmount | undefined
currencies: { [field in Field]?: Currency }
currencyBalances: { [field in Field]?: CurrencyAmount }
parsedAmount: CurrencyAmount | undefined
v2Trade: Trade | undefined
error?: string
v1Trade: Trade | undefined
@ -102,41 +104,41 @@ export function useDerivedSwapInfo(): {
const {
independentField,
typedValue,
[Field.INPUT]: { address: tokenInAddress },
[Field.OUTPUT]: { address: tokenOutAddress },
[Field.INPUT]: { currencyId: inputCurrencyId },
[Field.OUTPUT]: { currencyId: outputCurrencyId },
recipient
} = useSwapState()
const tokenIn = useToken(tokenInAddress)
const tokenOut = useToken(tokenOutAddress)
const inputCurrency = useCurrency(inputCurrencyId)
const outputCurrency = useCurrency(outputCurrencyId)
const recipientLookup = useENS(recipient ?? undefined)
const to: string | null = (recipient === null ? account : recipientLookup.address) ?? null
const relevantTokenBalances = useTokenBalancesTreatWETHAsETH(account ?? undefined, [
tokenIn ?? undefined,
tokenOut ?? undefined
const relevantTokenBalances = useCurrencyBalances(account ?? undefined, [
inputCurrency ?? undefined,
outputCurrency ?? undefined
])
const isExactIn: boolean = independentField === Field.INPUT
const parsedAmount = tryParseAmount(typedValue, (isExactIn ? tokenIn : tokenOut) ?? undefined)
const parsedAmount = tryParseAmount(typedValue, (isExactIn ? inputCurrency : outputCurrency) ?? undefined)
const bestTradeExactIn = useTradeExactIn(isExactIn ? parsedAmount : undefined, tokenOut ?? undefined)
const bestTradeExactOut = useTradeExactOut(tokenIn ?? undefined, !isExactIn ? parsedAmount : undefined)
const bestTradeExactIn = useTradeExactIn(isExactIn ? parsedAmount : undefined, outputCurrency ?? undefined)
const bestTradeExactOut = useTradeExactOut(inputCurrency ?? undefined, !isExactIn ? parsedAmount : undefined)
const v2Trade = isExactIn ? bestTradeExactIn : bestTradeExactOut
const tokenBalances = {
[Field.INPUT]: relevantTokenBalances?.[tokenIn?.address ?? ''],
[Field.OUTPUT]: relevantTokenBalances?.[tokenOut?.address ?? '']
const currencyBalances = {
[Field.INPUT]: relevantTokenBalances[0],
[Field.OUTPUT]: relevantTokenBalances[1]
}
const tokens: { [field in Field]?: Token } = {
[Field.INPUT]: tokenIn ?? undefined,
[Field.OUTPUT]: tokenOut ?? undefined
const currencies: { [field in Field]?: Currency } = {
[Field.INPUT]: inputCurrency ?? undefined,
[Field.OUTPUT]: outputCurrency ?? undefined
}
// get link to trade on v1, if a better rate exists
const v1Trade = useV1Trade(isExactIn, tokens[Field.INPUT], tokens[Field.OUTPUT], parsedAmount)
const v1Trade = useV1Trade(isExactIn, currencies[Field.INPUT], currencies[Field.OUTPUT], parsedAmount)
let error: string | undefined
if (!account) {
@ -147,7 +149,7 @@ export function useDerivedSwapInfo(): {
error = error ?? 'Enter an amount'
}
if (!tokens[Field.INPUT] || !tokens[Field.OUTPUT]) {
if (!currencies[Field.INPUT] || !currencies[Field.OUTPUT]) {
error = error ?? 'Select a token'
}
@ -162,9 +164,9 @@ export function useDerivedSwapInfo(): {
const slippageAdjustedAmountsV1 =
v1Trade && allowedSlippage && computeSlippageAdjustedAmounts(v1Trade, allowedSlippage)
// compare input balance to MAx input based on version
// compare input balance to max input based on version
const [balanceIn, amountIn] = [
tokenBalances[Field.INPUT],
currencyBalances[Field.INPUT],
toggledVersion === Version.v1
? slippageAdjustedAmountsV1
? slippageAdjustedAmountsV1[Field.INPUT]
@ -175,12 +177,12 @@ export function useDerivedSwapInfo(): {
]
if (balanceIn && amountIn && balanceIn.lessThan(amountIn)) {
error = 'Insufficient ' + amountIn.token.symbol + ' balance'
error = 'Insufficient ' + amountIn.currency.symbol + ' balance'
}
return {
tokens,
tokenBalances,
currencies,
currencyBalances,
parsedAmount,
v2Trade: v2Trade ?? undefined,
error,
@ -188,14 +190,14 @@ export function useDerivedSwapInfo(): {
}
}
function parseCurrencyFromURLParameter(urlParam: any, chainId: number): string {
function parseCurrencyFromURLParameter(urlParam: any): string {
if (typeof urlParam === 'string') {
const valid = isAddress(urlParam)
if (valid) return valid
if (urlParam.toLowerCase() === 'eth') return WETH[chainId as ChainId]?.address ?? ''
if (valid === false) return WETH[chainId as ChainId]?.address ?? ''
if (urlParam.toUpperCase() === 'ETH') return 'ETH'
if (valid === false) return 'ETH'
}
return WETH[chainId as ChainId]?.address ?? ''
return 'ETH' ?? ''
}
function parseTokenAmountURLParameter(urlParam: any): string {
@ -217,9 +219,9 @@ function validatedRecipient(recipient: any): string | null {
return null
}
export function queryParametersToSwapState(parsedQs: ParsedQs, chainId: ChainId): SwapState {
let inputCurrency = parseCurrencyFromURLParameter(parsedQs.inputCurrency, chainId)
let outputCurrency = parseCurrencyFromURLParameter(parsedQs.outputCurrency, chainId)
export function queryParametersToSwapState(parsedQs: ParsedQs): SwapState {
let inputCurrency = parseCurrencyFromURLParameter(parsedQs.inputCurrency)
let outputCurrency = parseCurrencyFromURLParameter(parsedQs.outputCurrency)
if (inputCurrency === outputCurrency) {
if (typeof parsedQs.outputCurrency === 'string') {
inputCurrency = ''
@ -232,10 +234,10 @@ export function queryParametersToSwapState(parsedQs: ParsedQs, chainId: ChainId)
return {
[Field.INPUT]: {
address: inputCurrency
currencyId: inputCurrency
},
[Field.OUTPUT]: {
address: outputCurrency
currencyId: outputCurrency
},
typedValue: parseTokenAmountURLParameter(parsedQs.exactAmount),
independentField: parseIndependentFieldURLParameter(parsedQs.exactField),
@ -251,14 +253,14 @@ export function useDefaultsFromURLSearch() {
useEffect(() => {
if (!chainId) return
const parsed = queryParametersToSwapState(parsedQs, chainId)
const parsed = queryParametersToSwapState(parsedQs)
dispatch(
replaceSwapState({
typedValue: parsed.typedValue,
field: parsed.independentField,
inputTokenAddress: parsed[Field.INPUT].address,
outputTokenAddress: parsed[Field.OUTPUT].address,
inputCurrencyId: parsed[Field.INPUT].currencyId,
outputCurrencyId: parsed[Field.OUTPUT].currencyId,
recipient: parsed.recipient
})
)

@ -1,5 +1,5 @@
import { createStore, Store } from 'redux'
import { Field, selectToken } from './actions'
import { Field, selectCurrency } from './actions'
import reducer, { SwapState } from './reducer'
describe('swap reducer', () => {
@ -7,27 +7,29 @@ describe('swap reducer', () => {
beforeEach(() => {
store = createStore(reducer, {
[Field.OUTPUT]: { address: '' },
[Field.INPUT]: { address: '' },
[Field.OUTPUT]: { currencyId: '' },
[Field.INPUT]: { currencyId: '' },
typedValue: '',
independentField: Field.INPUT
independentField: Field.INPUT,
recipient: null
})
})
describe('selectToken', () => {
it('changes token', () => {
store.dispatch(
selectToken({
selectCurrency({
field: Field.OUTPUT,
address: '0x0000'
currencyId: '0x0000'
})
)
expect(store.getState()).toEqual({
[Field.OUTPUT]: { address: '0x0000' },
[Field.INPUT]: { address: '' },
[Field.OUTPUT]: { currencyId: '0x0000' },
[Field.INPUT]: { currencyId: '' },
typedValue: '',
independentField: Field.INPUT
independentField: Field.INPUT,
recipient: null
})
})
})

@ -1,16 +1,16 @@
import { createReducer } from '@reduxjs/toolkit'
import { Field, replaceSwapState, selectToken, setRecipient, switchTokens, typeInput } from './actions'
import { Field, replaceSwapState, selectCurrency, setRecipient, switchCurrencies, typeInput } from './actions'
export interface SwapState {
readonly independentField: Field
readonly typedValue: string
readonly [Field.INPUT]: {
readonly address: string | undefined
readonly currencyId: string | undefined
}
readonly [Field.OUTPUT]: {
readonly address: string | undefined
readonly currencyId: string | undefined
}
// the typed recipient address, or null if swap should go to sender
// the typed recipient address or ENS name, or null if swap should go to sender
readonly recipient: string | null
}
@ -18,10 +18,10 @@ const initialState: SwapState = {
independentField: Field.INPUT,
typedValue: '',
[Field.INPUT]: {
address: ''
currencyId: ''
},
[Field.OUTPUT]: {
address: ''
currencyId: ''
},
recipient: null
}
@ -30,13 +30,13 @@ export default createReducer<SwapState>(initialState, builder =>
builder
.addCase(
replaceSwapState,
(state, { payload: { typedValue, recipient, field, inputTokenAddress, outputTokenAddress } }) => {
(state, { payload: { typedValue, recipient, field, inputCurrencyId, outputCurrencyId } }) => {
return {
[Field.INPUT]: {
address: inputTokenAddress
currencyId: inputCurrencyId
},
[Field.OUTPUT]: {
address: outputTokenAddress
currencyId: outputCurrencyId
},
independentField: field,
typedValue: typedValue,
@ -44,30 +44,30 @@ export default createReducer<SwapState>(initialState, builder =>
}
}
)
.addCase(selectToken, (state, { payload: { address, field } }) => {
.addCase(selectCurrency, (state, { payload: { currencyId, field } }) => {
const otherField = field === Field.INPUT ? Field.OUTPUT : Field.INPUT
if (address === state[otherField].address) {
if (currencyId === state[otherField].currencyId) {
// the case where we have to swap the order
return {
...state,
independentField: state.independentField === Field.INPUT ? Field.OUTPUT : Field.INPUT,
[field]: { address },
[otherField]: { address: state[field].address }
[field]: { currencyId: currencyId },
[otherField]: { currencyId: state[field].currencyId }
}
} else {
// the normal case
return {
...state,
[field]: { address }
[field]: { currencyId: currencyId }
}
}
})
.addCase(switchTokens, state => {
.addCase(switchCurrencies, state => {
return {
...state,
independentField: state.independentField === Field.INPUT ? Field.OUTPUT : Field.INPUT,
[Field.INPUT]: { address: state[Field.OUTPUT].address },
[Field.OUTPUT]: { address: state[Field.INPUT].address }
[Field.INPUT]: { currencyId: state[Field.OUTPUT].currencyId },
[Field.OUTPUT]: { currencyId: state[Field.INPUT].currencyId }
}
})
.addCase(typeInput, (state, { payload: { field, typedValue } }) => {

@ -1,7 +1,8 @@
import { ChainId, JSBI, Pair, Token, TokenAmount } from '@uniswap/sdk'
import { ChainId, Pair, Token } from '@uniswap/sdk'
import flatMap from 'lodash.flatmap'
import { useCallback, useMemo } from 'react'
import { shallowEqual, useDispatch, useSelector } from 'react-redux'
import { BASES_TO_TRACK_LIQUIDITY_FOR, PINNED_PAIRS } from '../../constants'
import { useActiveWeb3React } from '../../hooks'
import { useAllTokens } from '../../hooks/Tokens'
@ -14,11 +15,10 @@ import {
SerializedPair,
SerializedToken,
updateUserDarkMode,
updateUserDeadline,
updateUserExpertMode,
updateUserSlippageTolerance,
updateUserDeadline
updateUserSlippageTolerance
} from './actions'
import { BASES_TO_TRACK_LIQUIDITY_FOR, DUMMY_PAIRS_TO_PIN } from '../../constants'
function serializeToken(token: Token): SerializedToken {
return {
@ -67,8 +67,7 @@ export function useDarkModeManager(): [boolean, () => void] {
}
export function useIsExpertMode(): boolean {
const userExpertMode = useSelector<AppState, AppState['user']['userExpertMode']>(state => state.user.userExpertMode)
return userExpertMode
return useSelector<AppState, AppState['user']['userExpertMode']>(state => state.user.userExpertMode)
}
export function useExpertModeManager(): [boolean, () => void] {
@ -144,8 +143,6 @@ export function useUserAddedTokens(): Token[] {
}, [serializedTokensMap, chainId])
}
const ZERO = JSBI.BigInt(0)
function serializePair(pair: Pair): SerializedPair {
return {
token0: serializeToken(pair.token0),
@ -186,68 +183,79 @@ export function useTokenWarningDismissal(chainId?: number, token?: Token): [bool
}, [chainId, token, dismissalState, dispatch])
}
export function useAllDummyPairs(): Pair[] {
/**
* Given two tokens return the liquidity token that represents its liquidity shares
* @param tokenA one of the two tokens
* @param tokenB the other token
*/
export function toV2LiquidityToken([tokenA, tokenB]: [Token, Token]): Token {
return new Token(tokenA.chainId, Pair.getAddress(tokenA, tokenB), 18, 'UNI-V2', 'Uniswap V2')
}
/**
* Returns all the pairs of tokens that are tracked by the user for the current chain ID.
*/
export function useTrackedTokenPairs(): [Token, Token][] {
const { chainId } = useActiveWeb3React()
const tokens = useAllTokens()
// pinned pairs
const pinnedPairs = useMemo(() => DUMMY_PAIRS_TO_PIN[chainId as ChainId] ?? [], [chainId])
const pinnedPairs = useMemo(() => (chainId ? PINNED_PAIRS[chainId] ?? [] : []), [chainId])
// pairs for every token against every base
const generatedPairs: Pair[] = useMemo(
const generatedPairs: [Token, Token][] = useMemo(
() =>
flatMap(
Object.values(tokens)
// select only tokens on the current chain
.filter(token => token.chainId === chainId),
token => {
chainId
? flatMap(Object.keys(tokens), tokenAddress => {
const token = tokens[tokenAddress]
// for each token on the current chain,
return (
// loop though all bases on the current chain
(BASES_TO_TRACK_LIQUIDITY_FOR[chainId as ChainId] ?? [])
(BASES_TO_TRACK_LIQUIDITY_FOR[chainId] ?? [])
// to construct pairs of the given token with each base
.map(base => {
if (base.equals(token)) {
return null
} else {
return new Pair(new TokenAmount(base, ZERO), new TokenAmount(token, ZERO))
return [base, token]
}
})
.filter(pair => !!pair) as Pair[]
.filter((p): p is [Token, Token] => p !== null)
)
}
),
})
: [],
[tokens, chainId]
)
// pairs saved by users
const savedSerializedPairs = useSelector<AppState, AppState['user']['pairs']>(({ user: { pairs } }) => pairs)
const userPairs: Pair[] = useMemo(
() =>
Object.values<SerializedPair>(savedSerializedPairs[chainId ?? -1] ?? {}).map(
pair =>
new Pair(
new TokenAmount(deserializeToken(pair.token0), ZERO),
new TokenAmount(deserializeToken(pair.token1), ZERO)
)
),
[savedSerializedPairs, chainId]
)
const userPairs: [Token, Token][] = useMemo(() => {
if (!chainId || !savedSerializedPairs) return []
const forChain = savedSerializedPairs[chainId]
if (!forChain) return []
return Object.keys(forChain).map(pairId => {
return [deserializeToken(forChain[pairId].token0), deserializeToken(forChain[pairId].token1)]
})
}, [savedSerializedPairs, chainId])
const combinedList = useMemo(() => userPairs.concat(generatedPairs).concat(pinnedPairs), [
generatedPairs,
pinnedPairs,
userPairs
])
return useMemo(() => {
const cache: { [pairKey: string]: boolean } = {}
return (
pinnedPairs
.concat(generatedPairs)
.concat(userPairs)
// filter out duplicate pairs
.filter(pair => {
const pairKey = `${pair.token0.address}:${pair.token1.address}`
if (cache[pairKey]) {
return false
}
return (cache[pairKey] = true)
})
)
}, [pinnedPairs, generatedPairs, userPairs])
// dedupes pairs of tokens in the combined list
const keyed = combinedList.reduce<{ [key: string]: [Token, Token] }>((memo, [tokenA, tokenB]) => {
const sorted = tokenA.sortsBefore(tokenB)
const key = sorted ? `${tokenA.address}:${tokenB.address}` : `${tokenB.address}:${tokenA.address}`
if (memo[key]) return memo
memo[key] = sorted ? [tokenA, tokenB] : [tokenB, tokenA]
return memo
}, {})
return Object.keys(keyed).map(key => keyed[key])
}, [combinedList])
}

@ -1,4 +1,4 @@
import { ChainId, JSBI, Token, TokenAmount, WETH } from '@uniswap/sdk'
import { Currency, CurrencyAmount, ETHER, JSBI, Token, TokenAmount } from '@uniswap/sdk'
import { useMemo } from 'react'
import ERC20_INTERFACE from '../../constants/abis/erc20'
import { useAllTokens } from '../../hooks/Tokens'
@ -10,7 +10,9 @@ import { useSingleContractMultipleData, useMultipleContractSingleData } from '..
/**
* Returns a map of the given addresses to their eventually consistent ETH balances.
*/
export function useETHBalances(uncheckedAddresses?: (string | undefined)[]): { [address: string]: JSBI | undefined } {
export function useETHBalances(
uncheckedAddresses?: (string | undefined)[]
): { [address: string]: CurrencyAmount | undefined } {
const multicallContract = useMulticallContract()
const addresses: string[] = useMemo(
@ -32,9 +34,9 @@ export function useETHBalances(uncheckedAddresses?: (string | undefined)[]): { [
return useMemo(
() =>
addresses.reduce<{ [address: string]: JSBI | undefined }>((memo, address, i) => {
addresses.reduce<{ [address: string]: CurrencyAmount }>((memo, address, i) => {
const value = results?.[i]?.result?.[0]
if (value) memo[address] = JSBI.BigInt(value.toString())
if (value) memo[address] = CurrencyAmount.ether(JSBI.BigInt(value.toString()))
return memo
}, {}),
[addresses, results]
@ -57,7 +59,7 @@ export function useTokenBalancesWithLoadingIndicator(
const balances = useMultipleContractSingleData(validatedTokenAddresses, ERC20_INTERFACE, 'balanceOf', [address])
const anyLoading = balances.some(callState => callState.loading)
const anyLoading: boolean = useMemo(() => balances.some(callState => callState.loading), [balances])
return [
useMemo(
@ -85,45 +87,6 @@ export function useTokenBalances(
return useTokenBalancesWithLoadingIndicator(address, tokens)[0]
}
// contains the hacky logic to treat the WETH token input as if it's ETH to
// maintain compatibility until we handle them separately.
export function useTokenBalancesTreatWETHAsETH(
address?: string,
tokens?: (Token | undefined)[]
): { [tokenAddress: string]: TokenAmount | undefined } {
const { chainId } = useActiveWeb3React()
const { tokensWithoutWETH, includesWETH } = useMemo(() => {
if (!tokens || tokens.length === 0) {
return { includesWETH: false, tokensWithoutWETH: [] }
}
let includesWETH = false
const tokensWithoutWETH = tokens.filter(t => {
if (!chainId) return true
const isWETH = t?.equals(WETH[chainId as ChainId]) ?? false
if (isWETH) includesWETH = true
return !isWETH
})
return { includesWETH, tokensWithoutWETH }
}, [tokens, chainId])
const balancesWithoutWETH = useTokenBalances(address, tokensWithoutWETH)
const ETHBalance = useETHBalances(includesWETH ? [address] : [])
return useMemo(() => {
if (!chainId || !address) return {}
if (includesWETH) {
const weth = WETH[chainId as ChainId]
const ethBalance = ETHBalance[address]
return {
...balancesWithoutWETH,
...(ethBalance && weth ? { [weth.address]: new TokenAmount(weth, ethBalance) } : null)
}
} else {
return balancesWithoutWETH
}
}, [balancesWithoutWETH, ETHBalance, includesWETH, address, chainId])
}
// get the balance for a single token/account combo
export function useTokenBalance(account?: string, token?: Token): TokenAmount | undefined {
const tokenBalances = useTokenBalances(account, [token])
@ -131,18 +94,39 @@ export function useTokenBalance(account?: string, token?: Token): TokenAmount |
return tokenBalances[token.address]
}
// mimics the behavior of useAddressBalance
export function useTokenBalanceTreatingWETHasETH(account?: string, token?: Token): TokenAmount | undefined {
const balances = useTokenBalancesTreatWETHAsETH(account, [token])
if (!token) return
return balances?.[token.address]
export function useCurrencyBalances(
account?: string,
currencies?: (Currency | undefined)[]
): (CurrencyAmount | undefined)[] {
const tokens = useMemo(() => currencies?.filter((currency): currency is Token => currency instanceof Token) ?? [], [
currencies
])
const tokenBalances = useTokenBalances(account, tokens)
const containsETH: boolean = useMemo(() => currencies?.some(currency => currency === ETHER) ?? false, [currencies])
const ethBalance = useETHBalances(containsETH ? [account] : [])
return useMemo(
() =>
currencies?.map(currency => {
if (!account || !currency) return
if (currency instanceof Token) return tokenBalances[currency.address]
if (currency === ETHER) return ethBalance[account]
return
}) ?? [],
[account, currencies, ethBalance, tokenBalances]
)
}
export function useCurrencyBalance(account?: string, currency?: Currency): CurrencyAmount | undefined {
return useCurrencyBalances(account, [currency])[0]
}
// mimics useAllBalances
export function useAllTokenBalancesTreatingWETHasETH(): { [tokenAddress: string]: TokenAmount | undefined } {
export function useAllTokenBalances(): { [tokenAddress: string]: TokenAmount | undefined } {
const { account } = useActiveWeb3React()
const allTokens = useAllTokens()
const allTokensArray = useMemo(() => Object.values(allTokens ?? {}), [allTokens])
const balances = useTokenBalancesTreatWETHAsETH(account ?? undefined, allTokensArray)
const balances = useTokenBalances(account ?? undefined, allTokensArray)
return balances ?? {}
}

7
src/utils/currencyId.ts Normal file

@ -0,0 +1,7 @@
import { Currency, ETHER, Token } from '@uniswap/sdk'
export function currencyId(currency: Currency): string {
if (currency === ETHER) return 'ETH'
if (currency instanceof Token) return currency.address
throw new Error('invalid currency')
}

@ -6,7 +6,7 @@ import { BigNumber } from '@ethersproject/bignumber'
import { abi as IUniswapV2Router02ABI } from '@uniswap/v2-periphery/build/IUniswapV2Router02.json'
import { ROUTER_ADDRESS } from '../constants'
import { ALL_TOKENS } from '../constants/tokens'
import { ChainId, JSBI, Percent, TokenAmount, Token } from '@uniswap/sdk'
import { ChainId, JSBI, Percent, Token, CurrencyAmount, Currency, ETHER } from '@uniswap/sdk'
// returns the checksummed address if the address is valid, otherwise returns false
export function isAddress(value: any): string | false {
@ -61,7 +61,7 @@ export function basisPointsToPercent(num: number): Percent {
return new Percent(JSBI.BigInt(num), JSBI.BigInt(10000))
}
export function calculateSlippageAmount(value: TokenAmount, slippage: number): [JSBI, JSBI] {
export function calculateSlippageAmount(value: CurrencyAmount, slippage: number): [JSBI, JSBI] {
if (slippage < 0 || slippage > 10000) {
throw Error(`Unexpected slippage value: ${slippage}`)
}
@ -99,11 +99,12 @@ export function escapeRegExp(string: string): string {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string
}
export function isDefaultToken(token?: Token): boolean {
return Boolean(token && ALL_TOKENS[token.chainId]?.[token.address])
export function isDefaultToken(currency?: Currency): boolean {
if (currency === ETHER) return true
return Boolean(currency instanceof Token && ALL_TOKENS[currency.chainId]?.[currency.address])
}
export function isCustomAddedToken(allTokens: { [address: string]: Token }, token?: Token): boolean {
const isDefault = isDefaultToken(token)
return Boolean(token && allTokens[token.address] && !isDefault)
export function isCustomAddedToken(allTokens: { [address: string]: Token }, currency?: Currency): boolean {
const isDefault = isDefaultToken(currency)
return Boolean(!isDefault && currency instanceof Token && allTokens[currency.address])
}

@ -1,18 +1,18 @@
import { JSBI, TokenAmount, WETH } from '@uniswap/sdk'
import { CurrencyAmount, ETHER, JSBI } from '@uniswap/sdk'
import { MIN_ETH } from '../constants'
/**
* Given some token amount, return the max that can be spent of it
* @param tokenAmount to return max of
* @param currencyAmount to return max of
*/
export function maxAmountSpend(tokenAmount?: TokenAmount): TokenAmount | undefined {
if (!tokenAmount) return
if (tokenAmount.token.equals(WETH[tokenAmount.token.chainId])) {
if (JSBI.greaterThan(tokenAmount.raw, MIN_ETH)) {
return new TokenAmount(tokenAmount.token, JSBI.subtract(tokenAmount.raw, MIN_ETH))
export function maxAmountSpend(currencyAmount?: CurrencyAmount): CurrencyAmount | undefined {
if (!currencyAmount) return
if (currencyAmount.currency === ETHER) {
if (JSBI.greaterThan(currencyAmount.raw, MIN_ETH)) {
return CurrencyAmount.ether(JSBI.subtract(currencyAmount.raw, MIN_ETH))
} else {
return new TokenAmount(tokenAmount.token, JSBI.BigInt(0))
return CurrencyAmount.ether(JSBI.BigInt(0))
}
}
return tokenAmount
return currencyAmount
}

@ -1,5 +1,5 @@
import { BLOCKED_PRICE_IMPACT_NON_EXPERT } from '../constants'
import { Fraction, JSBI, Percent, TokenAmount, Trade } from '@uniswap/sdk'
import { CurrencyAmount, Fraction, JSBI, Percent, TokenAmount, Trade } from '@uniswap/sdk'
import { ALLOWED_PRICE_IMPACT_HIGH, ALLOWED_PRICE_IMPACT_LOW, ALLOWED_PRICE_IMPACT_MEDIUM } from '../constants'
import { Field } from '../state/swap/actions'
import { basisPointsToPercent } from './index'
@ -11,7 +11,7 @@ const INPUT_FRACTION_AFTER_FEE = ONE_HUNDRED_PERCENT.subtract(BASE_FEE)
// computes price breakdown for the trade
export function computeTradePriceBreakdown(
trade?: Trade
): { priceImpactWithoutFee?: Percent; realizedLPFee?: TokenAmount } {
): { priceImpactWithoutFee?: Percent; realizedLPFee?: CurrencyAmount } {
// for each hop in our trade, take away the x*y=k price impact from 0.3% fees
// e.g. for 3 tokens/2 hops: 1 - ((1 - .03) * (1-.03))
const realizedLPFee = !trade
@ -24,7 +24,7 @@ export function computeTradePriceBreakdown(
)
// remove lp fees from price impact
const priceImpactWithoutFeeFraction = trade && realizedLPFee ? trade.slippage.subtract(realizedLPFee) : undefined
const priceImpactWithoutFeeFraction = trade && realizedLPFee ? trade.priceImpact.subtract(realizedLPFee) : undefined
// the x*y=k impact
const priceImpactWithoutFeePercent = priceImpactWithoutFeeFraction
@ -35,7 +35,9 @@ export function computeTradePriceBreakdown(
const realizedLPFeeAmount =
realizedLPFee &&
trade &&
new TokenAmount(trade.inputAmount.token, realizedLPFee.multiply(trade.inputAmount.raw).quotient)
(trade.inputAmount instanceof TokenAmount
? new TokenAmount(trade.inputAmount.token, realizedLPFee.multiply(trade.inputAmount.raw).quotient)
: CurrencyAmount.ether(realizedLPFee.multiply(trade.inputAmount.raw).quotient))
return { priceImpactWithoutFee: priceImpactWithoutFeePercent, realizedLPFee: realizedLPFeeAmount }
}
@ -44,7 +46,7 @@ export function computeTradePriceBreakdown(
export function computeSlippageAdjustedAmounts(
trade: Trade | undefined,
allowedSlippage: number
): { [field in Field]?: TokenAmount } {
): { [field in Field]?: CurrencyAmount } {
const pct = basisPointsToPercent(allowedSlippage)
return {
[Field.INPUT]: trade?.maximumAmountIn(pct),
@ -65,8 +67,10 @@ export function formatExecutionPrice(trade?: Trade, inverted?: boolean): string
return ''
}
return inverted
? `${trade.executionPrice.invert().toSignificant(6)} ${trade.inputAmount.token.symbol} / ${
trade.outputAmount.token.symbol
? `${trade.executionPrice.invert().toSignificant(6)} ${trade.inputAmount.currency.symbol} / ${
trade.outputAmount.currency.symbol
}`
: `${trade.executionPrice.toSignificant(6)} ${trade.outputAmount.currency.symbol} / ${
trade.inputAmount.currency.symbol
}`
: `${trade.executionPrice.toSignificant(6)} ${trade.outputAmount.token.symbol} / ${trade.inputAmount.token.symbol}`
}

@ -0,0 +1,106 @@
import { CurrencyAmount, ETHER, Percent, Route, TokenAmount, Trade } from '@uniswap/sdk'
import { DAI, USDC } from '../constants/tokens/mainnet'
import { MockV1Pair } from '../data/V1'
import v1SwapArguments from './v1SwapArguments'
describe('v1SwapArguments', () => {
const USDC_WETH = new MockV1Pair('1000000', new TokenAmount(USDC, '1000000'))
const DAI_WETH = new MockV1Pair('1000000', new TokenAmount(DAI, '1000000'))
// just some random address
const TEST_RECIPIENT_ADDRESS = USDC_WETH.liquidityToken.address
function checkDeadline(hex: string | string[], ttl: number) {
if (typeof hex !== 'string') throw new Error('invalid hex')
const now = new Date().getTime() / 1000
expect(parseInt(hex) - now).toBeGreaterThanOrEqual(ttl - 3)
expect(parseInt(hex) - now).toBeLessThanOrEqual(ttl + 3)
}
it('exact eth to token', () => {
const trade = Trade.exactIn(new Route([USDC_WETH], ETHER), CurrencyAmount.ether('100'))
const result = v1SwapArguments(trade, {
recipient: TEST_RECIPIENT_ADDRESS,
allowedSlippage: new Percent('1', '100'),
ttl: 20 * 60
})
expect(result.methodName).toEqual('ethToTokenTransferInput')
expect(result.args[0]).toEqual('0x62')
expect(result.args[2]).toEqual(TEST_RECIPIENT_ADDRESS)
checkDeadline(result.args[1], 20 * 60)
expect(result.value).toEqual('0x64')
})
it('exact token to eth', () => {
const trade = Trade.exactIn(new Route([USDC_WETH], USDC, ETHER), new TokenAmount(USDC, '100'))
const result = v1SwapArguments(trade, {
recipient: TEST_RECIPIENT_ADDRESS,
allowedSlippage: new Percent('1', '100'),
ttl: 20 * 60
})
expect(result.methodName).toEqual('tokenToEthTransferInput')
expect(result.args[0]).toEqual('0x64')
expect(result.args[1]).toEqual('0x62')
checkDeadline(result.args[2], 20 * 60)
expect(result.args[3]).toEqual(TEST_RECIPIENT_ADDRESS)
expect(result.value).toEqual('0x0')
})
it('exact token to token', () => {
const trade = Trade.exactIn(new Route([USDC_WETH, DAI_WETH], USDC), new TokenAmount(USDC, '100'))
const result = v1SwapArguments(trade, {
recipient: TEST_RECIPIENT_ADDRESS,
allowedSlippage: new Percent('1', '100'),
ttl: 20 * 60
})
expect(result.methodName).toEqual('tokenToTokenTransferInput')
expect(result.args[0]).toEqual('0x64')
expect(result.args[1]).toEqual('0x61')
expect(result.args[2]).toEqual('0x1')
expect(result.args[4]).toEqual(TEST_RECIPIENT_ADDRESS)
expect(result.args[5]).toEqual(DAI.address)
checkDeadline(result.args[3], 20 * 60)
expect(result.value).toEqual('0x0')
})
it('eth to exact token', () => {
const trade = Trade.exactOut(new Route([USDC_WETH], ETHER), new TokenAmount(USDC, '100'))
const result = v1SwapArguments(trade, {
recipient: TEST_RECIPIENT_ADDRESS,
allowedSlippage: new Percent('1', '100'),
ttl: 20 * 60
})
expect(result.methodName).toEqual('ethToTokenTransferOutput')
expect(result.args[0]).toEqual('0x64')
checkDeadline(result.args[1], 20 * 60)
expect(result.args[2]).toEqual(TEST_RECIPIENT_ADDRESS)
expect(result.value).toEqual('0x66')
})
it('token to exact eth', () => {
const trade = Trade.exactOut(new Route([USDC_WETH], USDC, ETHER), CurrencyAmount.ether('100'))
const result = v1SwapArguments(trade, {
recipient: TEST_RECIPIENT_ADDRESS,
allowedSlippage: new Percent('1', '100'),
ttl: 20 * 60
})
expect(result.methodName).toEqual('tokenToEthTransferOutput')
expect(result.args[0]).toEqual('0x64')
expect(result.args[1]).toEqual('0x66')
checkDeadline(result.args[2], 20 * 60)
expect(result.args[3]).toEqual(TEST_RECIPIENT_ADDRESS)
expect(result.value).toEqual('0x0')
})
it('token to exact token', () => {
const trade = Trade.exactOut(new Route([USDC_WETH, DAI_WETH], USDC), new TokenAmount(DAI, '100'))
const result = v1SwapArguments(trade, {
recipient: TEST_RECIPIENT_ADDRESS,
allowedSlippage: new Percent('1', '100'),
ttl: 20 * 60
})
expect(result.methodName).toEqual('tokenToTokenTransferOutput')
expect(result.args[0]).toEqual('0x64')
expect(result.args[1]).toEqual('0x67')
expect(result.args[2]).toEqual(`0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff`)
checkDeadline(result.args[3], 20 * 60)
expect(result.args[4]).toEqual(TEST_RECIPIENT_ADDRESS)
expect(result.args[5]).toEqual(DAI.address)
expect(result.value).toEqual('0x0')
})
})

@ -0,0 +1,91 @@
import { MaxUint256 } from '@ethersproject/constants'
import { CurrencyAmount, ETHER, SwapParameters, Token, Trade, TradeOptions, TradeType } from '@uniswap/sdk'
import { getTradeVersion } from '../data/V1'
import { Version } from '../hooks/useToggledVersion'
function toHex(currencyAmount: CurrencyAmount): string {
return `0x${currencyAmount.raw.toString(16)}`
}
function deadlineFromNow(ttl: number): string {
return `0x${(Math.floor(new Date().getTime() / 1000) + ttl).toString(16)}`
}
/**
* Get the arguments to make for a swap
* @param trade trade to get v1 arguments for swapping
* @param options options for swapping
*/
export default function v1SwapArguments(trade: Trade, options: Omit<TradeOptions, 'feeOnTransfer'>): SwapParameters {
if (getTradeVersion(trade) !== Version.v1) {
throw new Error('invalid trade version')
}
if (trade.route.pairs.length > 2) {
throw new Error('too many pairs')
}
const isExactIn = trade.tradeType === TradeType.EXACT_INPUT
const inputETH = trade.inputAmount.currency === ETHER
const outputETH = trade.outputAmount.currency === ETHER
if (inputETH && outputETH) throw new Error('ETHER to ETHER')
const minimumAmountOut = toHex(trade.minimumAmountOut(options.allowedSlippage))
const maximumAmountIn = toHex(trade.maximumAmountIn(options.allowedSlippage))
const deadline = deadlineFromNow(options.ttl)
if (isExactIn) {
if (inputETH) {
return {
methodName: 'ethToTokenTransferInput',
args: [minimumAmountOut, deadline, options.recipient],
value: maximumAmountIn
}
} else if (outputETH) {
return {
methodName: 'tokenToEthTransferInput',
args: [maximumAmountIn, minimumAmountOut, deadline, options.recipient],
value: '0x0'
}
} else {
const outputToken = trade.outputAmount.currency
// should never happen, needed for type check
if (!(outputToken instanceof Token)) {
throw new Error('token to token')
}
return {
methodName: 'tokenToTokenTransferInput',
args: [maximumAmountIn, minimumAmountOut, '0x1', deadline, options.recipient, outputToken.address],
value: '0x0'
}
}
} else {
if (inputETH) {
return {
methodName: 'ethToTokenTransferOutput',
args: [minimumAmountOut, deadline, options.recipient],
value: maximumAmountIn
}
} else if (outputETH) {
return {
methodName: 'tokenToEthTransferOutput',
args: [minimumAmountOut, maximumAmountIn, deadline, options.recipient],
value: '0x0'
}
} else {
const output = trade.outputAmount.currency
if (!(output instanceof Token)) {
throw new Error('invalid output amount currency')
}
return {
methodName: 'tokenToTokenTransferOutput',
args: [
minimumAmountOut,
maximumAmountIn,
MaxUint256.toHexString(),
deadline,
options.recipient,
output.address
],
value: '0x0'
}
}
}
}

@ -0,0 +1,18 @@
import { ChainId, Currency, CurrencyAmount, ETHER, Token, TokenAmount, WETH } from '@uniswap/sdk'
export function wrappedCurrency(currency: Currency | undefined, chainId: ChainId | undefined): Token | undefined {
return chainId && currency === ETHER ? WETH[chainId] : currency instanceof Token ? currency : undefined
}
export function wrappedCurrencyAmount(
currencyAmount: CurrencyAmount | undefined,
chainId: ChainId | undefined
): TokenAmount | undefined {
const token = currencyAmount && chainId ? wrappedCurrency(currencyAmount.currency, chainId) : undefined
return token && currencyAmount ? new TokenAmount(token, currencyAmount.raw) : undefined
}
export function unwrappedToken(token: Token): Currency {
if (token.equals(WETH[token.chainId])) return ETHER
return token
}

3489
yarn.lock

File diff suppressed because it is too large Load Diff