stable state for swaps, add, and remove eth (no remove token token yet)

This commit is contained in:
ianlapham 2020-03-04 13:48:43 -05:00
parent 753e5f3423
commit 236c3030e1
24 changed files with 3606 additions and 2380 deletions

@ -12,7 +12,7 @@
"@types/node": "^13.7.4",
"@types/react": "^16.9.21",
"@types/react-dom": "^16.9.5",
"@uniswap/sdk": "next",
"@uniswap/sdk": "@uniswap/sdk@2.0.0-beta.17",
"@web3-react/core": "^6.0.2",
"@web3-react/fortmatic-connector": "^6.0.2",
"@web3-react/injected-connector": "^6.0.3",

@ -1,16 +1,24 @@
import React, { useState } from 'react'
import styled from 'styled-components'
import { ButtonPrimary } from '../../components/Button'
import { ButtonPrimary } from '../Button'
import { AutoColumn, ColumnCenter } from '../Column'
import Row, { RowBetween, RowFlat, RowFixed } from '../Row'
import { ArrowDown } from 'react-feather'
import { Text } from 'rebass'
import { LightCard } from '../Card'
import Modal from '../Modal'
import { CheckCircle } from 'react-feather'
import DoubleTokenLogo from '../DoubleLogo'
import TokenLogo from '../TokenLogo'
import { CloseIcon } from '../../theme/components'
import Loader from '../Loader'
import { Link } from '../../theme'
import { useWeb3React } from '../../hooks'
import { getEtherscanLink } from '../../utils'
import { TRANSACTION_TYPE } from '../../constants'
const Wrapper = styled.div`
width: 100%;
@ -30,141 +38,200 @@ const ConfirmedIcon = styled(ColumnCenter)`
export default function ConfirmationModal({
isOpen,
onDismiss,
liquidityMinted,
liquidityAmount = undefined,
poolTokenPercentage = undefined,
amount0,
amount1,
poolTokenPercentage,
price
price,
transactionType,
pendingConfirmation,
hash,
contractCall
}) {
const { address: address0, symbol: symbol0 } = amount0?.token || {}
const { address: address1, symbol: symbol1 } = amount1?.token || {}
const [confirmed, SetConfirmed] = useState(false)
const [waitingForConfirmation, setWaitingForConfirmation] = useState(false)
const { chainId } = useWeb3React()
const [confirmed, setConfirmed] = useState(false)
function WrappedOnDismissed() {
onDismiss()
SetConfirmed(false)
}
function fakeCall() {
setTimeout(() => {
setWaitingForConfirmation(false)
}, 2000)
setConfirmed(false)
}
return (
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={100}>
<Modal isOpen={isOpen} onDismiss={onDismiss}>
{!confirmed ? (
<Wrapper>
<Section gap="40px">
<RowBetween>
<Text fontWeight={500} fontSize={'20px'}>
You will receive
{transactionType === TRANSACTION_TYPE.SWAP ? 'Confirm Swap' : 'You will receive'}
</Text>
<CloseIcon onClick={WrappedOnDismissed} />
</RowBetween>
<AutoColumn gap="16px">
<RowFlat>
<Text fontSize="48px" fontWeight={500} lineHeight="32px" marginRight={10}>
{liquidityMinted?.toFixed(6)}
</Text>
<DoubleTokenLogo a0={address0 || ''} a1={address1 || ''} size={20} />
</RowFlat>
<Row>
<Text fontSize="24px">{symbol0 + ':' + symbol1 + ' Pool Tokens'}</Text>
</Row>
</AutoColumn>
{transactionType === TRANSACTION_TYPE.SWAP && (
<AutoColumn gap={'20px'}>
<LightCard>
<RowBetween>
<Text fontSize={24} fontWeight={500}>
{amount0?.toSignificant(6)}
</Text>
<RowFixed gap="10px">
<TokenLogo address={amount0?.token?.address} size={'24px'} />
<Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}>
{symbol0}
</Text>
</RowFixed>
</RowBetween>
</LightCard>
<ColumnCenter>
<ArrowDown size="16" color="#888D9B" />
</ColumnCenter>
<LightCard>
<RowBetween>
<Text fontSize={24} fontWeight={500}>
{amount1?.toSignificant(6)}
</Text>
<RowFixed gap="10px">
<TokenLogo address={amount1?.token?.address} size={'24px'} />
<Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}>
{symbol1}
</Text>
</RowFixed>
</RowBetween>
</LightCard>
</AutoColumn>
)}
{transactionType === TRANSACTION_TYPE.ADD && (
<AutoColumn gap="16px">
<RowFlat>
<Text fontSize="48px" fontWeight={500} lineHeight="32px" marginRight={10}>
{liquidityAmount?.toFixed(6)}
</Text>
<DoubleTokenLogo a0={address0 || ''} a1={address1 || ''} size={20} />
</RowFlat>
<Row>
<Text fontSize="24px">{symbol0 + ':' + symbol1 + ' Pool Tokens'}</Text>
</Row>
</AutoColumn>
)}
{transactionType === TRANSACTION_TYPE.REMOVE && (
<AutoColumn gap="16px">
<Row>
<TokenLogo address={address0} size={'30px'} />
<Text fontSize="24px" marginLeft={10}>
{symbol0} {amount0?.toSignificant(8)}
</Text>
</Row>
<Row>
<TokenLogo address={address1} size={'30px'} />
<Text fontSize="24px" marginLeft={10}>
{symbol1} {amount1?.toSignificant(8)}
</Text>
</Row>
</AutoColumn>
)}
</Section>
<BottomSection gap="12px">
{/* <Text fontWeight={500} fontSize={16}>
Deposited Tokens
</Text> */}
{/* <LightCard>
<RowBetween>
<Text fontWeight={500} fontSize={20}>
{amountFormatter(amount0, decimals0, 4)}
</Text>
<RowFixed>
<TokenLogo address={token0 || ''} size={'24px'} />
<Text fontWeight={500} fontSize={20} marginLeft="12px">
{symbol0}
</Text>
</RowFixed>
</RowBetween>
</LightCard>
<ColumnCenter>
<Plus size="16" color="#888D9B" />
</ColumnCenter>
<LightCard>
<RowBetween>
<Text fontWeight={500} fontSize={20}>
{amountFormatter(amount1, decimals1, 4)}
</Text>
<RowFixed>
<TokenLogo address={token1 || ''} size={'24px'} />
<Text fontWeight={500} fontSize={16} marginLeft="12px">
{symbol1}
</Text>
</RowFixed>
</RowBetween>
</LightCard> */}
<AutoColumn gap="12px">
<RowBetween>
<Text color="#565A69" fontWeight={500} fontSize={16}>
{symbol0} Deposited
</Text>
<RowFixed>
<TokenLogo address={address0 || ''} style={{ marginRight: '8px' }} />
<Text fontWeight={500} fontSize={16}>
{amount0?.toSignificant(6)}
{transactionType === TRANSACTION_TYPE.ADD && (
<>
<RowBetween>
<Text color="#565A69" fontWeight={500} fontSize={16}>
{symbol0} Deposited
</Text>
<RowFixed>
<TokenLogo address={address0 || ''} style={{ marginRight: '8px' }} />
<Text fontWeight={500} fontSize={16}>
{amount0?.toSignificant(6)}
</Text>
</RowFixed>
</RowBetween>
<RowBetween>
<Text color="#565A69" fontWeight={500} fontSize={16}>
{symbol1} Deposited
</Text>
<RowFixed>
<TokenLogo address={address1 || ''} style={{ marginRight: '8px' }} />
<Text fontWeight={500} fontSize={16}>
{amount1?.toSignificant(6)}
</Text>
</RowFixed>
</RowBetween>
</>
)}
{transactionType === TRANSACTION_TYPE.REMOVE && (
<RowBetween>
<Text color="#565A69" fontWeight={500} fontSize={16}>
{'UNI ' + symbol0 + ':' + symbol1} Burned
</Text>
</RowFixed>
</RowBetween>
<RowBetween>
<Text color="#565A69" fontWeight={500} fontSize={16}>
{symbol1} Deposited
</Text>
<RowFixed>
<TokenLogo address={address1 || ''} style={{ marginRight: '8px' }} />
<Text fontWeight={500} fontSize={16}>
{amount1?.toSignificant(6)}
<RowFixed>
<DoubleTokenLogo a0={address0 || ''} a1={address1 || ''} margin={true} />
<Text fontWeight={500} fontSize={16}>
{liquidityAmount?.toSignificant(6)}
</Text>
</RowFixed>
</RowBetween>
)}
{price && price?.adjusted && (
<RowBetween>
<Text color="#565A69" fontWeight={500} fontSize={16}>
Rate
</Text>
</RowFixed>
</RowBetween>
<RowBetween>
<Text color="#565A69" fontWeight={500} fontSize={16}>
Rate
</Text>
<Text fontWeight={500} fontSize={16}>
{price && `1 ${symbol0} = ${price?.adjusted.toFixed(8)} ${symbol1}`}
</Text>
</RowBetween>
<RowBetween>
<Text color="#565A69" fontWeight={500} fontSize={16}>
Minted Pool Share:
</Text>
<Text fontWeight={500} fontSize={16}>
{poolTokenPercentage?.toFixed(6) + '%'}
</Text>
</RowBetween>
<Text fontWeight={500} fontSize={16}>
{`1 ${symbol0} = ${price.adjusted.toFixed(8)} ${symbol1}`}
</Text>
</RowBetween>
)}
{transactionType === TRANSACTION_TYPE.ADD && poolTokenPercentage && (
<RowBetween>
<Text color="#565A69" fontWeight={500} fontSize={16}>
Minted Pool Share:
</Text>
<Text fontWeight={500} fontSize={16}>
{poolTokenPercentage?.toFixed(6) + '%'}
</Text>
</RowBetween>
)}
<ButtonPrimary
style={{ margin: '20px 0' }}
onClick={() => {
setWaitingForConfirmation(true)
SetConfirmed(true)
fakeCall()
setConfirmed(true)
contractCall()
}}
>
<Text fontWeight={500} fontSize={20}>
Confirm Supply
Confirm{' '}
{transactionType === TRANSACTION_TYPE.ADD
? 'Supply'
: transactionType === TRANSACTION_TYPE.REMOVE
? 'Remove'
: 'Swap'}
</Text>
</ButtonPrimary>
<Text fontSize={12} color="#565A69" textAlign="center">
{`Output is estimated. You will receive at least ${liquidityMinted?.toFixed(
6
)} UNI ${symbol0}/${symbol1} or the transaction will revert.`}
</Text>
{transactionType === TRANSACTION_TYPE.ADD && (
<Text fontSize={12} color="#565A69" textAlign="center">
{`Output is estimated. You will receive at least ${liquidityAmount?.toFixed(
6
)} UNI ${symbol0}/${symbol1} or the transaction will revert.`}
</Text>
)}
{transactionType === TRANSACTION_TYPE.REMOVE && (
<Text fontSize={12} color="#565A69" textAlign="center">
{`Output is estimated. You will receive at least ${amount0?.toSignificant(
6
)} ${symbol0} at least ${amount1?.toSignificant(6)} ${symbol1} or the transaction will revert.`}
</Text>
)}
{transactionType === TRANSACTION_TYPE.SWAP && (
<Text fontSize={12} color="#565A69" textAlign="center">
{`Output is estimated. You will receive at least ${amount1?.toSignificant(
6
)} ${symbol1} or the transaction will revert.`}
</Text>
)}
</AutoColumn>
</BottomSection>
</Wrapper>
@ -176,25 +243,33 @@ export default function ConfirmationModal({
<CloseIcon onClick={WrappedOnDismissed} />
</RowBetween>
<ConfirmedIcon>
{waitingForConfirmation ? <Loader size="90px" /> : <CheckCircle size={90} color="#27AE60" />}
{pendingConfirmation ? <Loader size="90px" /> : <CheckCircle size={90} color="#27AE60" />}
</ConfirmedIcon>
<AutoColumn gap="24px" justify={'center'}>
<Text fontWeight={500} fontSize={20}>
{!waitingForConfirmation ? 'Transaction Submitted' : 'Waiting For Confirmation'}
{!pendingConfirmation ? 'Transaction Submitted' : 'Waiting For Confirmation'}
</Text>
<AutoColumn gap="12px" justify={'center'}>
<Text fontWeight={500} fontSize={16} color="#2172E5">
Supplied
{transactionType === TRANSACTION_TYPE.ADD
? 'Supplied'
: transactionType === TRANSACTION_TYPE.REMOVE
? 'Removed'
: 'Swapped'}
</Text>
<Text fontWeight={600} fontSize={16} color="#2172E5">
{`${amount0?.toSignificant(6)} ${symbol0} and ${amount1?.toSignificant(6)} ${symbol1}`}
{`${amount0?.toSignificant(6)} ${symbol0} ${
transactionType === TRANSACTION_TYPE.SWAP ? 'for' : 'and'
} ${amount1?.toSignificant(6)} ${symbol1}`}
</Text>
</AutoColumn>
{!waitingForConfirmation && (
{!pendingConfirmation && (
<>
<Text fontWeight={500} fontSize={14} color="#2172E5">
View on Etherscan
</Text>
<Link href={getEtherscanLink(chainId, hash, 'transaction')}>
<Text fontWeight={500} fontSize={14} color="#2172E5">
View on Etherscan
</Text>
</Link>
<ButtonPrimary onClick={WrappedOnDismissed} style={{ margin: '20px 0' }}>
<Text fontWeight={500} fontSize={20}>
Close
@ -202,9 +277,9 @@ export default function ConfirmationModal({
</ButtonPrimary>
</>
)}
{waitingForConfirmation && <div style={{ height: '138px' }} />}
{pendingConfirmation && <div style={{ height: '138px' }} />}
<Text fontSize={12} color="#565A69">
{waitingForConfirmation
{pendingConfirmation
? 'Confirm this transaction in your wallet'
: `Estimated time until confirmation: 3 min`}
</Text>

@ -1,23 +1,24 @@
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { ethers } from 'ethers'
import styled from 'styled-components'
import { darken } from 'polished'
import '@reach/tooltip/styles.css'
import { ethers } from 'ethers'
import { darken } from 'polished'
import { WETH } from '@uniswap/sdk'
import { Text } from 'rebass'
import { ReactComponent as DropDown } from '../../assets/images/dropdown.svg'
import TokenLogo from '../TokenLogo'
import DoubleLogo from '../DoubleLogo'
import SearchModal from '../SearchModal'
import { Input as NumericalInput } from '../NumericalInput'
import { Text } from 'rebass'
import { RowBetween } from '../Row'
import { ReactComponent as DropDown } from '../../assets/images/dropdown.svg'
import { Input as NumericalInput } from '../NumericalInput'
import { useWeb3React } from '../../hooks'
import { useTransactionAdder, usePendingApproval } from '../../contexts/Transactions'
import { useToken, useAllTokens } from '../../contexts/Tokens'
import { useTranslation } from 'react-i18next'
import { useTokenContract } from '../../hooks'
import { calculateGasMargin } from '../../utils'
import { useAddressBalance } from '../../contexts/Balances'
import { useTransactionAdder, usePendingApproval } from '../../contexts/Transactions'
const GAS_MARGIN = ethers.utils.bigNumberify(1000)
@ -26,9 +27,9 @@ const SubCurrencySelect = styled.button`
padding: 4px 50px 4px 15px;
margin-right: -40px;
line-height: 0;
height: 2rem;
align-items: center;
border-radius: 2.5rem;
height: 2rem;
outline: none;
cursor: pointer;
user-select: none;
@ -41,15 +42,16 @@ const InputRow = styled.div`
${({ theme }) => theme.flexRowNoWrap}
align-items: center;
padding: 0.25rem 0.85rem 0.75rem;
padding: 0.75rem 0.85rem 0.75rem;
`
const CurrencySelect = styled.button`
align-items: center;
height: 2.2rem;
font-size: 20px;
background-color: ${({ selected, theme }) => (selected ? theme.buttonBackgroundPlain : theme.zumthorBlue)};
color: ${({ selected, theme }) => (selected ? theme.textColor : theme.royalBlue)};
height: 2rem;
border: 1px solid
${({ selected, theme, disableTokenSelect }) =>
disableTokenSelect ? theme.buttonBackgroundPlain : selected ? theme.buttonOutlinePlain : theme.royalBlue};
@ -111,7 +113,7 @@ const LabelRow = styled.div`
color: ${({ theme }) => theme.doveGray};
font-size: 0.75rem;
line-height: 1rem;
padding: 0.75rem 1rem;
padding: 0.75rem 1rem 0;
span:hover {
cursor: pointer;
color: ${({ theme }) => darken(0.2, theme.doveGray)};
@ -159,49 +161,44 @@ export default function CurrencyInputPanel({
value,
field,
onUserInput,
selectedTokenAddress,
onTokenSelection,
title,
onMax,
atMax,
error
// disableUnlock,
// disableTokenSelect,
// urlAddedTokens
error,
urlAddedTokens = [], // used
token = null,
showUnlock = false, // used to show unlock if approval needed
disableUnlock = false,
disableTokenSelect = false,
hideBalance = false,
isExchange = false,
exchange = null, // used for double token logo
customBalance = null // used for LP balances instead of token balance
}) {
const { account } = useWeb3React()
const { account, chainId } = useWeb3React()
const { t } = useTranslation()
const disableUnlock = false
const disableTokenSelect = false
const urlAddedTokens = []
const errorMessage = error
const [modalIsOpen, setModalIsOpen] = useState(false)
const tokenContract = useTokenContract(selectedTokenAddress)
const { exchangeAddress: selectedTokenExchangeAddress } = useToken(selectedTokenAddress)
const pendingApproval = usePendingApproval(selectedTokenAddress)
const addTransaction = useTransactionAdder()
const allTokens = useAllTokens()
const token = useToken(selectedTokenAddress)
const userTokenBalance = useAddressBalance(account, token)
const [showUnlock] = useState(false)
const [modalOpen, setModalOpen] = useState(false)
const [showMax, setShowMax] = useState(false)
// this one causes the infinite loop
const userTokenBalance = useAddressBalance(account, token)
const tokenContract = useTokenContract(token?.address)
const pendingApproval = usePendingApproval(token?.address)
const routerAddress = '0xd9210Ff5A0780E083BB40e30d005d93a2DcFA4EF'
function renderUnlockButton() {
if (disableUnlock || !showUnlock || selectedTokenAddress === 'ETH' || !selectedTokenAddress) {
if (
disableUnlock ||
!showUnlock ||
token?.address === 'ETH' ||
token?.address === WETH[chainId].address ||
!token?.address
) {
return null
} else {
if (!pendingApproval) {
@ -211,25 +208,21 @@ export default function CurrencyInputPanel({
let estimatedGas
let useUserBalance = false
estimatedGas = await tokenContract.estimate
.approve(selectedTokenExchangeAddress, ethers.constants.MaxUint256)
.approve(routerAddress, ethers.constants.MaxUint256)
.catch(e => {
console.log('Error setting max token approval.')
})
if (!estimatedGas) {
// general fallback for tokens who restrict approval amounts
estimatedGas = await tokenContract.estimate.approve(selectedTokenExchangeAddress, userTokenBalance)
estimatedGas = await tokenContract.estimate.approve(routerAddress, userTokenBalance)
useUserBalance = true
}
tokenContract
.approve(
selectedTokenExchangeAddress,
useUserBalance ? userTokenBalance : ethers.constants.MaxUint256,
{
gasLimit: calculateGasMargin(estimatedGas, GAS_MARGIN)
}
)
.approve(routerAddress, useUserBalance ? userTokenBalance : ethers.constants.MaxUint256, {
gasLimit: calculateGasMargin(estimatedGas, GAS_MARGIN)
})
.then(response => {
addTransaction(response, { approval: selectedTokenAddress })
addTransaction(response, { approval: token?.address })
})
}}
>
@ -244,16 +237,20 @@ export default function CurrencyInputPanel({
return (
<InputPanel>
<Container error={!!errorMessage}>
<LabelRow>
<RowBetween>
<Text>{title}</Text>
<ErrorSpan data-tip={'Enter max'} error={!!errorMessage} onClick={() => {}}></ErrorSpan>
<ClickableText onClick={onMax}>
<Text>Balance: {userTokenBalance?.toFixed(2)}</Text>
</ClickableText>
</RowBetween>
</LabelRow>
<Container error={!!error}>
{!hideBalance && (
<LabelRow>
<RowBetween>
<Text>{title}</Text>
<ErrorSpan data-tip={'Enter max'} error={!!error} onClick={() => {}}></ErrorSpan>
<ClickableText onClick={onMax}>
<Text>
Balance: {customBalance ? customBalance.toSignificant(4) : userTokenBalance?.toSignificant(4)}
</Text>
</ClickableText>
</RowBetween>
</LabelRow>
)}
<InputRow>
<NumericalInput
field={field}
@ -263,34 +260,40 @@ export default function CurrencyInputPanel({
setShowMax(true)
}}
/>
{!!selectedTokenAddress && !atMax && showMax && <StyledBalanceMax onClick={onMax}>MAX</StyledBalanceMax>}
{!!token?.address && !atMax && showMax && <StyledBalanceMax onClick={onMax}>MAX</StyledBalanceMax>}
{renderUnlockButton()}
<CurrencySelect
selected={!!selectedTokenAddress}
selected={!!token?.address}
onClick={() => {
if (!disableTokenSelect) {
setModalIsOpen(true)
setModalOpen(true)
}
}}
disableTokenSelect={disableTokenSelect}
>
<Aligner>
{selectedTokenAddress ? <TokenLogo address={selectedTokenAddress} size={'24px'} /> : null}
{
{isExchange ? (
<DoubleLogo a0={exchange?.token0.address} a1={exchange?.token1.address} margin={true} />
) : token?.address ? (
<TokenLogo address={token?.address} size={'24px'} />
) : null}
{isExchange ? (
<StyledTokenName>
{(allTokens[selectedTokenAddress] && allTokens[selectedTokenAddress].symbol) || t('selectToken')}
{exchange?.token0.symbol}:{exchange?.token1.symbol}
</StyledTokenName>
}
{!disableTokenSelect && <StyledDropDown selected={!!selectedTokenAddress} />}
) : (
<StyledTokenName>{(token && token.symbol) || t('selectToken')}</StyledTokenName>
)}
{!disableTokenSelect && <StyledDropDown selected={!!token?.address} />}
</Aligner>
</CurrencySelect>
</InputRow>
</Container>
{!disableTokenSelect && (
<SearchModal
isOpen={modalIsOpen}
isOpen={modalOpen}
onDismiss={() => {
setModalIsOpen(false)
setModalOpen(false)
}}
filterType="tokens"
urlAddedTokens={urlAddedTokens}

@ -1,939 +0,0 @@
import React, { useState, useReducer, useEffect } from 'react'
import ReactGA from 'react-ga'
import { createBrowserHistory } from 'history'
import { ethers } from 'ethers'
import styled from 'styled-components'
import { useTranslation } from 'react-i18next'
import { useWeb3React } from '../../hooks'
import { brokenTokens } from '../../constants'
import { amountFormatter, calculateGasMargin, isAddress } from '../../utils'
import { useExchangeContract } from '../../hooks'
import { useToken, INITIAL_TOKENS_CONTEXT } from '../../contexts/Tokens'
import { useTransactionAdder } from '../../contexts/Transactions'
import { useAddressBalance, useExchangeReserves } from '../../contexts/Balances'
import { useAddressAllowance } from '../../contexts/Allowances'
import { useWalletModalToggle } from '../../contexts/Application'
import { Button } from '../../theme'
import CurrencyInputPanel from '../CurrencyInputPanel'
import AddressInputPanel from '../AddressInputPanel'
import OversizedPanel from '../OversizedPanel'
import TransactionDetails from '../TransactionDetails'
import ArrowDown from '../../assets/svg/SVGArrowDown'
import WarningCard from '../WarningCard'
const INPUT = 0
const OUTPUT = 1
const ETH_TO_TOKEN = 0
const TOKEN_TO_ETH = 1
const TOKEN_TO_TOKEN = 2
// denominated in bips
const ALLOWED_SLIPPAGE_DEFAULT = 50
const TOKEN_ALLOWED_SLIPPAGE_DEFAULT = 50
// 15 minutes, denominated in seconds
const DEFAULT_DEADLINE_FROM_NOW = 60 * 15
// % above the calculated gas cost that we actually send, denominated in bips
const GAS_MARGIN = ethers.utils.bigNumberify(1000)
const DownArrowBackground = styled.div`
${({ theme }) => theme.flexRowNoWrap}
justify-content: center;
align-items: center;
`
const WrappedArrowDown = ({ clickable, active, ...rest }) => <ArrowDown {...rest} />
const DownArrow = styled(WrappedArrowDown)`
color: ${({ theme, active }) => (active ? theme.royalBlue : theme.chaliceGray)};
width: 0.625rem;
height: 0.625rem;
position: relative;
padding: 0.875rem;
cursor: ${({ clickable }) => clickable && 'pointer'};
`
const ExchangeRateWrapper = styled.div`
${({ theme }) => theme.flexRowNoWrap};
align-items: center;
color: ${({ theme }) => theme.doveGray};
font-size: 0.75rem;
padding: 0.5rem 1rem;
`
const ExchangeRate = styled.span`
flex: 1 1 auto;
width: 0;
color: ${({ theme }) => theme.doveGray};
`
const Flex = styled.div`
display: flex;
justify-content: center;
padding: 2rem;
button {
max-width: 20rem;
}
`
function calculateSlippageBounds(value, token = false, tokenAllowedSlippage, allowedSlippage) {
if (value) {
const offset = value.mul(token ? tokenAllowedSlippage : allowedSlippage).div(ethers.utils.bigNumberify(10000))
const minimum = value.sub(offset)
const maximum = value.add(offset)
return {
minimum: minimum.lt(ethers.constants.Zero) ? ethers.constants.Zero : minimum,
maximum: maximum.gt(ethers.constants.MaxUint256) ? ethers.constants.MaxUint256 : maximum
}
} else {
return {}
}
}
function getSwapType(inputCurrency, outputCurrency) {
if (!inputCurrency || !outputCurrency) {
return null
} else if (inputCurrency === 'ETH') {
return ETH_TO_TOKEN
} else if (outputCurrency === 'ETH') {
return TOKEN_TO_ETH
} else {
return TOKEN_TO_TOKEN
}
}
// this mocks the getInputPrice function, and calculates the required output
function calculateEtherTokenOutputFromInput(inputAmount, inputReserve, outputReserve) {
const inputAmountWithFee = inputAmount.mul(ethers.utils.bigNumberify(997))
const numerator = inputAmountWithFee.mul(outputReserve)
const denominator = inputReserve.mul(ethers.utils.bigNumberify(1000)).add(inputAmountWithFee)
return numerator.div(denominator)
}
// this mocks the getOutputPrice function, and calculates the required input
function calculateEtherTokenInputFromOutput(outputAmount, inputReserve, outputReserve) {
const numerator = inputReserve.mul(outputAmount).mul(ethers.utils.bigNumberify(1000))
const denominator = outputReserve.sub(outputAmount).mul(ethers.utils.bigNumberify(997))
return numerator.div(denominator).add(ethers.constants.One)
}
function getInitialSwapState(state) {
return {
independentValue: state.exactFieldURL && state.exactAmountURL ? state.exactAmountURL : '', // this is a user input
dependentValue: '', // this is a calculated number
independentField: state.exactFieldURL === 'output' ? OUTPUT : INPUT,
inputCurrency: state.inputCurrencyURL ? state.inputCurrencyURL : 'ETH',
outputCurrency: state.outputCurrencyURL
? state.outputCurrencyURL === 'ETH'
? state.inputCurrencyURL && state.inputCurrencyURL !== 'ETH'
? 'ETH'
: ''
: state.outputCurrencyURL
: state.initialCurrency
? state.initialCurrency
: ''
}
}
function swapStateReducer(state, action) {
switch (action.type) {
case 'FLIP_INDEPENDENT': {
const { independentField, inputCurrency, outputCurrency } = state
return {
...state,
dependentValue: '',
independentField: independentField === INPUT ? OUTPUT : INPUT,
inputCurrency: outputCurrency,
outputCurrency: inputCurrency
}
}
case 'SELECT_CURRENCY': {
const { inputCurrency, outputCurrency } = state
const { field, currency } = action.payload
const newInputCurrency = field === INPUT ? currency : inputCurrency
const newOutputCurrency = field === OUTPUT ? currency : outputCurrency
if (newInputCurrency === newOutputCurrency) {
return {
...state,
inputCurrency: field === INPUT ? currency : '',
outputCurrency: field === OUTPUT ? currency : ''
}
} else {
return {
...state,
inputCurrency: newInputCurrency,
outputCurrency: newOutputCurrency
}
}
}
case 'UPDATE_INDEPENDENT': {
const { field, value } = action.payload
const { dependentValue, independentValue } = state
return {
...state,
independentValue: value,
dependentValue: value === independentValue ? dependentValue : '',
independentField: field
}
}
case 'UPDATE_DEPENDENT': {
return {
...state,
dependentValue: action.payload
}
}
default: {
return getInitialSwapState()
}
}
}
function getExchangeRate(inputValue, inputDecimals, outputValue, outputDecimals, invert = false) {
try {
if (
inputValue &&
(inputDecimals || inputDecimals === 0) &&
outputValue &&
(outputDecimals || outputDecimals === 0)
) {
const factor = ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(18))
if (invert) {
return inputValue
.mul(factor)
.mul(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(outputDecimals)))
.div(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(inputDecimals)))
.div(outputValue)
} else {
return outputValue
.mul(factor)
.mul(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(inputDecimals)))
.div(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(outputDecimals)))
.div(inputValue)
}
}
} catch {}
}
function getMarketRate(
swapType,
inputReserveETH,
inputReserveToken,
inputDecimals,
outputReserveETH,
outputReserveToken,
outputDecimals,
invert = false
) {
if (swapType === ETH_TO_TOKEN) {
return getExchangeRate(outputReserveETH, 18, outputReserveToken, outputDecimals, invert)
} else if (swapType === TOKEN_TO_ETH) {
return getExchangeRate(inputReserveToken, inputDecimals, inputReserveETH, 18, invert)
} else if (swapType === TOKEN_TO_TOKEN) {
const factor = ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(18))
const firstRate = getExchangeRate(inputReserveToken, inputDecimals, inputReserveETH, 18)
const secondRate = getExchangeRate(outputReserveETH, 18, outputReserveToken, outputDecimals)
try {
return !!(firstRate && secondRate) ? firstRate.mul(secondRate).div(factor) : undefined
} catch {}
}
}
export default function ExchangePage({ initialCurrency, sending = false, params }) {
const { t } = useTranslation()
const { account, chainId, error } = useWeb3React()
const urlAddedTokens = {}
if (params.inputCurrency) {
urlAddedTokens[params.inputCurrency] = true
}
if (params.outputCurrency) {
urlAddedTokens[params.outputCurrency] = true
}
if (isAddress(initialCurrency)) {
urlAddedTokens[initialCurrency] = true
}
const addTransaction = useTransactionAdder()
// check if URL specifies valid slippage, if so use as default
const initialSlippage = (token = false) => {
let slippage = Number.parseInt(params.slippage)
if (!isNaN(slippage) && (slippage === 0 || slippage >= 1)) {
return slippage // round to match custom input availability
}
// check for token <-> token slippage option
return token ? TOKEN_ALLOWED_SLIPPAGE_DEFAULT : ALLOWED_SLIPPAGE_DEFAULT
}
// check URL params for recipient, only on send page
const initialRecipient = () => {
if (sending && params.recipient) {
return params.recipient
}
return ''
}
const [brokenTokenWarning, setBrokenTokenWarning] = useState()
const [deadlineFromNow, setDeadlineFromNow] = useState(DEFAULT_DEADLINE_FROM_NOW)
const [rawSlippage, setRawSlippage] = useState(() => initialSlippage())
const [rawTokenSlippage, setRawTokenSlippage] = useState(() => initialSlippage(true))
const allowedSlippageBig = ethers.utils.bigNumberify(rawSlippage)
const tokenAllowedSlippageBig = ethers.utils.bigNumberify(rawTokenSlippage)
// analytics
useEffect(() => {
ReactGA.pageview(window.location.pathname + window.location.search)
}, [])
// core swap state
const [swapState, dispatchSwapState] = useReducer(
swapStateReducer,
{
initialCurrency: initialCurrency,
inputCurrencyURL: params.inputCurrency,
outputCurrencyURL: params.outputCurrency,
exactFieldURL: params.exactField,
exactAmountURL: params.exactAmount
},
getInitialSwapState
)
const { independentValue, dependentValue, independentField, inputCurrency, outputCurrency } = swapState
useEffect(() => {
setBrokenTokenWarning(false)
for (let i = 0; i < brokenTokens.length; i++) {
if (
brokenTokens[i].toLowerCase() === outputCurrency.toLowerCase() ||
brokenTokens[i].toLowerCase() === inputCurrency.toLowerCase()
) {
setBrokenTokenWarning(true)
}
}
}, [outputCurrency, inputCurrency])
const [recipient, setRecipient] = useState({
address: initialRecipient(),
name: ''
})
const [recipientError, setRecipientError] = useState()
// get swap type from the currency types
const swapType = getSwapType(inputCurrency, outputCurrency)
// get decimals and exchange address for each of the currency types
const { symbol: inputSymbol, decimals: inputDecimals, exchangeAddress: inputExchangeAddress } = useToken(
inputCurrency
)
const { symbol: outputSymbol, decimals: outputDecimals, exchangeAddress: outputExchangeAddress } = useToken(
outputCurrency
)
const inputExchangeContract = useExchangeContract(inputExchangeAddress)
const outputExchangeContract = useExchangeContract(outputExchangeAddress)
const contract = swapType === ETH_TO_TOKEN ? outputExchangeContract : inputExchangeContract
// get input allowance
const inputAllowance = useAddressAllowance(account, inputCurrency, inputExchangeAddress)
// fetch reserves for each of the currency types
const { reserveETH: inputReserveETH, reserveToken: inputReserveToken } = useExchangeReserves(inputCurrency)
const { reserveETH: outputReserveETH, reserveToken: outputReserveToken } = useExchangeReserves(outputCurrency)
// get balances for each of the currency types
const inputBalance = 0
const outputBalance = 0
const inputBalanceFormatted = !!(inputBalance && Number.isInteger(inputDecimals))
? amountFormatter(inputBalance, inputDecimals, Math.min(4, inputDecimals))
: ''
const outputBalanceFormatted = !!(outputBalance && Number.isInteger(outputDecimals))
? amountFormatter(outputBalance, outputDecimals, Math.min(4, outputDecimals))
: ''
// compute useful transforms of the data above
const independentDecimals = independentField === INPUT ? inputDecimals : outputDecimals
const dependentDecimals = independentField === OUTPUT ? inputDecimals : outputDecimals
// declare/get parsed and formatted versions of input/output values
const [independentValueParsed, setIndependentValueParsed] = useState()
const dependentValueFormatted = !!(dependentValue && (dependentDecimals || dependentDecimals === 0))
? amountFormatter(dependentValue, dependentDecimals, Math.min(4, dependentDecimals), false)
: ''
const inputValueParsed = independentField === INPUT ? independentValueParsed : dependentValue
const inputValueFormatted = independentField === INPUT ? independentValue : dependentValueFormatted
const outputValueParsed = independentField === OUTPUT ? independentValueParsed : dependentValue
const outputValueFormatted = independentField === OUTPUT ? independentValue : dependentValueFormatted
// validate + parse independent value
const [independentError, setIndependentError] = useState()
useEffect(() => {
if (independentValue && (independentDecimals || independentDecimals === 0)) {
try {
const parsedValue = ethers.utils.parseUnits(independentValue, independentDecimals)
if (parsedValue.lte(ethers.constants.Zero) || parsedValue.gte(ethers.constants.MaxUint256)) {
throw Error()
} else {
setIndependentValueParsed(parsedValue)
setIndependentError(null)
}
} catch {
setIndependentError(t('inputNotValid'))
}
return () => {
setIndependentValueParsed()
setIndependentError()
}
}
}, [independentValue, independentDecimals, t])
// calculate slippage from target rate
const { minimum: dependentValueMinumum, maximum: dependentValueMaximum } = calculateSlippageBounds(
dependentValue,
swapType === TOKEN_TO_TOKEN,
tokenAllowedSlippageBig,
allowedSlippageBig
)
// validate input allowance + balance
const [inputError, setInputError] = useState()
const [showUnlock, setShowUnlock] = useState(false)
useEffect(() => {
const inputValueCalculation = independentField === INPUT ? independentValueParsed : dependentValueMaximum
if (inputBalance && (inputAllowance || inputCurrency === 'ETH') && inputValueCalculation) {
if (inputBalance.lt(inputValueCalculation)) {
setInputError(t('insufficientBalance'))
} else if (inputCurrency !== 'ETH' && inputAllowance.lt(inputValueCalculation)) {
setInputError(t('unlockTokenCont'))
setShowUnlock(true)
} else {
setInputError(null)
setShowUnlock(false)
}
return () => {
setInputError()
setShowUnlock(false)
}
}
}, [independentField, independentValueParsed, dependentValueMaximum, inputBalance, inputCurrency, inputAllowance, t])
// calculate dependent value
useEffect(() => {
const amount = independentValueParsed
if (swapType === ETH_TO_TOKEN) {
const reserveETH = outputReserveETH
const reserveToken = outputReserveToken
if (amount && reserveETH && reserveToken) {
try {
const calculatedDependentValue =
independentField === INPUT
? calculateEtherTokenOutputFromInput(amount, reserveETH, reserveToken)
: calculateEtherTokenInputFromOutput(amount, reserveETH, reserveToken)
if (calculatedDependentValue.lte(ethers.constants.Zero)) {
throw Error()
}
dispatchSwapState({
type: 'UPDATE_DEPENDENT',
payload: calculatedDependentValue
})
} catch {
setIndependentError(t('insufficientLiquidity'))
}
return () => {
dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: '' })
}
}
} else if (swapType === TOKEN_TO_ETH) {
const reserveETH = inputReserveETH
const reserveToken = inputReserveToken
if (amount && reserveETH && reserveToken) {
try {
const calculatedDependentValue =
independentField === INPUT
? calculateEtherTokenOutputFromInput(amount, reserveToken, reserveETH)
: calculateEtherTokenInputFromOutput(amount, reserveToken, reserveETH)
if (calculatedDependentValue.lte(ethers.constants.Zero)) {
throw Error()
}
dispatchSwapState({
type: 'UPDATE_DEPENDENT',
payload: calculatedDependentValue
})
} catch {
setIndependentError(t('insufficientLiquidity'))
}
return () => {
dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: '' })
}
}
} else if (swapType === TOKEN_TO_TOKEN) {
const reserveETHFirst = inputReserveETH
const reserveTokenFirst = inputReserveToken
const reserveETHSecond = outputReserveETH
const reserveTokenSecond = outputReserveToken
if (amount && reserveETHFirst && reserveTokenFirst && reserveETHSecond && reserveTokenSecond) {
try {
if (independentField === INPUT) {
const intermediateValue = calculateEtherTokenOutputFromInput(amount, reserveTokenFirst, reserveETHFirst)
if (intermediateValue.lte(ethers.constants.Zero)) {
throw Error()
}
const calculatedDependentValue = calculateEtherTokenOutputFromInput(
intermediateValue,
reserveETHSecond,
reserveTokenSecond
)
if (calculatedDependentValue.lte(ethers.constants.Zero)) {
throw Error()
}
dispatchSwapState({
type: 'UPDATE_DEPENDENT',
payload: calculatedDependentValue
})
} else {
const intermediateValue = calculateEtherTokenInputFromOutput(amount, reserveETHSecond, reserveTokenSecond)
if (intermediateValue.lte(ethers.constants.Zero)) {
throw Error()
}
const calculatedDependentValue = calculateEtherTokenInputFromOutput(
intermediateValue,
reserveTokenFirst,
reserveETHFirst
)
if (calculatedDependentValue.lte(ethers.constants.Zero)) {
throw Error()
}
dispatchSwapState({
type: 'UPDATE_DEPENDENT',
payload: calculatedDependentValue
})
}
} catch {
setIndependentError(t('insufficientLiquidity'))
}
return () => {
dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: '' })
}
}
}
}, [
independentValueParsed,
swapType,
outputReserveETH,
outputReserveToken,
inputReserveETH,
inputReserveToken,
independentField,
t
])
useEffect(() => {
const history = createBrowserHistory()
history.push(window.location.pathname + '')
}, [])
const [inverted, setInverted] = useState(false)
const exchangeRate = getExchangeRate(inputValueParsed, inputDecimals, outputValueParsed, outputDecimals)
const exchangeRateInverted = getExchangeRate(inputValueParsed, inputDecimals, outputValueParsed, outputDecimals, true)
const marketRate = getMarketRate(
swapType,
inputReserveETH,
inputReserveToken,
inputDecimals,
outputReserveETH,
outputReserveToken,
outputDecimals
)
const percentSlippage =
exchangeRate && marketRate && !marketRate.isZero()
? exchangeRate
.sub(marketRate)
.abs()
.mul(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(18)))
.div(marketRate)
.sub(ethers.utils.bigNumberify(3).mul(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(15))))
: undefined
const percentSlippageFormatted = percentSlippage && amountFormatter(percentSlippage, 16, 2)
const slippageWarning =
percentSlippage &&
percentSlippage.gte(ethers.utils.parseEther('.05')) &&
percentSlippage.lt(ethers.utils.parseEther('.2')) // [5% - 20%)
const highSlippageWarning = percentSlippage && percentSlippage.gte(ethers.utils.parseEther('.2')) // [20+%
const isValid = sending
? exchangeRate && inputError === null && independentError === null && recipientError === null && deadlineFromNow
: exchangeRate && inputError === null && independentError === null && deadlineFromNow
const estimatedText = `(${t('estimated')})`
function formatBalance(value) {
return `Balance: ${value}`
}
async function onSwap() {
//if user changed deadline, log new one in minutes
if (deadlineFromNow !== DEFAULT_DEADLINE_FROM_NOW) {
ReactGA.event({
category: 'Advanced Interaction',
action: 'Set Custom Deadline',
value: deadlineFromNow / 60
})
}
const deadline = Math.ceil(Date.now() / 1000) + deadlineFromNow
// if user has changed slippage, log
if (swapType === TOKEN_TO_TOKEN) {
if (parseInt(tokenAllowedSlippageBig.toString()) !== TOKEN_ALLOWED_SLIPPAGE_DEFAULT) {
ReactGA.event({
category: 'Advanced Interaction',
action: 'Set Custom Slippage',
value: parseInt(tokenAllowedSlippageBig.toString())
})
}
} else {
if (parseInt(allowedSlippageBig.toString()) !== ALLOWED_SLIPPAGE_DEFAULT) {
ReactGA.event({
category: 'Advanced Interaction',
action: 'Set Custom Slippage',
value: parseInt(allowedSlippageBig.toString())
})
}
}
let estimate, method, args, value, ethTransactionSize
if (inputCurrency === 'ETH') {
ethTransactionSize = inputValueFormatted
} else if (outputCurrency === 'ETH') {
ethTransactionSize = inputValueFormatted * amountFormatter(exchangeRate, 18, 6, false)
} else {
const tokenBalance = 1
const ethBalance = 1
let ethRate = ethBalance && tokenBalance && ethBalance.div(tokenBalance)
ethTransactionSize = inputValueFormatted * ethRate
}
// params for GA event
let action = ''
let label = ''
if (independentField === INPUT) {
// set GA params
action = sending ? 'SendInput' : 'SwapInput'
label = outputCurrency
if (swapType === ETH_TO_TOKEN) {
estimate = sending ? contract.estimate.ethToTokenTransferInput : contract.estimate.ethToTokenSwapInput
method = sending ? contract.ethToTokenTransferInput : contract.ethToTokenSwapInput
args = sending ? [dependentValueMinumum, deadline, recipient.address] : [dependentValueMinumum, deadline]
value = independentValueParsed
} else if (swapType === TOKEN_TO_ETH) {
estimate = sending ? contract.estimate.tokenToEthTransferInput : contract.estimate.tokenToEthSwapInput
method = sending ? contract.tokenToEthTransferInput : contract.tokenToEthSwapInput
args = sending
? [independentValueParsed, dependentValueMinumum, deadline, recipient.address]
: [independentValueParsed, dependentValueMinumum, deadline]
value = ethers.constants.Zero
} else if (swapType === TOKEN_TO_TOKEN) {
estimate = sending ? contract.estimate.tokenToTokenTransferInput : contract.estimate.tokenToTokenSwapInput
method = sending ? contract.tokenToTokenTransferInput : contract.tokenToTokenSwapInput
args = sending
? [
independentValueParsed,
dependentValueMinumum,
ethers.constants.One,
deadline,
recipient.address,
outputCurrency
]
: [independentValueParsed, dependentValueMinumum, ethers.constants.One, deadline, outputCurrency]
value = ethers.constants.Zero
}
} else if (independentField === OUTPUT) {
// set GA params
action = sending ? 'SendOutput' : 'SwapOutput'
label = outputCurrency
if (swapType === ETH_TO_TOKEN) {
estimate = sending ? contract.estimate.ethToTokenTransferOutput : contract.estimate.ethToTokenSwapOutput
method = sending ? contract.ethToTokenTransferOutput : contract.ethToTokenSwapOutput
args = sending ? [independentValueParsed, deadline, recipient.address] : [independentValueParsed, deadline]
value = dependentValueMaximum
} else if (swapType === TOKEN_TO_ETH) {
estimate = sending ? contract.estimate.tokenToEthTransferOutput : contract.estimate.tokenToEthSwapOutput
method = sending ? contract.tokenToEthTransferOutput : contract.tokenToEthSwapOutput
args = sending
? [independentValueParsed, dependentValueMaximum, deadline, recipient.address]
: [independentValueParsed, dependentValueMaximum, deadline]
value = ethers.constants.Zero
} else if (swapType === TOKEN_TO_TOKEN) {
estimate = sending ? contract.estimate.tokenToTokenTransferOutput : contract.estimate.tokenToTokenSwapOutput
method = sending ? contract.tokenToTokenTransferOutput : contract.tokenToTokenSwapOutput
args = sending
? [
independentValueParsed,
dependentValueMaximum,
ethers.constants.MaxUint256,
deadline,
recipient.address,
outputCurrency
]
: [independentValueParsed, dependentValueMaximum, ethers.constants.MaxUint256, deadline, outputCurrency]
value = ethers.constants.Zero
}
}
const estimatedGasLimit = await estimate(...args, { value })
method(...args, {
value,
gasLimit: calculateGasMargin(estimatedGasLimit, GAS_MARGIN)
}).then(response => {
addTransaction(response)
ReactGA.event({
category: 'Transaction',
action: action,
label: label,
value: ethTransactionSize,
dimension1: response.hash
})
ReactGA.event({
category: 'Hash',
action: response.hash,
label: ethTransactionSize.toString()
})
})
}
const [customSlippageError, setcustomSlippageError] = useState('')
const toggleWalletModal = useWalletModalToggle()
const newInputDetected =
inputCurrency !== 'ETH' && inputCurrency && !INITIAL_TOKENS_CONTEXT[chainId].hasOwnProperty(inputCurrency)
const newOutputDetected =
outputCurrency !== 'ETH' && outputCurrency && !INITIAL_TOKENS_CONTEXT[chainId].hasOwnProperty(outputCurrency)
const [showInputWarning, setShowInputWarning] = useState(false)
const [showOutputWarning, setShowOutputWarning] = useState(false)
useEffect(() => {
if (newInputDetected) {
setShowInputWarning(true)
} else {
setShowInputWarning(false)
}
}, [newInputDetected, setShowInputWarning])
useEffect(() => {
if (newOutputDetected) {
setShowOutputWarning(true)
} else {
setShowOutputWarning(false)
}
}, [newOutputDetected, setShowOutputWarning])
return (
<>
{showInputWarning && (
<WarningCard
onDismiss={() => {
setShowInputWarning(false)
}}
urlAddedTokens={urlAddedTokens}
currency={inputCurrency}
/>
)}
{showOutputWarning && (
<WarningCard
onDismiss={() => {
setShowOutputWarning(false)
}}
urlAddedTokens={urlAddedTokens}
currency={outputCurrency}
/>
)}
<CurrencyInputPanel
title={t('input')}
urlAddedTokens={urlAddedTokens}
description={inputValueFormatted && independentField === OUTPUT ? estimatedText : ''}
extraText={inputBalanceFormatted && formatBalance(inputBalanceFormatted)}
extraTextClickHander={() => {
if (inputBalance && inputDecimals) {
const valueToSet = inputCurrency === 'ETH' ? inputBalance.sub(ethers.utils.parseEther('.1')) : inputBalance
if (valueToSet.gt(ethers.constants.Zero)) {
dispatchSwapState({
type: 'UPDATE_INDEPENDENT',
payload: {
value: amountFormatter(valueToSet, inputDecimals, inputDecimals, false),
field: INPUT
}
})
}
}
}}
onCurrencySelected={inputCurrency => {
dispatchSwapState({
type: 'SELECT_CURRENCY',
payload: { currency: inputCurrency, field: INPUT }
})
}}
onValueChange={inputValue => {
dispatchSwapState({
type: 'UPDATE_INDEPENDENT',
payload: { value: inputValue, field: INPUT }
})
}}
showUnlock={showUnlock}
selectedTokens={[inputCurrency, outputCurrency]}
selectedTokenAddress={inputCurrency}
value={inputValueFormatted}
errorMessage={inputError ? inputError : independentField === INPUT ? independentError : ''}
/>
<OversizedPanel>
<DownArrowBackground>
<DownArrow
onClick={() => {
dispatchSwapState({ type: 'FLIP_INDEPENDENT' })
}}
clickable
alt="swap"
active={isValid}
/>
</DownArrowBackground>
</OversizedPanel>
<CurrencyInputPanel
title={t('output')}
description={outputValueFormatted && independentField === INPUT ? estimatedText : ''}
extraText={outputBalanceFormatted && formatBalance(outputBalanceFormatted)}
urlAddedTokens={urlAddedTokens}
onCurrencySelected={outputCurrency => {
dispatchSwapState({
type: 'SELECT_CURRENCY',
payload: { currency: outputCurrency, field: OUTPUT }
})
}}
onValueChange={outputValue => {
dispatchSwapState({
type: 'UPDATE_INDEPENDENT',
payload: { value: outputValue, field: OUTPUT }
})
}}
selectedTokens={[inputCurrency, outputCurrency]}
selectedTokenAddress={outputCurrency}
value={outputValueFormatted}
errorMessage={independentField === OUTPUT ? independentError : ''}
disableUnlock
/>
{sending ? (
<>
<OversizedPanel>
<DownArrowBackground>
<DownArrow active={isValid} alt="arrow" />
</DownArrowBackground>
</OversizedPanel>
<AddressInputPanel onChange={setRecipient} onError={setRecipientError} initialInput={recipient} />
</>
) : (
''
)}
<OversizedPanel hideBottom>
<ExchangeRateWrapper
onClick={() => {
setInverted(inverted => !inverted)
}}
>
<ExchangeRate>{t('exchangeRate')}</ExchangeRate>
{inverted ? (
<span>
{exchangeRate
? `1 ${inputSymbol} = ${amountFormatter(exchangeRate, 18, 6, false)} ${outputSymbol}`
: ' - '}
</span>
) : (
<span>
{exchangeRate
? `1 ${outputSymbol} = ${amountFormatter(exchangeRateInverted, 18, 6, false)} ${inputSymbol}`
: ' - '}
</span>
)}
</ExchangeRateWrapper>
</OversizedPanel>
<TransactionDetails
account={account}
setRawSlippage={setRawSlippage}
setRawTokenSlippage={setRawTokenSlippage}
rawSlippage={rawSlippage}
slippageWarning={slippageWarning}
highSlippageWarning={highSlippageWarning}
brokenTokenWarning={brokenTokenWarning}
setDeadline={setDeadlineFromNow}
deadline={deadlineFromNow}
inputError={inputError}
independentError={independentError}
inputCurrency={inputCurrency}
outputCurrency={outputCurrency}
independentValue={independentValue}
independentValueParsed={independentValueParsed}
independentField={independentField}
INPUT={INPUT}
inputValueParsed={inputValueParsed}
outputValueParsed={outputValueParsed}
inputSymbol={inputSymbol}
outputSymbol={outputSymbol}
dependentValueMinumum={dependentValueMinumum}
dependentValueMaximum={dependentValueMaximum}
dependentDecimals={dependentDecimals}
independentDecimals={independentDecimals}
percentSlippageFormatted={percentSlippageFormatted}
setcustomSlippageError={setcustomSlippageError}
recipientAddress={recipient.address}
sending={sending}
/>
<Flex>
<Button
disabled={
brokenTokenWarning ? true : !account && !error ? false : !isValid || customSlippageError === 'invalid'
}
onClick={account && !error ? onSwap : toggleWalletModal}
warning={highSlippageWarning || customSlippageError === 'warning'}
loggedOut={!account}
>
{brokenTokenWarning
? 'Swap'
: !account
? 'Connect to a Wallet'
: sending
? highSlippageWarning || customSlippageError === 'warning'
? t('sendAnyway')
: t('send')
: highSlippageWarning || customSlippageError === 'warning'
? t('swapAnyway')
: t('swap')}
</Button>
</Flex>
</>
)
}

@ -0,0 +1,566 @@
import React, { useState, useReducer, useCallback, useEffect } from 'react'
import { ethers } from 'ethers'
import styled from 'styled-components'
import { parseUnits, parseEther } from '@ethersproject/units'
import { WETH, TradeType, Route, Trade, TokenAmount, JSBI } from '@uniswap/sdk'
import { useWeb3React } from '../../hooks'
import { useToken } from '../../contexts/Tokens'
import { useExchange } from '../../contexts/Exchanges'
import { useTransactionAdder } from '../../contexts/Transactions'
import { useAddressBalance } from '../../contexts/Balances'
import { useAddressAllowance } from '../../contexts/Allowances'
import { Text } from 'rebass'
import { ButtonPrimary } from '../Button'
import { AutoColumn, ColumnCenter } from '../../components/Column'
import { RowBetween } from '../../components/Row'
import { ArrowDown, ArrowUp } from 'react-feather'
import CurrencyInputPanel from '../CurrencyInputPanel'
import ConfirmationModal from '../ConfirmationModal'
import { getRouterContract, calculateGasMargin } from '../../utils'
import { TRANSACTION_TYPE } from '../../constants'
const ArrowWrapper = styled.div`
padding: 4px;
border: 1px solid ${({ theme }) => theme.malibuBlue};
border-radius: 12px;
display: flex;
justify-content: center;
align-items: center;
:hover {
cursor: pointer;
opacity: 0.8;
}
`
const ErrorText = styled(Text)`
color: ${({ theme, error }) => (error ? theme.salmonRed : theme.chaliceGray)};
`
enum Field {
INPUT,
OUTPUT
}
interface SwapState {
independentField: Field
typedValue: string
[Field.INPUT]: {
address: string | undefined
}
[Field.OUTPUT]: {
address: string | undefined
}
}
function initializeSwapState(inputAddress?: string, outputAddress?: string): SwapState {
return {
independentField: Field.INPUT,
typedValue: '',
[Field.INPUT]: {
address: inputAddress
},
[Field.OUTPUT]: {
address: '0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735'
}
}
}
enum SwapAction {
SELECT_TOKEN,
SWITCH_TOKENS,
TYPE
}
interface Payload {
[SwapAction.SELECT_TOKEN]: {
field: Field
address: string
}
[SwapAction.SWITCH_TOKENS]: undefined
[SwapAction.TYPE]: {
field: Field
typedValue: string
}
}
function reducer(
state: SwapState,
action: {
type: SwapAction
payload: Payload[SwapAction]
}
): SwapState {
switch (action.type) {
case SwapAction.SELECT_TOKEN: {
const { field, address } = action.payload as Payload[SwapAction.SELECT_TOKEN]
const otherField = field === Field.INPUT ? Field.OUTPUT : Field.INPUT
if (address === state[otherField].address) {
// 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 }
}
} else {
// the normal case
return {
...state,
[field]: { address }
}
}
}
case SwapAction.SWITCH_TOKENS: {
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 }
}
}
case SwapAction.TYPE: {
const { field, typedValue } = action.payload as Payload[SwapAction.TYPE]
return {
...state,
independentField: field,
typedValue
}
}
default: {
throw Error
}
}
}
export default function ExchangePage() {
const { chainId, account, library } = useWeb3React()
const [state, dispatch] = useReducer(reducer, WETH[chainId].address, initializeSwapState)
const { independentField, typedValue, ...fieldData } = state
// get derived state
const dependentField = independentField === Field.INPUT ? Field.OUTPUT : Field.INPUT
const tradeType = independentField === Field.INPUT ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT
// get basic SDK entities
const tokens = {
[Field.INPUT]: useToken(fieldData[Field.INPUT].address),
[Field.OUTPUT]: useToken(fieldData[Field.OUTPUT].address)
}
const exchange = useExchange(tokens[Field.INPUT], tokens[Field.OUTPUT])
const route = !!exchange ? new Route([exchange], tokens[Field.INPUT]) : undefined // no useRoute hook
// get user- and token-specific lookup data
const userBalances = {
[Field.INPUT]: useAddressBalance(account, tokens[Field.INPUT]),
[Field.OUTPUT]: useAddressBalance(account, tokens[Field.OUTPUT])
}
const parsedAmounts: { [field: number]: TokenAmount } = {}
// try to parse typed value
if (typedValue !== '' && typedValue !== '.' && tokens[independentField]) {
try {
const typedValueParsed = parseUnits(typedValue, tokens[independentField].decimals).toString()
if (typedValueParsed !== '0')
parsedAmounts[independentField] = new TokenAmount(tokens[independentField], typedValueParsed)
} catch (error) {
// should only fail if the user specifies too many decimal places of precision (or maybe exceed max uint?)
console.error(error)
}
}
// get trade
let trade: Trade
try {
trade =
!!route && !!parsedAmounts[independentField]
? new Trade(route, parsedAmounts[independentField], tradeType)
: undefined
} catch (error) {
console.error(error)
}
if (trade)
parsedAmounts[dependentField] = tradeType === TradeType.EXACT_INPUT ? trade.outputAmount : trade.inputAmount
// get formatted amounts
const formattedAmounts = {
[independentField]: typedValue,
[dependentField]: parsedAmounts[dependentField] ? parsedAmounts[dependentField].toSignificant(8) : ''
}
const onTokenSelection = useCallback((field: Field, address: string) => {
dispatch({
type: SwapAction.SELECT_TOKEN,
payload: { field, address }
})
}, [])
const onSwapTokens = useCallback(() => {
dispatch({
type: SwapAction.SWITCH_TOKENS,
payload: undefined
})
}, [])
const onUserInput = useCallback((field: Field, typedValue: string) => {
dispatch({ type: SwapAction.TYPE, payload: { field, typedValue } })
}, [])
const onMaxInput = useCallback((typedValue: string) => {
dispatch({
type: SwapAction.TYPE,
payload: {
field: Field.INPUT,
typedValue
}
})
}, [])
const onMaxOutput = useCallback((typedValue: string) => {
dispatch({
type: SwapAction.TYPE,
payload: {
field: Field.OUTPUT,
typedValue
}
})
}, [])
const MIN_ETHER = new TokenAmount(WETH[chainId], JSBI.BigInt(parseEther('.01')))
const maxAmountInput =
!!userBalances[Field.INPUT] &&
JSBI.greaterThan(
userBalances[Field.INPUT].raw,
tokens[Field.INPUT].equals(WETH[chainId]) ? MIN_ETHER.raw : JSBI.BigInt(0)
)
? tokens[Field.INPUT].equals(WETH[chainId])
? userBalances[Field.INPUT].subtract(MIN_ETHER)
: userBalances[Field.INPUT]
: undefined
const atMaxAmountInput =
!!maxAmountInput && !!parsedAmounts[Field.INPUT]
? JSBI.equal(maxAmountInput.raw, parsedAmounts[Field.INPUT].raw)
: undefined
const maxAmountOutput =
!!userBalances[Field.OUTPUT] &&
JSBI.greaterThan(
userBalances[Field.OUTPUT].raw,
tokens[Field.OUTPUT].equals(WETH[chainId]) ? MIN_ETHER.raw : JSBI.BigInt(0)
)
? tokens[Field.OUTPUT].equals(WETH[chainId])
? userBalances[Field.OUTPUT].subtract(MIN_ETHER)
: userBalances[Field.OUTPUT]
: undefined
const atMaxAmountOutput =
!!maxAmountOutput && !!parsedAmounts[Field.OUTPUT]
? JSBI.equal(maxAmountOutput.raw, parsedAmounts[Field.OUTPUT].raw)
: undefined
const [showConfirm, setShowConfirm] = useState(false)
const [pendingConfirmation, toggelPendingConfirmation] = useState(true)
// state for txn
const addTransaction = useTransactionAdder()
const [txHash, setTxHash] = useState()
const SWAP_TYPE = {
EXACT_TOKENS_FOR_TOKENS: 'EXACT_TOKENS_FOR_TOKENS',
EXACT_TOKENS_FOR_ETH: 'EXACT_TOKENS_FOR_ETH',
EXACT_ETH_FOR_TOKENS: 'EXACT_ETH_FOR_TOKENS',
TOKENS_FOR_EXACT_TOKENS: 'TOKENS_FOR_EXACT_TOKENS',
TOKENS_FOR_EXACT_ETH: 'TOKENS_FOR_EXACT_ETH',
ETH_FOR_EXACT_TOKENS: 'ETH_FOR_EXACT_TOKENS'
}
function getSwapType() {
if (tradeType === TradeType.EXACT_INPUT) {
if (tokens[Field.INPUT] === WETH[chainId]) {
return SWAP_TYPE.EXACT_ETH_FOR_TOKENS
} else if (tokens[Field.OUTPUT] === WETH[chainId]) {
return SWAP_TYPE.EXACT_TOKENS_FOR_ETH
} else {
return SWAP_TYPE.EXACT_TOKENS_FOR_TOKENS
}
} else if (tradeType === TradeType.EXACT_OUTPUT) {
if (tokens[Field.INPUT] === WETH[chainId]) {
return SWAP_TYPE.ETH_FOR_EXACT_TOKENS
} else if (tokens[Field.OUTPUT] === WETH[chainId]) {
return SWAP_TYPE.TOKENS_FOR_EXACT_ETH
} else {
return SWAP_TYPE.TOKENS_FOR_EXACT_TOKENS
}
}
}
const ALLOWED_SLIPPAGE = 100
function calculateSlippageAmount(value: TokenAmount): JSBI[] {
if (value && value.raw) {
const offset = JSBI.divide(JSBI.multiply(JSBI.BigInt(ALLOWED_SLIPPAGE), value.raw), JSBI.BigInt(10000))
return [JSBI.subtract(value.raw, offset), JSBI.add(value.raw, offset)]
}
return null
}
function hex(value: JSBI) {
return ethers.utils.bigNumberify(value.toString())
}
const slippageAdjustedAmountsRaw = {
[Field.INPUT]:
Field.INPUT === independentField
? parsedAmounts[Field.INPUT]?.raw
: calculateSlippageAmount(parsedAmounts[Field.INPUT])?.[1],
[Field.OUTPUT]:
Field.OUTPUT === independentField
? parsedAmounts[Field.OUTPUT]?.raw
: calculateSlippageAmount(parsedAmounts[Field.OUTPUT])?.[0]
}
const routerAddress = '0xd9210Ff5A0780E083BB40e30d005d93a2DcFA4EF'
const inputApproval = useAddressAllowance(account, tokens[Field.INPUT], routerAddress)
const outputApproval = useAddressAllowance(account, tokens[Field.OUTPUT], routerAddress)
const [showInputUnlock, setShowInputUnlock] = useState(false)
// monitor parsed amounts and update unlocked buttons
useEffect(() => {
if (
parsedAmounts[Field.INPUT] &&
inputApproval &&
JSBI.greaterThan(parsedAmounts[Field.INPUT].raw, inputApproval.raw)
) {
setShowInputUnlock(true)
} else {
setShowInputUnlock(false)
}
}, [inputApproval, outputApproval, parsedAmounts])
async function onSwap() {
const routerContract = getRouterContract(chainId, library, account)
const path = Object.keys(route.path).map(key => {
return route.path[key].address
})
let estimate: Function, method: Function, args, value
const deadline = 1739591241
const swapType = getSwapType()
switch (swapType) {
case SWAP_TYPE.EXACT_TOKENS_FOR_TOKENS:
estimate = routerContract.estimate.swapExactTokensForTokens
method = routerContract.swapExactTokensForTokens
args = [
slippageAdjustedAmountsRaw[Field.INPUT].toString(),
slippageAdjustedAmountsRaw[Field.OUTPUT].toString(),
path,
account,
deadline
]
value = ethers.constants.Zero
break
case SWAP_TYPE.TOKENS_FOR_EXACT_TOKENS:
estimate = routerContract.estimate.swapTokensForExactTokens
method = routerContract.swapTokensForExactTokens
args = [
slippageAdjustedAmountsRaw[Field.OUTPUT].toString(),
slippageAdjustedAmountsRaw[Field.INPUT].toString(),
path,
account,
deadline
]
value = ethers.constants.Zero
break
case SWAP_TYPE.EXACT_ETH_FOR_TOKENS:
estimate = routerContract.estimate.swapExactETHForTokens
method = routerContract.swapExactETHForTokens
args = [slippageAdjustedAmountsRaw[Field.OUTPUT].toString(), path, account, deadline]
value = hex(slippageAdjustedAmountsRaw[Field.INPUT])
break
case SWAP_TYPE.TOKENS_FOR_EXACT_ETH:
estimate = routerContract.estimate.swapTokensForExactETH
method = routerContract.swapTokensForExactETH
args = [
slippageAdjustedAmountsRaw[Field.OUTPUT].toString(),
slippageAdjustedAmountsRaw[Field.INPUT].toString(),
path,
account,
deadline
]
value = ethers.constants.Zero
break
case SWAP_TYPE.EXACT_TOKENS_FOR_ETH:
estimate = routerContract.estimate.swapExactTokensForETH
method = routerContract.swapExactTokensForETH
args = [
slippageAdjustedAmountsRaw[Field.INPUT].toString(),
slippageAdjustedAmountsRaw[Field.OUTPUT].toString(),
path,
account,
deadline
]
value = ethers.constants.Zero
break
case SWAP_TYPE.ETH_FOR_EXACT_TOKENS:
estimate = routerContract.estimate.swapETHForExactTokens
method = routerContract.swapETHForExactTokens
args = [slippageAdjustedAmountsRaw[Field.OUTPUT].toString(), path, account, deadline]
value = hex(slippageAdjustedAmountsRaw[Field.INPUT])
break
}
const GAS_MARGIN = ethers.utils.bigNumberify(1000)
const estimatedGasLimit = await estimate(...args, { value }).catch(e => {
console.log('error getting gas limit')
})
method(...args, {
value,
gasLimit: calculateGasMargin(estimatedGasLimit, GAS_MARGIN)
})
.then(response => {
setTxHash(response)
addTransaction(response)
toggelPendingConfirmation(false)
})
.catch(e => {
console.log('error when trying transaction')
console.log(e)
setShowConfirm(false)
})
}
// errors
const [inputError, setInputError] = useState()
const [outputError, setOutputError] = useState()
const [errorText, setErrorText] = useState(' ')
const [isError, setIsError] = useState(false)
// update errors live
useEffect(() => {
// reset errors
setInputError(null)
setOutputError(null)
setIsError(false)
if (showInputUnlock) {
setInputError('Need to approve amount on input.')
}
if (
userBalances[Field.INPUT] &&
parsedAmounts[Field.INPUT] &&
JSBI.lessThan(userBalances[Field.INPUT].raw, parsedAmounts[Field.INPUT]?.raw)
) {
setInputError('Insufficient balance.')
setIsError(true)
}
}, [parsedAmounts, showInputUnlock, userBalances])
// set error text based on all errors
useEffect(() => {
setErrorText(null)
if (!parsedAmounts[Field.INPUT]) {
setErrorText('Enter an amount to continue')
} else if (outputError) {
setErrorText(outputError)
} else if (inputError) {
setErrorText(inputError)
return
}
}, [inputError, outputError, parsedAmounts])
// error state for button
const isValid = !errorText
return (
<>
<ConfirmationModal
isOpen={showConfirm}
onDismiss={() => {
setTxHash(null)
setShowConfirm(false)
}}
amount0={parsedAmounts[Field.INPUT]}
amount1={parsedAmounts[Field.OUTPUT]}
price={route?.midPrice}
transactionType={TRANSACTION_TYPE.SWAP}
contractCall={onSwap}
pendingConfirmation={pendingConfirmation}
hash={txHash ? txHash.hash : ''}
/>
<AutoColumn gap={'20px'}>
<CurrencyInputPanel
field={Field.INPUT}
value={formattedAmounts[Field.INPUT]}
onUserInput={onUserInput}
onMax={() => {
maxAmountInput && onMaxInput(maxAmountInput.toExact())
}}
atMax={atMaxAmountInput}
token={tokens[Field.INPUT]}
onTokenSelection={onTokenSelection}
title={'Input'}
error={inputError}
exchange={exchange}
showUnlock={showInputUnlock}
/>
<ColumnCenter>
<ArrowWrapper onClick={onSwapTokens}>
<ArrowDown size="16" color="#2F80ED" />
<ArrowUp size="16" color="#2F80ED" />
</ArrowWrapper>
</ColumnCenter>
<CurrencyInputPanel
field={Field.OUTPUT}
value={formattedAmounts[Field.OUTPUT]}
onUserInput={onUserInput}
onMax={() => {
maxAmountOutput && onMaxOutput(maxAmountOutput.toExact())
}}
atMax={atMaxAmountOutput}
token={tokens[Field.OUTPUT]}
onTokenSelection={onTokenSelection}
title={'Output'}
error={outputError}
exchange={exchange}
disableUnlock
/>
<RowBetween>
Rate:
<div>
{exchange
? `1 ${tokens[Field.INPUT].symbol} = ${route?.midPrice.toSignificant(6)} ${tokens[Field.OUTPUT].symbol}`
: '-'}
</div>
</RowBetween>
<ColumnCenter style={{ height: '20px' }}>
<ErrorText fontSize={12} error={isError}>
{errorText && errorText}
</ErrorText>
</ColumnCenter>
<ButtonPrimary
onClick={() => {
setShowConfirm(true)
}}
disabled={!isValid}
>
<Text fontSize={20} fontWeight={500}>
Swap
</Text>
</ButtonPrimary>
</AutoColumn>
</>
)
}

@ -95,7 +95,14 @@ const HiddenCloseButton = styled.button`
border: none;
`
export default function Modal({ isOpen, onDismiss, minHeight = false, maxHeight = 50, initialFocusRef, children }) {
export default function Modal({
isOpen,
onDismiss,
minHeight = false,
maxHeight = 50,
initialFocusRef = null,
children
}) {
const transitions = useTransition(isOpen, null, {
config: { duration: 200 },
from: { opacity: 0 },

@ -130,16 +130,17 @@ function NavigationTabs({ location: { pathname }, history }) {
useBodyKeyDown('ArrowLeft', navigateLeft)
const adding = pathname.match('/add')
const removing = pathname.match('/remove')
return (
<>
{adding ? (
{adding || removing ? (
<Tabs>
<RowBetween style={{ padding: '1rem' }}>
<HistoryLink to="/supply">
<ArrowLink />
</HistoryLink>
<ActiveText>Add Liquidity</ActiveText>
<ActiveText>{adding ? 'Add' : 'Remove'} Liquidity</ActiveText>
<QuestionHelper text={'helper text'} />
</RowBetween>
</Tabs>
@ -152,7 +153,6 @@ function NavigationTabs({ location: { pathname }, history }) {
))}
</Tabs>
)}
{showBetaMessage && (
<BetaMessage onClick={dismissBetaMessage}>
<span role="img" aria-label="warning">

@ -94,10 +94,13 @@ const PaddedColumn = styled(AutoColumn)`
padding-bottom: 12px;
`
const MenuItem = styled(RowBetween)`
const PaddedItem = styled(RowBetween)`
padding: 4px 24px;
width: calc(100% - 48px);
height: 56px;
`
const MenuItem = styled(PaddedItem)`
cursor: pointer;
:hover {
@ -107,13 +110,19 @@ const MenuItem = styled(RowBetween)`
function SearchModal({ history, isOpen, onDismiss, onTokenSelect, field, urlAddedTokens, filterType }) {
const { t } = useTranslation()
const [searchQuery, setSearchQuery] = useState('')
const { exchangeAddress } = useToken(searchQuery)
const allTokens = useAllTokens()
const { account, chainId } = useWeb3React()
const [searchQuery, setSearchQuery] = useState('')
// get all exchanges
const allExchanges = useAllExchanges()
const exchange = useToken(searchQuery)
const exchangeAddress = exchange && exchange.exchangeAddress
// get all tokens
const allTokens = useAllTokens()
// all balances for both account and exchanges
let allBalances = useAllBalances()
@ -138,11 +147,7 @@ function SearchModal({ history, isOpen, onDismiss, onTokenSelect, field, urlAdde
.map(k => {
let balance
// only update if we have data
if (k === 'ETH' && allBalances[account] && allBalances[account][k] && allBalances[account][k].value) {
balance = allBalances[account][k].value
} else if (allBalances[account] && allBalances[account][k] && allBalances[account][k].value) {
balance = (allBalances[account][k].value, allTokens[k].decimals)
}
balance = allBalances?.[account]?.[k]
return {
name: allTokens[k].name,
symbol: allTokens[k].symbol,
@ -195,9 +200,6 @@ function SearchModal({ history, isOpen, onDismiss, onTokenSelect, field, urlAdde
onDismiss()
}
// get all exchanges
const allExchanges = useAllExchanges()
// amount of tokens to display at once
const [tokensShown, setTokensShown] = useState(0)
const [pairsShown, setPairsShown] = useState(0)
@ -218,32 +220,30 @@ function SearchModal({ history, isOpen, onDismiss, onTokenSelect, field, urlAdde
const filteredPairList = useMemo(() => {
// check if the search is an address
const isAddress = searchQuery.slice(0, 2) === '0x'
return Object.keys(allExchanges).filter(exchangeAddress => {
const exchange = allExchanges[exchangeAddress]
return Object.keys(allExchanges).filter(token0Address => {
return Object.keys(allExchanges[token0Address]).map(token1Address => {
if (searchQuery === '') {
return true
if (searchQuery === '') {
return true
}
const token0 = allTokens[exchange.token0]
const token1 = allTokens[exchange.token1]
const regexMatches = Object.keys(token0).map(field => {
if (
(field === 'address' && isAddress) ||
(field === 'name' && !isAddress) ||
(field === 'symbol' && !isAddress)
) {
return (
token0[field].match(new RegExp(escapeStringRegexp(searchQuery), 'i')) ||
token1[field].match(new RegExp(escapeStringRegexp(searchQuery), 'i'))
)
}
const token0 = allTokens[token0Address]
const token1 = allTokens[token1Address]
const regexMatches = Object.keys(token0).map(field => {
if (
(field === 'address' && isAddress) ||
(field === 'name' && !isAddress) ||
(field === 'symbol' && !isAddress)
) {
return (
token0[field].match(new RegExp(escapeStringRegexp(searchQuery), 'i')) ||
token1[field].match(new RegExp(escapeStringRegexp(searchQuery), 'i'))
)
}
return false
})
return regexMatches.some(m => m)
return false
})
return regexMatches.some(m => m)
})
}, [allExchanges, allTokens, searchQuery])
@ -256,34 +256,39 @@ function SearchModal({ history, isOpen, onDismiss, onTokenSelect, field, urlAdde
}, [filteredPairList])
function renderPairsList() {
if (filteredPairList?.length === 0) {
return (
<PaddedColumn justify="center">
<Text>No Pools Found</Text>
</PaddedColumn>
)
}
return (
filteredPairList &&
filteredPairList.map((token0Address, i) => {
return Object.keys(allExchanges[token0Address]).map(token1Address => {
const token0 = allTokens[token0Address]
const token1 = allTokens[token1Address]
filteredPairList.map((exchangeAddress, i) => {
const token0 = allTokens[allExchanges[exchangeAddress].token0]
const token1 = allTokens[allExchanges[exchangeAddress].token1]
const exchangeAddress = allExchanges[token0Address][token1Address]
const balance = allBalances?.[account]?.[exchangeAddress]?.toSignificant(6)
const balance = allBalances?.[account]?.[exchangeAddress]?.toSignificant(6)
return (
<MenuItem
key={i}
onClick={() => {
history.push('/add/' + token0.address + '-' + token1.address)
onDismiss()
}}
>
<RowFixed>
<DoubleTokenLogo a0={token0?.address || ''} a1={token1?.address || ''} size={24} margin={true} />
<Text fontWeight={500} fontSize={16}>{`${token0?.symbol}/${token1?.symbol}`}</Text>
</RowFixed>
<Text fontWeight={500} fontSize={16}>
{balance ? balance.toString() : '-'}
</Text>
</MenuItem>
)
})
return (
<MenuItem
key={i}
onClick={() => {
history.push('/add/' + token0.address + '-' + token1.address)
onDismiss()
}}
>
<RowFixed>
<DoubleTokenLogo a0={token0?.address || ''} a1={token1?.address || ''} size={24} margin={true} />
<Text fontWeight={500} fontSize={16}>{`${token0?.symbol}/${token1?.symbol}`}</Text>
</RowFixed>
<Text fontWeight={500} fontSize={16}>
{balance ? balance.toString() : '-'}
</Text>
</MenuItem>
)
})
)
}
@ -326,7 +331,7 @@ function SearchModal({ history, isOpen, onDismiss, onTokenSelect, field, urlAdde
</RowFixed>
<AutoColumn gap="4px" justify="end">
{balance ? (
<Text>{balance && (balance > 0 || balance === '<0.0001') ? balance : '-'}</Text>
<Text>{balance ? balance.toSignificant(6) : '-'}</Text>
) : account ? (
<SpinnerWrapper src={Circle} alt="loader" />
) : (
@ -389,6 +394,7 @@ function SearchModal({ history, isOpen, onDismiss, onTokenSelect, field, urlAdde
</RowBetween>
</PaddedColumn>
<div style={{ width: '100%', height: '1px', backgroundColor: '#E1E1E1' }} />
<TokenList>{filterType === 'tokens' ? renderTokenList() : renderPairsList()}</TokenList>
</TokenModal>
</Modal>

@ -1,6 +1,8 @@
import React, { useState } from 'react'
import styled from 'styled-components'
import { isAddress } from '../../utils'
import { useWeb3React } from '../../hooks'
import { WETH } from '@uniswap/sdk'
import { ReactComponent as EthereumLogo } from '../../assets/images/ethereum-logo.svg'
@ -21,8 +23,11 @@ const Emoji = styled.span`
display: flex;
align-items: center;
justify-content: center;
font-size: ${({ size }) => size};
width: ${({ size }) => size};
height: ${({ size }) => size};
margin-bottom: -2px;
margin-right: ${({ sizeraw, margin }) => margin && (sizeraw / 2 + 10).toString() + 'px'};
`
const StyledEthereumLogo = styled(EthereumLogo)`
@ -32,6 +37,17 @@ const StyledEthereumLogo = styled(EthereumLogo)`
export default function TokenLogo({ address, size = '1rem', ...rest }) {
const [error, setError] = useState(false)
const { chainId } = useWeb3React()
// hard code change to show ETH instead of WETH in UI
if (address === WETH[chainId].address) {
address = 'ETH'
}
// remove this just for testing
if (address === isAddress('0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735')) {
address = '0x6b175474e89094c44da98b954eedeac495271d0f'
}
let path = ''
if (address === 'ETH') {

@ -104,7 +104,6 @@ const FancyButton = styled.button`
color: ${({ theme }) => theme.textColor};
align-items: center;
min-width: 55px;
height: 2rem;
border-radius: 36px;
font-size: 12px;
border: 1px solid ${({ theme }) => theme.mercuryGray};
@ -219,7 +218,6 @@ const BottomError = styled.div`
`
const OptionCustom = styled(FancyButton)`
height: 2rem;
position: relative;
width: 120px;
margin-top: 6px;

File diff suppressed because it is too large Load Diff

@ -0,0 +1,929 @@
[
{
"inputs": [
{
"internalType": "address",
"name": "_WETH",
"type": "address"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"payable": true,
"stateMutability": "payable",
"type": "fallback"
},
{
"constant": true,
"inputs": [],
"name": "WETH",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"internalType": "address",
"name": "tokenA",
"type": "address"
},
{
"internalType": "address",
"name": "tokenB",
"type": "address"
},
{
"internalType": "uint256",
"name": "amountADesired",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountBDesired",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountAMin",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountBMin",
"type": "uint256"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
}
],
"name": "addLiquidity",
"outputs": [
{
"internalType": "uint256",
"name": "amountA",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountB",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "liquidity",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "uint256",
"name": "amountTokenDesired",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountTokenMin",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountETHMin",
"type": "uint256"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
}
],
"name": "addLiquidityETH",
"outputs": [
{
"internalType": "uint256",
"name": "amountToken",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountETH",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "liquidity",
"type": "uint256"
}
],
"payable": true,
"stateMutability": "payable",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"internalType": "address",
"name": "tokenA",
"type": "address"
},
{
"internalType": "address",
"name": "tokenB",
"type": "address"
}
],
"name": "exchangeFor",
"outputs": [
{
"internalType": "address",
"name": "exchange",
"type": "address"
}
],
"payable": false,
"stateMutability": "pure",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "factory",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"internalType": "uint256",
"name": "amountOut",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "reserveIn",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "reserveOut",
"type": "uint256"
}
],
"name": "getAmountIn",
"outputs": [
{
"internalType": "uint256",
"name": "amountIn",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "pure",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"internalType": "uint256",
"name": "amountIn",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "reserveIn",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "reserveOut",
"type": "uint256"
}
],
"name": "getAmountOut",
"outputs": [
{
"internalType": "uint256",
"name": "amountOut",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "pure",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"internalType": "uint256",
"name": "amountOut",
"type": "uint256"
},
{
"internalType": "address[]",
"name": "path",
"type": "address[]"
}
],
"name": "getAmountsIn",
"outputs": [
{
"internalType": "uint256[]",
"name": "amounts",
"type": "uint256[]"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"internalType": "uint256",
"name": "amountIn",
"type": "uint256"
},
{
"internalType": "address[]",
"name": "path",
"type": "address[]"
}
],
"name": "getAmountsOut",
"outputs": [
{
"internalType": "uint256[]",
"name": "amounts",
"type": "uint256[]"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"internalType": "address",
"name": "tokenA",
"type": "address"
},
{
"internalType": "address",
"name": "tokenB",
"type": "address"
}
],
"name": "getReserves",
"outputs": [
{
"internalType": "uint256",
"name": "reserveA",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "reserveB",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "initCodeHash",
"outputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"internalType": "uint256",
"name": "amountA",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "reserveA",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "reserveB",
"type": "uint256"
}
],
"name": "quote",
"outputs": [
{
"internalType": "uint256",
"name": "amountB",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "pure",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"internalType": "address",
"name": "tokenA",
"type": "address"
},
{
"internalType": "address",
"name": "tokenB",
"type": "address"
},
{
"internalType": "uint256",
"name": "liquidity",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountAMin",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountBMin",
"type": "uint256"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
}
],
"name": "removeLiquidity",
"outputs": [
{
"internalType": "uint256",
"name": "amountA",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountB",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "uint256",
"name": "liquidity",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountTokenMin",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountETHMin",
"type": "uint256"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
}
],
"name": "removeLiquidityETH",
"outputs": [
{
"internalType": "uint256",
"name": "amountToken",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountETH",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "uint256",
"name": "liquidity",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountTokenMin",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountETHMin",
"type": "uint256"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
},
{
"internalType": "uint8",
"name": "v",
"type": "uint8"
},
{
"internalType": "bytes32",
"name": "r",
"type": "bytes32"
},
{
"internalType": "bytes32",
"name": "s",
"type": "bytes32"
}
],
"name": "removeLiquidityETHWithPermit",
"outputs": [
{
"internalType": "uint256",
"name": "amountToken",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountETH",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"internalType": "address",
"name": "tokenA",
"type": "address"
},
{
"internalType": "address",
"name": "tokenB",
"type": "address"
},
{
"internalType": "uint256",
"name": "liquidity",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountAMin",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountBMin",
"type": "uint256"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
},
{
"internalType": "uint8",
"name": "v",
"type": "uint8"
},
{
"internalType": "bytes32",
"name": "r",
"type": "bytes32"
},
{
"internalType": "bytes32",
"name": "s",
"type": "bytes32"
}
],
"name": "removeLiquidityWithPermit",
"outputs": [
{
"internalType": "uint256",
"name": "amountA",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountB",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"internalType": "address",
"name": "tokenA",
"type": "address"
},
{
"internalType": "address",
"name": "tokenB",
"type": "address"
}
],
"name": "sortTokens",
"outputs": [
{
"internalType": "address",
"name": "token0",
"type": "address"
},
{
"internalType": "address",
"name": "token1",
"type": "address"
}
],
"payable": false,
"stateMutability": "pure",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"internalType": "uint256",
"name": "amountOut",
"type": "uint256"
},
{
"internalType": "address[]",
"name": "path",
"type": "address[]"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
}
],
"name": "swapETHForExactTokens",
"outputs": [
{
"internalType": "uint256[]",
"name": "amounts",
"type": "uint256[]"
}
],
"payable": true,
"stateMutability": "payable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"internalType": "uint256",
"name": "amountOutMin",
"type": "uint256"
},
{
"internalType": "address[]",
"name": "path",
"type": "address[]"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
}
],
"name": "swapExactETHForTokens",
"outputs": [
{
"internalType": "uint256[]",
"name": "amounts",
"type": "uint256[]"
}
],
"payable": true,
"stateMutability": "payable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"internalType": "uint256",
"name": "amountIn",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountOutMin",
"type": "uint256"
},
{
"internalType": "address[]",
"name": "path",
"type": "address[]"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
}
],
"name": "swapExactTokensForETH",
"outputs": [
{
"internalType": "uint256[]",
"name": "amounts",
"type": "uint256[]"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"internalType": "uint256",
"name": "amountIn",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountOutMin",
"type": "uint256"
},
{
"internalType": "address[]",
"name": "path",
"type": "address[]"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
}
],
"name": "swapExactTokensForTokens",
"outputs": [
{
"internalType": "uint256[]",
"name": "amounts",
"type": "uint256[]"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"internalType": "uint256",
"name": "amountOut",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountInMax",
"type": "uint256"
},
{
"internalType": "address[]",
"name": "path",
"type": "address[]"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
}
],
"name": "swapTokensForExactETH",
"outputs": [
{
"internalType": "uint256[]",
"name": "amounts",
"type": "uint256[]"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"internalType": "uint256",
"name": "amountOut",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountInMax",
"type": "uint256"
},
{
"internalType": "address[]",
"name": "path",
"type": "address[]"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
}
],
"name": "swapTokensForExactTokens",
"outputs": [
{
"internalType": "uint256[]",
"name": "amounts",
"type": "uint256[]"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "transferFromSelector",
"outputs": [
{
"internalType": "bytes4",
"name": "",
"type": "bytes4"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "transferSelector",
"outputs": [
{
"internalType": "bytes4",
"name": "",
"type": "bytes4"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
}
]

@ -7,11 +7,25 @@ export const FACTORY_ADDRESSES = {
42: '0xD3E51Ef092B2845f10401a0159B2B96e8B6c3D30'
}
export const ROUTER_ADDRESSES = {
1: '',
3: '',
4: '0xd9210Ff5A0780E083BB40e30d005d93a2DcFA4EF',
42: ''
}
export const SUPPORTED_THEMES = {
DARK: 'DARK',
LIGHT: 'LIGHT'
}
export enum TRANSACTION_TYPE {
SWAP,
SEND,
ADD,
REMOVE
}
const MAINNET_WALLETS = {
INJECTED: {
connector: injected,

@ -1,5 +1,5 @@
import React, { createContext, useContext, useReducer, useMemo, useCallback, useEffect } from 'react'
import { Token, TokenAmount } from '@uniswap/sdk'
import { Token, TokenAmount, WETH } from '@uniswap/sdk'
import { useWeb3React } from '../hooks'
import { safeAccess, isAddress, getTokenAllowance } from '../utils'
@ -60,12 +60,13 @@ export function useAddressAllowance(address: string, token: Token, spenderAddres
const globalBlockNumber = useBlockNumber()
const [state, { update }] = useAllowancesContext()
const { value, blockNumber } = safeAccess(state, [chainId, address, token.address, spenderAddress]) || {}
const { value, blockNumber } = safeAccess(state, [chainId, address, token?.address, spenderAddress]) || {}
useEffect(() => {
if (
isAddress(address) &&
isAddress(token.address) &&
isAddress(token?.address) &&
isAddress(token?.address) !== WETH[chainId].address &&
isAddress(spenderAddress) &&
(value === undefined || blockNumber !== globalBlockNumber) &&
(chainId || chainId === 0) &&
@ -73,15 +74,15 @@ export function useAddressAllowance(address: string, token: Token, spenderAddres
) {
let stale = false
getTokenAllowance(address, token.address, spenderAddress, library)
getTokenAllowance(address, token?.address, spenderAddress, library)
.then(value => {
if (!stale) {
update(chainId, address, token.address, spenderAddress, value, globalBlockNumber)
update(chainId, address, token?.address, spenderAddress, value, globalBlockNumber)
}
})
.catch(() => {
if (!stale) {
update(chainId, address, token.address, spenderAddress, null, globalBlockNumber)
update(chainId, address, token?.address, spenderAddress, null, globalBlockNumber)
}
})
@ -89,7 +90,7 @@ export function useAddressAllowance(address: string, token: Token, spenderAddres
stale = true
}
}
}, [address, token.address, spenderAddress, value, blockNumber, globalBlockNumber, chainId, library, update])
}, [address, token, spenderAddress, value, blockNumber, globalBlockNumber, chainId, library, update])
const newTokenAmount: TokenAmount = value ? new TokenAmount(token, value) : null
return newTokenAmount

@ -1,5 +1,5 @@
import React, { createContext, useContext, useReducer, useRef, useMemo, useCallback, useEffect, ReactNode } from 'react'
import { TokenAmount, Token, JSBI } from '@uniswap/sdk'
import { TokenAmount, Token, JSBI, WETH } from '@uniswap/sdk'
import { useWeb3React, useDebounce } from '../hooks'
import { getEtherBalance, getTokenBalance, isAddress } from '../utils'
@ -13,10 +13,6 @@ const LONG_BLOCK_TIMEOUT = (60 * 15) / 15 // in seconds, represented as a block
const EXCHANGES_BLOCK_TIMEOUT = (60 * 5) / 15 // in seconds, represented as a block number delta
const TRACK_LP_BLANCES = 'TRACK_LP_BLANCES'
const UPDATABLE_KEYS = [TRACK_LP_BLANCES]
interface BalancesState {
[chainId: number]: {
[address: string]: {
@ -42,8 +38,7 @@ enum Action {
STOP_LISTENING,
UPDATE,
BATCH_UPDATE_ACCOUNT,
BATCH_UPDATE_EXCHANGES,
UPDATE_KEY
BATCH_UPDATE_EXCHANGES
}
function reducer(state: BalancesState, { type, payload }: { type: Action; payload: any }) {
@ -147,17 +142,6 @@ function reducer(state: BalancesState, { type, payload }: { type: Action; payloa
}
}
}
case Action.UPDATE_KEY: {
const { key, value } = payload
if (!UPDATABLE_KEYS.some(k => k === key)) {
throw Error(`Unexpected key in LocalStorageContext reducer: '${key}'.`)
} else {
return {
...state,
[key]: value
}
}
}
default: {
throw Error(`Unexpected action type in BalancesContext reducer: '${type}'.`)
}
@ -196,15 +180,11 @@ export default function Provider({ children }: { children: ReactNode }) {
})
}, [])
const updateKey = useCallback((key, value) => {
dispatch({ type: Action.UPDATE_KEY, payload: { key, value } })
}, [])
return (
<BalancesContext.Provider
value={useMemo(
() => [state, { startListening, stopListening, update, batchUpdateAccount, batchUpdateExchanges, updateKey }],
[state, startListening, stopListening, update, batchUpdateAccount, batchUpdateExchanges, updateKey]
() => [state, { startListening, stopListening, update, batchUpdateAccount, batchUpdateExchanges }],
[state, startListening, stopListening, update, batchUpdateAccount, batchUpdateExchanges]
)}
>
{children}
@ -235,15 +215,21 @@ export function Updater() {
// generic balances fetcher abstracting away difference between fetching ETH + token balances
const fetchBalance = useCallback(
(address: string, tokenAddress: string) =>
(tokenAddress === 'ETH' ? getEtherBalance(address, library) : getTokenBalance(tokenAddress, address, library))
(address: string, tokenAddress: string) => {
return (tokenAddress === 'ETH'
? getEtherBalance(address, library)
: address === account && tokenAddress === WETH[chainId].address
? getEtherBalance(address, library)
: getTokenBalance(tokenAddress, address, library)
)
.then(value => {
return value.toString()
})
.catch(() => {
return null
}),
[library]
})
},
[account, chainId, library]
)
// ensure that all balances with >=1 listeners are updated every block
@ -338,22 +324,7 @@ export function Updater() {
}, [chainId, account, blockNumber, allTokens, fetchBalance, batchUpdateAccount])
// ensure token balances for all exchanges
const allExchangeDetails = useAllExchanges()
// format so we can index by exchange and only update specifc values
const allExchanges = useMemo(() => {
const formattedExchanges = {}
Object.keys(allExchangeDetails).map(token0Address => {
return Object.keys(allExchangeDetails[token0Address]).map(token1Address => {
const exchangeAddress = allExchangeDetails[token0Address][token1Address]
return (formattedExchanges[exchangeAddress] = {
token0: token0Address,
token1: token1Address
})
})
})
return formattedExchanges
}, [allExchangeDetails])
const allExchanges = useAllExchanges()
useEffect(() => {
if (typeof chainId === 'number' && typeof blockNumber === 'number') {
@ -443,23 +414,25 @@ export function useAllBalances(): Array<TokenAmount> {
let newBalances = {}
Object.keys(state[chainId]).map(address => {
return Object.keys(state[chainId][address]).map(tokenAddress => {
return (newBalances[chainId] = {
...newBalances[chainId],
[address]: {
...newBalances[chainId]?.[address],
[tokenAddress]: new TokenAmount(
// if token not in token list, must be an exchange -
/**
* @TODO
*
* should we live fetch data here if token not in list
*
*/
allTokens && allTokens[tokenAddress] ? allTokens[tokenAddress] : new Token(chainId, tokenAddress, 18),
JSBI.BigInt(state?.[chainId][address][tokenAddress].value)
)
}
})
if (state?.[chainId][address][tokenAddress].value) {
return (newBalances[chainId] = {
...newBalances[chainId],
[address]: {
...newBalances[chainId]?.[address],
[tokenAddress]: new TokenAmount(
// if token not in token list, must be an exchange -
/**
* @TODO
*
* should we live fetch data here if token not in list
*
*/
allTokens && allTokens[tokenAddress] ? allTokens[tokenAddress] : new Token(chainId, tokenAddress, 18),
JSBI.BigInt(state?.[chainId][address][tokenAddress].value)
)
}
})
}
})
})
return newBalances
@ -476,24 +449,43 @@ export function useAddressBalance(address: string, token: Token): TokenAmount |
const { chainId } = useWeb3React()
const [state, { startListening, stopListening }] = useBalancesContext()
const allTokens = useAllTokens()
/**
* @todo
* when catching for token, causes infinite rerender
* when the token is an exchange liquidity token
*/
useEffect(() => {
if (typeof chainId === 'number' && isAddress(address) && isAddress(token.address)) {
if (typeof chainId === 'number' && isAddress(address) && token && token.address && isAddress(token.address)) {
startListening(chainId, address, token.address)
return () => {
stopListening(chainId, address, token.address)
}
}
}, [chainId, address, token, startListening, stopListening])
}, [chainId, address, startListening, stopListening])
const value = typeof chainId === 'number' ? state?.[chainId]?.[address]?.[token.address]?.value : undefined
const formattedValue = value && new TokenAmount(allTokens?.[token.address], value)
const value = typeof chainId === 'number' ? state?.[chainId]?.[address]?.[token?.address]?.value : undefined
const formattedValue = value && token && new TokenAmount(token, value)
return useMemo(() => formattedValue, [formattedValue])
}
export function useAccountLPBalances(account: string) {
const { chainId } = useWeb3React()
const [, { startListening, stopListening }] = useBalancesContext()
const allExchanges = useAllExchanges()
useEffect(() => {
Object.keys(allExchanges).map(exchangeAddress => {
if (typeof chainId === 'number' && isAddress(account)) {
startListening(chainId, account, exchangeAddress)
return () => {
stopListening(chainId, account, exchangeAddress)
}
}
})
}, [account, allExchanges, chainId, startListening, stopListening])
}
export function useExchangeReserves(tokenAddress: string) {
return []
}

@ -1,6 +1,6 @@
import React, { createContext, useContext, useReducer, useMemo, useCallback, useEffect } from 'react'
import { ChainId, WETH, Token, Exchange } from '@uniswap/sdk'
import { INITIAL_TOKENS_CONTEXT, useToken } from './Tokens'
import { INITIAL_TOKENS_CONTEXT } from './Tokens'
import { useAddressBalance } from './Balances'
import { useWeb3React } from '../hooks'
@ -11,6 +11,10 @@ const ALL_EXCHANGES: [Token, Token][] = [
[
INITIAL_TOKENS_CONTEXT[ChainId.RINKEBY][WETH[ChainId.RINKEBY].address],
INITIAL_TOKENS_CONTEXT[ChainId.RINKEBY]['0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735']
],
[
INITIAL_TOKENS_CONTEXT[ChainId.RINKEBY]['0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735'],
INITIAL_TOKENS_CONTEXT[ChainId.RINKEBY]['0x8ab15C890E5C03B5F240f2D146e3DF54bEf3Df44']
]
]
@ -82,9 +86,9 @@ export function useExchangeAddress(tokenA?: Token, tokenB?: Token): string | und
const { chainId } = useWeb3React()
const [state, { update }] = useExchangesContext()
const tokens: [Token, Token] = tokenA.sortsBefore(tokenB) ? [tokenA, tokenB] : [tokenB, tokenA]
const tokens: [Token, Token] = tokenA && tokenB && tokenA.sortsBefore(tokenB) ? [tokenA, tokenB] : [tokenB, tokenA]
const address = state?.[chainId]?.[tokens[0].address]?.[tokens[1].address]
const address = state?.[chainId]?.[tokens[0]?.address]?.[tokens[1]?.address]
useEffect(() => {
if (address === undefined && tokenA && tokenB) {
@ -111,7 +115,26 @@ export function useAllExchanges() {
const { chainId } = useWeb3React()
const [state] = useExchangesContext()
const allExchangeDetails = state?.[chainId]
const allExchanges = useMemo(() => {
if (!allExchangeDetails) {
return {}
}
const formattedExchanges = {}
Object.keys(allExchangeDetails).map(token0Address => {
return Object.keys(allExchangeDetails[token0Address]).map(token1Address => {
const exchangeAddress = allExchangeDetails[token0Address][token1Address]
return (formattedExchanges[exchangeAddress] = {
token0: token0Address,
token1: token1Address
})
})
})
return formattedExchanges
}, [allExchangeDetails])
return useMemo(() => {
return state?.[chainId] || {}
}, [state, chainId])
return allExchanges || {}
}, [allExchanges])
}

@ -7,7 +7,8 @@ const UPDATE = 'UPDATE'
export const ALL_TOKENS = [
WETH[ChainId.RINKEBY],
new Token(ChainId.RINKEBY, '0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735', 18, 'DAI', 'Dai Stablecoin')
new Token(ChainId.RINKEBY, '0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735', 18, 'DAI', 'Dai Stablecoin'),
new Token(ChainId.RINKEBY, '0x8ab15C890E5C03B5F240f2D146e3DF54bEf3Df44', 18, 'IANV2', 'IAn V2 Coin')
]
// only meant to be used in exchanges.ts!
@ -67,10 +68,11 @@ export function useToken(tokenAddress: string): Token {
const [state, { update }] = useTokensContext()
const allTokensInNetwork = state?.[chainId] || {}
const token = safeAccess(allTokensInNetwork, [tokenAddress]) || {}
const token = safeAccess(allTokensInNetwork, [tokenAddress])
useEffect(() => {
if (
token &&
isAddress(tokenAddress) &&
(token === undefined || token.name === undefined || token.symbol === undefined || token.decimals === undefined) &&
(chainId || chainId === 0) &&
@ -95,6 +97,14 @@ export function useToken(tokenAddress: string): Token {
}
}, [tokenAddress, token, chainId, library, update])
// hard coded change in UI to display WETH as ETH
if (token && token.name === 'WETH') {
token.name = 'ETH'
}
if (token && token.symbol === 'WETH') {
token.symbol = 'ETH'
}
return token
}

@ -122,6 +122,25 @@ export default function App() {
}
}}
/>
<Route
exact
strict
path={'/remove/:tokens'}
component={({ match }) => {
const tokens = match.params.tokens.split('-')
let t0
let t1
if (tokens) {
t0 = tokens[0] === 'ETH' ? 'ETH' : isAddress(tokens[0])
t1 = tokens[1] === 'ETH' ? 'ETH' : isAddress(tokens[1])
}
if (t0 && t1) {
return <Remove params={params} token0={t0} token1={t1} />
} else {
return <Redirect to={{ pathname: '/supply' }} />
}
}}
/>
<Route exaxct path={'/remove'} component={() => <Remove params={params} />} />
</Switch>
</Suspense>

@ -1,32 +1,37 @@
import React, { useReducer, useState, useCallback, useEffect } from 'react'
import { WETH, TokenAmount, JSBI, Percent, Route } from '@uniswap/sdk'
import { parseUnits, parseEther } from '@ethersproject/units'
import styled from 'styled-components'
import { ethers } from 'ethers'
import { parseUnits, parseEther } from '@ethersproject/units'
import { WETH, TokenAmount, JSBI, Percent, Route } from '@uniswap/sdk'
import SearchModal from '../../components/SearchModal'
import DoubleLogo from '../../components/DoubleLogo'
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
import ConfirmationModal from '../../components/ConfirmationModal'
import { Text } from 'rebass'
import { ChevronDown } from 'react-feather'
import { ButtonPrimary, ButtonEmpty } from '../../components/Button'
import ConfirmationModal from '../../components/ConfirmationModal'
import { AutoColumn, ColumnCenter } from '../../components/Column'
import { RowBetween } from '../../components/Row'
import DoubleLogo from '../../components/DoubleLogo'
import { ArrowDown, Plus } from 'react-feather'
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
import { LightCard } from '../../components/Card'
import SearchModal from '../../components/SearchModal'
import { ArrowDown, Plus } from 'react-feather'
import { ButtonPrimary, ButtonEmpty } from '../../components/Button'
import { AutoColumn, ColumnCenter } from '../../components/Column'
import { useWeb3React } from '../../hooks'
import { useToken } from '../../contexts/Tokens'
import { useAddressBalance } from '../../contexts/Balances'
import { useExchange } from '../../contexts/Exchanges'
import { useWeb3React } from '../../hooks'
import { useAddressBalance } from '../../contexts/Balances'
import { useExchangeContract } from '../../hooks'
import { useAddressAllowance } from '../../contexts/Allowances'
import { useTransactionAdder } from '../../contexts/Transactions'
import { TRANSACTION_TYPE, ROUTER_ADDRESSES } from '../../constants'
import { getRouterContract, calculateGasMargin } from '../../utils'
const ErrorText = styled(Text)`
color: ${({ theme, error }) => (error ? theme.salmonRed : theme.chaliceGray)};
`
const ALLOWED_SLIPPAGE = JSBI.BigInt(200)
enum Field {
INPUT,
OUTPUT
@ -101,7 +106,6 @@ function reducer(
}
}
}
case AddAction.TYPE: {
const { field, typedValue } = action.payload as Payload[AddAction.TYPE]
return {
@ -116,15 +120,16 @@ function reducer(
}
}
export default function AddLiquidity() {
// mock to set initial values either from URL or route from supply page
const token1 = '0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735'
const token0 = '0xc778417E063141139Fce010982780140Aa0cD5Ab'
export default function AddLiquidity({ token0, token1 }) {
const { account, chainId, library } = useWeb3React()
const routerAddress = ROUTER_ADDRESSES[chainId]
// modal state
const [showSearch, toggleSearch] = useState(false)
// state for confirmation popup
const [showConfirm, toggleConfirm] = useState(false)
const [pendingConfirmation, toggelPendingConfirmation] = useState(true)
// input state
const [state, dispatch] = useReducer(reducer, initializeAddState(token0, token1))
@ -147,8 +152,42 @@ export default function AddLiquidity() {
[Field.OUTPUT]: useAddressBalance(account, tokens[Field.OUTPUT])
}
// check if no exchange or no liquidity
const [noLiquidity, setNoLiquidity] = useState(false)
useEffect(() => {
if (
exchange &&
JSBI.equal(exchange.reserve0.raw, JSBI.BigInt(0)) &&
JSBI.equal(exchange.reserve1.raw, JSBI.BigInt(0))
) {
setNoLiquidity(true)
}
}, [exchange])
// track non relational amounts if first person to add liquidity
const [nonrelationalAmounts, setNonrelationalAmounts] = useState({
[Field.INPUT]: null,
[Field.OUTPUT]: null
})
useEffect(() => {
if (typedValue !== '' && typedValue !== '.' && tokens[independentField] && noLiquidity) {
const newNonRelationalAmounts = nonrelationalAmounts
const typedValueParsed = parseUnits(typedValue, tokens[independentField].decimals).toString()
if (independentField === Field.OUTPUT) {
newNonRelationalAmounts[Field.OUTPUT] = new TokenAmount(tokens[independentField], typedValueParsed)
} else {
newNonRelationalAmounts[Field.INPUT] = new TokenAmount(tokens[independentField], typedValueParsed)
}
setNonrelationalAmounts(newNonRelationalAmounts)
}
}, [independentField, nonrelationalAmounts, tokens, typedValue, noLiquidity])
const parsedAmounts: { [field: number]: TokenAmount } = {}
// try to parse typed value
//if no liquidity set parsed to non relational, else get dependent calculated amounts
if (noLiquidity) {
parsedAmounts[independentField] = nonrelationalAmounts[independentField]
parsedAmounts[dependentField] = nonrelationalAmounts[dependentField]
}
if (typedValue !== '' && typedValue !== '.' && tokens[independentField]) {
try {
const typedValueParsed = parseUnits(typedValue, tokens[independentField].decimals).toString()
@ -163,6 +202,7 @@ export default function AddLiquidity() {
// get the price data and update dependent field
if (
route &&
!noLiquidity &&
parsedAmounts[independentField] &&
JSBI.greaterThan(parsedAmounts[independentField].raw, JSBI.BigInt(0))
) {
@ -172,25 +212,32 @@ export default function AddLiquidity() {
// get formatted amounts
const formattedAmounts = {
[independentField]: typedValue,
[dependentField]: parsedAmounts[dependentField] ? parsedAmounts[dependentField].toSignificant(8) : ''
[dependentField]: parsedAmounts[dependentField] ? parsedAmounts[dependentField]?.toSignificant(8) : ''
}
// pool token data
const poolToken = useToken(exchange?.address)
const [totalPoolTokens, setTotalPoolTokens] = useState<TokenAmount>()
const exchangeContract = useExchangeContract(exchange?.address)
const fetchPoolTokens = useCallback(() => {
if (exchangeContract) {
exchangeContract.totalSupply().then(totalSupply => {
if (totalSupply !== undefined && poolToken?.decimals) {
const supplyFormatted = JSBI.BigInt(totalSupply)
const tokenSupplyFormatted = new TokenAmount(poolToken, supplyFormatted)
setTotalPoolTokens(tokenSupplyFormatted)
// move this to a hook
const exchangeContract = useExchangeContract(exchange?.liquidityToken.address)
const fetchPoolTokens = useCallback(async () => {
exchangeContract
.deployed()
.then(() => {
if (exchangeContract) {
exchangeContract.totalSupply().then(totalSupply => {
if (totalSupply !== undefined && exchange?.liquidityToken?.decimals) {
const supplyFormatted = JSBI.BigInt(totalSupply)
const tokenSupplyFormatted = new TokenAmount(exchange?.liquidityToken, supplyFormatted)
setTotalPoolTokens(tokenSupplyFormatted)
}
})
}
})
}
}, [exchangeContract, poolToken])
.catch(e => {
console.log('error')
})
}, [exchangeContract])
useEffect(() => {
fetchPoolTokens()
library.on('block', fetchPoolTokens)
@ -200,26 +247,10 @@ export default function AddLiquidity() {
}
}, [fetchPoolTokens, library])
function minTokenAmount(x: JSBI, y: JSBI): JSBI {
return JSBI.lessThan(x, y) ? x : y
}
// check for estimated liquidity minted
const liquidityMinted =
!!poolToken && !!parsedAmounts[Field.INPUT] && !!parsedAmounts[Field.OUTPUT] && !!totalPoolTokens && exchange
? new TokenAmount(
poolToken,
minTokenAmount(
JSBI.divide(
JSBI.multiply(parsedAmounts[Field.INPUT].raw, totalPoolTokens.raw),
exchange.reserveOf(tokens[Field.INPUT]).raw
),
JSBI.divide(
JSBI.multiply(parsedAmounts[Field.OUTPUT].raw, totalPoolTokens.raw),
exchange.reserveOf(tokens[Field.OUTPUT]).raw
)
)
)
!!exchange && !!parsedAmounts[Field.INPUT] && !!parsedAmounts[Field.OUTPUT] && !!totalPoolTokens && exchange
? exchange.getLiquidityMinted(totalPoolTokens, parsedAmounts[Field.INPUT], parsedAmounts[Field.OUTPUT])
: undefined
const poolTokenPercentage =
@ -290,8 +321,33 @@ export default function AddLiquidity() {
? JSBI.equal(maxAmountOutput.raw, parsedAmounts[Field.OUTPUT].raw)
: undefined
// state for confirmation popup
const [showConfirm, toggleConfirm] = useState(false)
const inputApproval = useAddressAllowance(account, tokens[Field.INPUT], routerAddress)
const outputApproval = useAddressAllowance(account, tokens[Field.OUTPUT], routerAddress)
const [showInputUnlock, setShowInputUnlock] = useState(false)
const [showOutputUnlock, setShowOutputUnlock] = useState(false)
// monitor parsed amounts and update unlocked buttons
useEffect(() => {
if (
parsedAmounts[Field.INPUT] &&
inputApproval &&
JSBI.greaterThan(parsedAmounts[Field.INPUT].raw, inputApproval.raw)
) {
setShowInputUnlock(true)
} else {
setShowInputUnlock(false)
}
if (
parsedAmounts[Field.OUTPUT] &&
outputApproval &&
JSBI.greaterThan(parsedAmounts[Field.OUTPUT]?.raw, outputApproval?.raw)
) {
setShowOutputUnlock(true)
} else {
setShowOutputUnlock(false)
}
}, [inputApproval, outputApproval, parsedAmounts])
// errors
const [inputError, setInputError] = useState()
@ -305,6 +361,12 @@ export default function AddLiquidity() {
setInputError(null)
setOutputError(null)
setIsError(false)
if (showInputUnlock) {
setInputError('Need to approve amount on input.')
}
if (showOutputUnlock) {
setOutputError('Need to approve amount on output.')
}
if (parseFloat(parsedAmounts?.[Field.INPUT]?.toExact()) > parseFloat(userBalances?.[Field.INPUT]?.toExact())) {
setInputError('Insufficient balance.')
setIsError(true)
@ -313,7 +375,7 @@ export default function AddLiquidity() {
setOutputError('Insufficient balance.')
setIsError(true)
}
}, [parsedAmounts, userBalances])
}, [parsedAmounts, showInputUnlock, showOutputUnlock, userBalances])
// set error text based on all errors
useEffect(() => {
@ -331,6 +393,48 @@ export default function AddLiquidity() {
// error state for button
const isValid = !errorText
// state for txn
const addTransaction = useTransactionAdder()
const [txHash, setTxHash] = useState()
async function onAdd() {
const router = getRouterContract(chainId, library, account)
const minTokenInput = JSBI.divide(JSBI.multiply(JSBI.BigInt(99), parsedAmounts[Field.INPUT].raw), JSBI.BigInt(100))
const minTokenOutput = JSBI.divide(
JSBI.multiply(JSBI.BigInt(99), parsedAmounts[Field.OUTPUT].raw),
JSBI.BigInt(100)
)
const args = [
tokens[Field.INPUT].address,
tokens[Field.OUTPUT].address,
parsedAmounts[Field.INPUT].raw.toString(),
parsedAmounts[Field.OUTPUT].raw.toString(),
noLiquidity ? parsedAmounts[Field.INPUT].raw.toString() : minTokenInput.toString(),
noLiquidity ? parsedAmounts[Field.OUTPUT].raw.toString() : minTokenOutput.toString(),
account.toString(),
1739591241
]
const estimatedGasLimit = await router.estimate.addLiquidity(...args, {
value: ethers.constants.Zero
})
const GAS_MARGIN = ethers.utils.bigNumberify(1000)
router
.addLiquidity(...args, {
gasLimit: calculateGasMargin(estimatedGasLimit, GAS_MARGIN)
})
.then(response => {
setTxHash(response)
addTransaction(response)
toggelPendingConfirmation(false)
})
.catch(e => {
toggleConfirm(false)
})
}
return (
<>
<ConfirmationModal
@ -338,7 +442,7 @@ export default function AddLiquidity() {
onDismiss={() => {
toggleConfirm(false)
}}
liquidityMinted={liquidityMinted}
liquidityAmount={liquidityMinted}
amount0={
parsedAmounts[independentField]?.token.equals(exchange?.token0)
? parsedAmounts[independentField]
@ -350,7 +454,11 @@ export default function AddLiquidity() {
: parsedAmounts[independentField]
}
poolTokenPercentage={poolTokenPercentage}
price={route?.midPrice}
price={route?.midPrice && route?.midPrice?.raw?.denominator}
transactionType={TRANSACTION_TYPE.ADD}
contractCall={onAdd}
pendingConfirmation={pendingConfirmation}
hash={txHash ? txHash.hash : ''}
/>
<SearchModal
isOpen={showSearch}
@ -375,18 +483,31 @@ export default function AddLiquidity() {
<ChevronDown size={24} />
</RowBetween>
</ButtonEmpty>
{noLiquidity && (
<ColumnCenter>
<Text fontWeight={500} style={{ textAlign: 'center' }}>
<span role="img" aria-label="Thinking">
🥇
</span>{' '}
You are the first to add liquidity. Make sure you're setting rates correctly.
</Text>
</ColumnCenter>
)}
<CurrencyInputPanel
field={Field.INPUT}
value={formattedAmounts[Field.INPUT]}
onUserInput={onUserInput}
onMax={() => {
onMaxInput(maxAmountInput.toExact())
maxAmountInput && onMaxInput(maxAmountInput.toExact())
}}
atMax={atMaxAmountInput}
selectedTokenAddress={tokens[Field.INPUT]?.address}
token={tokens[Field.INPUT]}
onTokenSelection={onTokenSelection}
title={'Deposit'}
error={inputError}
exchange={exchange}
showUnlock={showInputUnlock}
disableTokenSelect
/>
<ColumnCenter>
<Plus size="16" color="#888D9B" />
@ -399,10 +520,13 @@ export default function AddLiquidity() {
onMaxOutput(maxAmountOutput.toExact())
}}
atMax={atMaxAmountOutput}
selectedTokenAddress={tokens[Field.OUTPUT]?.address}
token={tokens[Field.OUTPUT]}
onTokenSelection={onTokenSelection}
title={'Deposit'}
error={outputError}
exchange={exchange}
showUnlock={showOutputUnlock}
disableTokenSelect
/>
<ColumnCenter>
<ArrowDown size="16" color="#888D9B" />
@ -411,7 +535,7 @@ export default function AddLiquidity() {
<AutoColumn gap="10px">
<RowBetween>
Minted pool tokens:
<div>{liquidityMinted ? liquidityMinted.toFixed(6) : '-'}</div>
<div>{liquidityMinted ? liquidityMinted.toExact() : '-'}</div>
</RowBetween>
<RowBetween>
Minted pool share:
@ -420,7 +544,11 @@ export default function AddLiquidity() {
<RowBetween>
Rate:
<div>
1 {exchange?.token0.symbol} = {route?.midPrice.toSignificant(6)} {exchange?.token1.symbol}
1 {exchange?.token0.symbol} ={' '}
{independentField === Field.OUTPUT
? route?.midPrice.invert().toSignificant(6)
: route?.midPrice.toSignificant(6)}{' '}
{exchange?.token1.symbol}
</div>
</RowBetween>
</AutoColumn>

@ -1,481 +0,0 @@
import React, { useState, useEffect, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import ReactGA from 'react-ga'
import { createBrowserHistory } from 'history'
import { ethers } from 'ethers'
import styled from 'styled-components'
import { useWeb3React, useExchangeContract } from '../../hooks'
import { useTransactionAdder } from '../../contexts/Transactions'
import { useToken, INITIAL_TOKENS_CONTEXT } from '../../contexts/Tokens'
import { useAddressBalance } from '../../contexts/Balances'
import { calculateGasMargin, amountFormatter } from '../../utils'
import { brokenTokens } from '../../constants'
import { Button } from '../../theme'
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
import ContextualInfo from '../../components/ContextualInfo'
import OversizedPanel from '../../components/OversizedPanel'
import ArrowDown from '../../assets/svg/SVGArrowDown'
import WarningCard from '../../components/WarningCard'
// denominated in bips
const ALLOWED_SLIPPAGE = ethers.utils.bigNumberify(200)
// denominated in seconds
const DEADLINE_FROM_NOW = 60 * 15
// denominated in bips
const GAS_MARGIN = ethers.utils.bigNumberify(1000)
const BlueSpan = styled.span`
color: ${({ theme }) => theme.royalBlue};
`
const DownArrowBackground = styled.div`
${({ theme }) => theme.flexRowNoWrap}
justify-content: center;
align-items: center;
`
const DownArrow = styled(ArrowDown)`
${({ theme }) => theme.flexRowNoWrap}
color: ${({ theme, active }) => (active ? theme.royalBlue : theme.doveGray)};
width: 0.625rem;
height: 0.625rem;
position: relative;
padding: 0.875rem;
`
const RemoveLiquidityOutput = styled.div`
${({ theme }) => theme.flexRowNoWrap}
min-height: 3.5rem;
`
const RemoveLiquidityOutputText = styled.div`
font-size: 1.25rem;
line-height: 1.5rem;
padding: 1rem 0.75rem;
`
const RemoveLiquidityOutputPlus = styled.div`
font-size: 1.25rem;
line-height: 1.5rem;
padding: 1rem 0;
`
const SummaryPanel = styled.div`
${({ theme }) => theme.flexColumnNoWrap}
padding: 1rem 0;
`
const LastSummaryText = styled.div`
margin-top: 1rem;
`
const ExchangeRateWrapper = styled.div`
${({ theme }) => theme.flexRowNoWrap};
align-items: center;
color: ${({ theme }) => theme.doveGray};
font-size: 0.75rem;
padding: 0.25rem 1rem 0;
`
const ExchangeRate = styled.span`
flex: 1 1 auto;
width: 0;
color: ${({ theme }) => theme.doveGray};
`
const Flex = styled.div`
display: flex;
justify-content: center;
padding: 2rem;
button {
max-width: 20rem;
}
`
function getExchangeRate(inputValue, inputDecimals, outputValue, outputDecimals, invert = false) {
try {
if (
inputValue &&
(inputDecimals || inputDecimals === 0) &&
outputValue &&
(outputDecimals || outputDecimals === 0)
) {
const factor = ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(18))
if (invert) {
return inputValue
.mul(factor)
.mul(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(outputDecimals)))
.div(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(inputDecimals)))
.div(outputValue)
} else {
return outputValue
.mul(factor)
.mul(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(inputDecimals)))
.div(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(outputDecimals)))
.div(inputValue)
}
}
} catch {}
}
function getMarketRate(reserveETH, reserveToken, decimals, invert = false) {
return getExchangeRate(reserveETH, 18, reserveToken, decimals, invert)
}
function calculateSlippageBounds(value) {
if (value) {
const offset = value.mul(ALLOWED_SLIPPAGE).div(ethers.utils.bigNumberify(10000))
const minimum = value.sub(offset)
const maximum = value.add(offset)
return {
minimum: minimum.lt(ethers.constants.Zero) ? ethers.constants.Zero : minimum,
maximum: maximum.gt(ethers.constants.MaxUint256) ? ethers.constants.MaxUint256 : maximum
}
} else {
return {}
}
}
export default function RemoveLiquidity({ params }) {
const { t } = useTranslation()
const { library, account, active, chainId } = useWeb3React()
const addTransaction = useTransactionAdder()
const [brokenTokenWarning, setBrokenTokenWarning] = useState()
// clear url of query
useEffect(() => {
const history = createBrowserHistory()
history.push(window.location.pathname + '')
}, [])
const [outputCurrency, setOutputCurrency] = useState(params.poolTokenAddress)
const [value, setValue] = useState(params.poolTokenAmount ? params.poolTokenAmount : '')
const [inputError, setInputError] = useState()
const [valueParsed, setValueParsed] = useState()
useEffect(() => {
setBrokenTokenWarning(false)
for (let i = 0; i < brokenTokens.length; i++) {
if (brokenTokens[i].toLowerCase() === outputCurrency.toLowerCase()) {
setBrokenTokenWarning(true)
}
}
}, [outputCurrency])
// parse value
useEffect(() => {
try {
const parsedValue = ethers.utils.parseUnits(value, 18)
setValueParsed(parsedValue)
} catch {
if (value !== '') {
setInputError(t('inputNotValid'))
}
}
return () => {
setInputError()
setValueParsed()
}
}, [t, value])
const { symbol, decimals, exchangeAddress } = useToken(outputCurrency)
const [totalPoolTokens, setTotalPoolTokens] = useState()
const poolTokenBalance = useAddressBalance(account, exchangeAddress)
const exchangeETHBalance = useAddressBalance(exchangeAddress, 'ETH')
const exchangeTokenBalance = useAddressBalance(exchangeAddress, outputCurrency)
const urlAddedTokens = {}
if (params.poolTokenAddress) {
urlAddedTokens[params.poolTokenAddress] = true
}
// input validation
useEffect(() => {
if (valueParsed && poolTokenBalance) {
if (valueParsed.gt(poolTokenBalance)) {
setInputError(t('insufficientBalance'))
} else {
setInputError(null)
}
}
}, [poolTokenBalance, t, valueParsed])
const exchange = useExchangeContract(exchangeAddress)
const ownershipPercentage =
poolTokenBalance && totalPoolTokens && !totalPoolTokens.isZero()
? poolTokenBalance.mul(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(18))).div(totalPoolTokens)
: undefined
const ownershipPercentageFormatted = ownershipPercentage && amountFormatter(ownershipPercentage, 16, 4)
const ETHOwnShare =
exchangeETHBalance &&
ownershipPercentage &&
exchangeETHBalance.mul(ownershipPercentage).div(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(18)))
const TokenOwnShare =
exchangeTokenBalance &&
ownershipPercentage &&
exchangeTokenBalance.mul(ownershipPercentage).div(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(18)))
const ETHPer = exchangeETHBalance
? exchangeETHBalance.mul(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(18)))
: undefined
const tokenPer = exchangeTokenBalance
? exchangeTokenBalance.mul(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(18)))
: undefined
const ethWithdrawn =
ETHPer && valueParsed && totalPoolTokens && !totalPoolTokens.isZero()
? ETHPer.mul(valueParsed)
.div(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(18)))
.div(totalPoolTokens)
: undefined
const tokenWithdrawn =
tokenPer && valueParsed && totalPoolTokens && !totalPoolTokens.isZero()
? tokenPer
.mul(valueParsed)
.div(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(18)))
.div(totalPoolTokens)
: undefined
const ethWithdrawnMin = ethWithdrawn ? calculateSlippageBounds(ethWithdrawn).minimum : undefined
const tokenWithdrawnMin = tokenWithdrawn ? calculateSlippageBounds(tokenWithdrawn).minimum : undefined
const fetchPoolTokens = useCallback(() => {
if (exchange) {
exchange.totalSupply().then(totalSupply => {
setTotalPoolTokens(totalSupply)
})
}
}, [exchange])
useEffect(() => {
fetchPoolTokens()
library.on('block', fetchPoolTokens)
return () => {
library.removeListener('block', fetchPoolTokens)
}
}, [fetchPoolTokens, library])
async function onRemoveLiquidity() {
// take ETH amount, multiplied by ETH rate and 2 for total tx size
let ethTransactionSize = (ethWithdrawn / 1e18) * 2
const deadline = Math.ceil(Date.now() / 1000) + DEADLINE_FROM_NOW
const estimatedGasLimit = await exchange.estimate.removeLiquidity(
valueParsed,
ethWithdrawnMin,
tokenWithdrawnMin,
deadline
)
exchange
.removeLiquidity(valueParsed, ethWithdrawnMin, tokenWithdrawnMin, deadline, {
gasLimit: calculateGasMargin(estimatedGasLimit, GAS_MARGIN)
})
.then(response => {
ReactGA.event({
category: 'Transaction',
action: 'Remove Liquidity',
label: outputCurrency,
value: ethTransactionSize,
dimension1: response.hash
})
ReactGA.event({
category: 'Hash',
action: response.hash,
label: ethTransactionSize.toString(),
value: ethTransactionSize
})
addTransaction(response)
})
}
const b = text => <BlueSpan>{text}</BlueSpan>
function renderTransactionDetails() {
return (
<div>
<div>
{t('youAreRemoving')} {b(`${amountFormatter(ethWithdrawn, 18, 4)} ETH`)} {t('and')}{' '}
{b(`${amountFormatter(tokenWithdrawn, decimals, Math.min(decimals, 4))} ${symbol}`)} {t('outPool')}
</div>
<LastSummaryText>
{t('youWillRemove')} {b(amountFormatter(valueParsed, 18, 4))} {t('liquidityTokens')}
</LastSummaryText>
<LastSummaryText>
{t('totalSupplyIs')} {b(amountFormatter(totalPoolTokens, 18, 4))}
</LastSummaryText>
<LastSummaryText>
{t('tokenWorth')} {b(amountFormatter(ETHPer.div(totalPoolTokens), 18, 4))} ETH {t('and')}{' '}
{b(amountFormatter(tokenPer.div(totalPoolTokens), decimals, Math.min(4, decimals)))} {symbol}
</LastSummaryText>
</div>
)
}
function renderSummary() {
let contextualInfo = ''
let isError = false
if (brokenTokenWarning) {
contextualInfo = t('brokenToken')
isError = true
} else if (inputError) {
contextualInfo = inputError
isError = true
} else if (!outputCurrency || outputCurrency === 'ETH') {
contextualInfo = t('selectTokenCont')
} else if (!valueParsed) {
contextualInfo = t('enterValueCont')
} else if (!account) {
contextualInfo = t('noWallet')
isError = true
}
return (
<ContextualInfo
key="context-info"
openDetailsText={t('transactionDetails')}
closeDetailsText={t('hideDetails')}
contextualInfo={contextualInfo}
isError={isError}
renderTransactionDetails={renderTransactionDetails}
/>
)
}
function formatBalance(value) {
return `Balance: ${value}`
}
const isActive = active && account
const isValid = inputError === null
const marketRate = getMarketRate(exchangeETHBalance, exchangeTokenBalance, decimals)
const newOutputDetected =
outputCurrency !== 'ETH' && outputCurrency && !INITIAL_TOKENS_CONTEXT[chainId].hasOwnProperty(outputCurrency)
const [showCustomTokenWarning, setShowCustomTokenWarning] = useState(false)
useEffect(() => {
if (newOutputDetected) {
setShowCustomTokenWarning(true)
} else {
setShowCustomTokenWarning(false)
}
}, [newOutputDetected])
return (
<>
{showCustomTokenWarning && (
<WarningCard
onDismiss={() => {
setShowCustomTokenWarning(false)
}}
urlAddedTokens={urlAddedTokens}
currency={outputCurrency}
/>
)}
<CurrencyInputPanel
title={t('poolTokens')}
extraText={poolTokenBalance && formatBalance(amountFormatter(poolTokenBalance, 18, 4))}
extraTextClickHander={() => {
if (poolTokenBalance) {
const valueToSet = poolTokenBalance
if (valueToSet.gt(ethers.constants.Zero)) {
setValue(amountFormatter(valueToSet, 18, 18, false))
}
}
}}
urlAddedTokens={urlAddedTokens}
onCurrencySelected={setOutputCurrency}
onValueChange={setValue}
value={value}
errorMessage={inputError}
selectedTokenAddress={outputCurrency}
/>
<OversizedPanel>
<DownArrowBackground>
<DownArrow active={isActive} alt="arrow" />
</DownArrowBackground>
</OversizedPanel>
<CurrencyInputPanel
title={t('output')}
description={!!(ethWithdrawn && tokenWithdrawn) ? `(${t('estimated')})` : ''}
key="remove-liquidity-input"
renderInput={() =>
!!(ethWithdrawn && tokenWithdrawn) ? (
<RemoveLiquidityOutput>
<RemoveLiquidityOutputText>
{`${amountFormatter(ethWithdrawn, 18, 4, false)} ETH`}
</RemoveLiquidityOutputText>
<RemoveLiquidityOutputPlus> + </RemoveLiquidityOutputPlus>
<RemoveLiquidityOutputText>
{`${amountFormatter(tokenWithdrawn, decimals, Math.min(4, decimals))} ${symbol}`}
</RemoveLiquidityOutputText>
</RemoveLiquidityOutput>
) : (
<RemoveLiquidityOutput />
)
}
disableTokenSelect
disableUnlock
/>
<OversizedPanel key="remove-liquidity-input-under" hideBottom>
<SummaryPanel>
<ExchangeRateWrapper>
<ExchangeRate>{t('exchangeRate')}</ExchangeRate>
{marketRate ? <span>{`1 ETH = ${amountFormatter(marketRate, 18, 4)} ${symbol}`}</span> : ' - '}
</ExchangeRateWrapper>
<ExchangeRateWrapper>
<ExchangeRate>{t('currentPoolSize')}</ExchangeRate>
{exchangeETHBalance && exchangeTokenBalance && (decimals || decimals === 0) ? (
<span>{`${amountFormatter(exchangeETHBalance, 18, 4)} ETH + ${amountFormatter(
exchangeTokenBalance,
decimals,
Math.min(decimals, 4)
)} ${symbol}`}</span>
) : (
' - '
)}
</ExchangeRateWrapper>
<ExchangeRateWrapper>
<ExchangeRate>
{t('yourPoolShare')} ({ownershipPercentageFormatted && ownershipPercentageFormatted}%)
</ExchangeRate>
{ETHOwnShare && TokenOwnShare ? (
<span>
{`${amountFormatter(ETHOwnShare, 18, 4)} ETH + ${amountFormatter(
TokenOwnShare,
decimals,
Math.min(decimals, 4)
)} ${symbol}`}
</span>
) : (
' - '
)}
</ExchangeRateWrapper>
</SummaryPanel>
</OversizedPanel>
{renderSummary()}
<Flex>
<Button disabled={!isValid} onClick={onRemoveLiquidity}>
{t('removeLiquidity')}
</Button>
</Flex>
</>
)
}

@ -0,0 +1,672 @@
import React, { useReducer, useState, useCallback, useEffect } from 'react'
import { WETH, TokenAmount, JSBI, Route } from '@uniswap/sdk'
import { ethers } from 'ethers'
import { parseUnits, parseEther } from '@ethersproject/units'
import styled from 'styled-components'
import { Text } from 'rebass'
import { ChevronDown } from 'react-feather'
import { ButtonPrimary, ButtonEmpty } from '../../components/Button'
import ConfirmationModal from '../../components/ConfirmationModal'
import { AutoColumn, ColumnCenter } from '../../components/Column'
import { RowBetween } from '../../components/Row'
import DoubleLogo from '../../components/DoubleLogo'
import { ArrowDown, Plus } from 'react-feather'
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
import { LightCard } from '../../components/Card'
import SearchModal from '../../components/SearchModal'
import { useWeb3React } from '../../hooks'
import { useToken } from '../../contexts/Tokens'
import { useAddressBalance, useAllBalances } from '../../contexts/Balances'
import { useExchange } from '../../contexts/Exchanges'
import { useExchangeContract } from '../../hooks'
import { useTransactionAdder } from '../../contexts/Transactions'
import { TRANSACTION_TYPE } from '../../constants'
import { getRouterContract, calculateGasMargin } from '../../utils'
import { splitSignature } from '@ethersproject/bytes'
const ErrorText = styled(Text)`
color: ${({ theme, error }) => (error ? theme.salmonRed : theme.chaliceGray)};
`
enum Field {
POOL,
INPUT,
OUTPUT
}
interface RemoveState {
independentField: Field
typedValue: string
[Field.POOL]: {
address: string | undefined
}
[Field.INPUT]: {
address: string | undefined
}
[Field.OUTPUT]: {
address: string | undefined
}
}
function initializeRemoveState(inputAddress?: string, outputAddress?: string): RemoveState {
return {
independentField: Field.INPUT,
typedValue: '',
[Field.POOL]: {
address: ''
},
[Field.INPUT]: {
address: inputAddress
},
[Field.OUTPUT]: {
address: outputAddress
}
}
}
enum RemoveAction {
SELECT_TOKEN,
SWITCH_TOKENS,
TYPE
}
interface Payload {
[RemoveAction.SELECT_TOKEN]: {
field: Field
address: string
}
[RemoveAction.SWITCH_TOKENS]: undefined
[RemoveAction.TYPE]: {
field: Field
typedValue: string
}
}
function reducer(
state: RemoveState,
action: {
type: RemoveAction
payload: Payload[RemoveAction]
}
): RemoveState {
switch (action.type) {
case RemoveAction.TYPE: {
const { field, typedValue } = action.payload as Payload[RemoveAction.TYPE]
return {
...state,
independentField: field,
typedValue
}
}
default: {
throw Error
}
}
}
export default function RemoveLiquidity({ token0, token1 }) {
// console.log('DEBUG: Rendering')
const { account, chainId, library, connector } = useWeb3React()
// modal state
const [showSearch, toggleSearch] = useState(false)
const [pendingConfirmation, toggelPendingConfirmation] = useState(false)
// input state
const [state, dispatch] = useReducer(reducer, initializeRemoveState(token0, token1))
const { independentField, typedValue, ...fieldData } = state
const inputToken = useToken(fieldData[Field.INPUT].address)
const outputToken = useToken(fieldData[Field.OUTPUT].address)
// get basic SDK entities
const tokens = {
[Field.INPUT]: inputToken,
[Field.OUTPUT]: outputToken
}
const exchange = useExchange(inputToken, outputToken)
const exchangeContract = useExchangeContract(exchange?.liquidityToken.address)
// pool token data
const [totalPoolTokens, setTotalPoolTokens] = useState<TokenAmount>()
// get user- and token-specific lookup data
const userBalances = {
[Field.INPUT]: useAddressBalance(account, tokens[Field.INPUT]),
[Field.OUTPUT]: useAddressBalance(account, tokens[Field.OUTPUT])
}
const allBalances = useAllBalances()
const userLiquidity = allBalances?.[account]?.[exchange.liquidityToken.address]
// state for confirmation popup
const [showConfirm, toggleConfirm] = useState(false)
// errors
const [inputError, setInputError] = useState()
const [outputError, setOutputError] = useState()
const [poolTokenError, setPoolTokenError] = useState()
const [errorText, setErrorText] = useState(' ')
const [isError, setIsError] = useState(false)
const fetchPoolTokens = useCallback(() => {
if (exchangeContract && exchange && exchange.liquidityToken) {
exchangeContract.totalSupply().then(totalSupply => {
if (totalSupply !== undefined) {
const supplyFormatted = JSBI.BigInt(totalSupply)
const tokenSupplyFormatted = new TokenAmount(exchange.liquidityToken, supplyFormatted)
setTotalPoolTokens(tokenSupplyFormatted)
}
})
}
/**
* @todo
*
* when exchange is used here enter infinite loop
*
*/
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [exchangeContract])
useEffect(() => {
fetchPoolTokens()
library.on('block', fetchPoolTokens)
return () => {
library.removeListener('block', fetchPoolTokens)
}
}, [fetchPoolTokens, library])
const TokensDeposited = {
[Field.INPUT]:
exchange &&
totalPoolTokens &&
userLiquidity &&
exchange.getLiquidityValue(tokens[Field.INPUT], totalPoolTokens, userLiquidity, false),
[Field.OUTPUT]:
exchange &&
totalPoolTokens &&
userLiquidity &&
exchange.getLiquidityValue(tokens[Field.OUTPUT], totalPoolTokens, userLiquidity, false)
}
const route = exchange
? new Route([exchange], independentField === Field.POOL ? tokens[Field.INPUT] : tokens[independentField])
: undefined
// parse the amounts based on input
const parsedAmounts: { [field: number]: TokenAmount } = {}
// try to parse typed value
if (independentField === Field.INPUT) {
if (typedValue !== '' && typedValue !== '.' && tokens[Field.INPUT] && exchange && userLiquidity) {
try {
const typedValueParsed = parseUnits(typedValue, tokens[Field.INPUT].decimals).toString()
if (typedValueParsed !== '0') {
parsedAmounts[Field.INPUT] = new TokenAmount(tokens[Field.INPUT], typedValueParsed)
parsedAmounts[Field.OUTPUT] = route.midPrice.quote(parsedAmounts[Field.INPUT])
parsedAmounts[Field.POOL] = exchange.getLiquidityMinted(
totalPoolTokens,
parsedAmounts[Field.INPUT],
parsedAmounts[Field.OUTPUT]
)
}
} catch (error) {
// should only fail if the user specifies too many decimal places of precision (or maybe exceed max uint?)
console.error(error)
}
}
} else if (independentField === Field.OUTPUT) {
if (typedValue !== '' && typedValue !== '.' && tokens[Field.OUTPUT]) {
try {
const typedValueParsed = parseUnits(typedValue, tokens[Field.OUTPUT].decimals).toString()
if (typedValueParsed !== '0') {
parsedAmounts[Field.OUTPUT] = new TokenAmount(tokens[Field.OUTPUT], typedValueParsed)
parsedAmounts[Field.INPUT] = route.midPrice.quote(parsedAmounts[Field.OUTPUT])
parsedAmounts[Field.POOL] = exchange.getLiquidityMinted(
totalPoolTokens,
parsedAmounts[Field.INPUT],
parsedAmounts[Field.OUTPUT]
)
}
} catch (error) {
// should only fail if the user specifies too many decimal places of precision (or maybe exceed max uint?)
console.error(error)
}
}
} else {
if (typedValue !== '' && typedValue !== '.' && exchange) {
try {
const typedValueParsed = parseUnits(typedValue, exchange?.liquidityToken.decimals).toString()
if (typedValueParsed !== '0') {
parsedAmounts[Field.POOL] = new TokenAmount(exchange?.liquidityToken, typedValueParsed)
if (
!JSBI.lessThanOrEqual(parsedAmounts[Field.POOL].raw, totalPoolTokens.raw) ||
!JSBI.lessThanOrEqual(parsedAmounts[Field.POOL].raw, userLiquidity.raw)
) {
} else {
parsedAmounts[Field.INPUT] = exchange.getLiquidityValue(
tokens[Field.INPUT],
totalPoolTokens,
parsedAmounts[Field.POOL],
false
)
parsedAmounts[Field.OUTPUT] = exchange.getLiquidityValue(
tokens[Field.OUTPUT],
totalPoolTokens,
parsedAmounts[Field.POOL],
false
)
}
}
} catch (error) {
// should only fail if the user specifies too many decimal places of precision (or maybe exceed max uint?)
console.error(error)
}
}
}
// get formatted amounts
const formattedAmounts = {
[Field.POOL]:
independentField === Field.POOL
? typedValue
: parsedAmounts[Field.POOL]
? parsedAmounts[Field.POOL].toSignificant(8)
: '',
[Field.INPUT]:
independentField === Field.INPUT
? typedValue
: parsedAmounts[Field.INPUT]
? parsedAmounts[Field.INPUT].toSignificant(8)
: '',
[Field.OUTPUT]:
independentField === Field.OUTPUT
? typedValue
: parsedAmounts[Field.OUTPUT]
? parsedAmounts[Field.OUTPUT].toSignificant(8)
: ''
}
const onTokenSelection = useCallback((field: Field, address: string) => {
dispatch({
type: RemoveAction.SELECT_TOKEN,
payload: { field, address }
})
}, [])
// update input value when user types
const onUserInput = useCallback((field: Field, typedValue: string) => {
dispatch({ type: RemoveAction.TYPE, payload: { field, typedValue } })
}, [])
const onMaxInput = useCallback((typedValue: string) => {
dispatch({
type: RemoveAction.TYPE,
payload: {
field: Field.INPUT,
typedValue
}
})
}, [])
const onMaxOutput = useCallback((typedValue: string) => {
dispatch({
type: RemoveAction.TYPE,
payload: {
field: Field.OUTPUT,
typedValue
}
})
}, [])
const onMaxPool = useCallback((typedValue: string) => {
dispatch({
type: RemoveAction.TYPE,
payload: {
field: Field.POOL,
typedValue
}
})
}, [])
const MIN_ETHER = new TokenAmount(WETH[chainId], JSBI.BigInt(parseEther('.01')))
const maxAmountInput =
TokensDeposited[Field.INPUT] &&
JSBI.greaterThan(
TokensDeposited[Field.INPUT].raw,
tokens[Field.INPUT].equals(WETH[chainId]) ? MIN_ETHER.raw : JSBI.BigInt(0)
)
? tokens[Field.INPUT].equals(WETH[chainId])
? TokensDeposited[Field.INPUT].subtract(MIN_ETHER)
: TokensDeposited[Field.INPUT]
: undefined
const atMaxAmountInput =
!!maxAmountInput && !!parsedAmounts[Field.INPUT]
? JSBI.equal(maxAmountInput.raw, parsedAmounts[Field.INPUT].raw)
: undefined
const maxAmountOutput =
!!userBalances[Field.OUTPUT] &&
TokensDeposited[Field.OUTPUT] &&
JSBI.greaterThan(
TokensDeposited[Field.OUTPUT]?.raw,
tokens[Field.OUTPUT].equals(WETH[chainId]) ? MIN_ETHER.raw : JSBI.BigInt(0)
)
? tokens[Field.OUTPUT].equals(WETH[chainId])
? TokensDeposited[Field.OUTPUT].subtract(MIN_ETHER)
: TokensDeposited[Field.OUTPUT]
: undefined
const atMaxAmountOutput =
!!maxAmountOutput && !!parsedAmounts[Field.OUTPUT]
? JSBI.equal(maxAmountOutput.raw, parsedAmounts[Field.OUTPUT].raw)
: undefined
const maxAmountPoolToken = userLiquidity
const atMaxAmountPoolToken =
!!maxAmountOutput && !!parsedAmounts[Field.POOL]
? JSBI.equal(maxAmountPoolToken.raw, parsedAmounts[Field.POOL].raw)
: undefined
// update errors live
useEffect(() => {
// reset errors
setInputError(null)
setOutputError(null)
setPoolTokenError(null)
setIsError(false)
if (
totalPoolTokens &&
userLiquidity &&
parsedAmounts[Field.POOL] &&
(!JSBI.lessThanOrEqual(parsedAmounts[Field.POOL].raw, totalPoolTokens.raw) ||
!JSBI.lessThanOrEqual(parsedAmounts[Field.POOL].raw, userLiquidity.raw))
) {
setPoolTokenError('Input a liquidity amount less than or equal to your balance.')
setIsError(true)
}
if (parseFloat(parsedAmounts?.[Field.INPUT]?.toExact()) > parseFloat(userBalances?.[Field.INPUT]?.toExact())) {
setInputError('Insufficient balance.')
setIsError(true)
}
if (parseFloat(parsedAmounts?.[Field.OUTPUT]?.toExact()) > parseFloat(userBalances?.[Field.OUTPUT]?.toExact())) {
setOutputError('Insufficient balance.')
setIsError(true)
}
}, [parsedAmounts, totalPoolTokens, userBalances, userLiquidity])
// set error text based on all errors
useEffect(() => {
setErrorText(null)
if (poolTokenError) {
setErrorText(poolTokenError)
} else if (!parsedAmounts[Field.INPUT]) {
setErrorText('Enter an amount to continue')
} else if (outputError) {
setErrorText(outputError)
} else if (inputError) {
setErrorText(inputError)
return
}
}, [inputError, outputError, parsedAmounts, poolTokenError])
// error state for button
const isValid = !errorText
// state for txn
const addTransaction = useTransactionAdder()
const [txHash, setTxHash] = useState()
// mock to set initial values either from URL or route from supply page
const routerAddress = '0xd9210Ff5A0780E083BB40e30d005d93a2DcFA4EF'
const router = getRouterContract(chainId, library, account)
const [sigInputs, setSigInputs] = useState([])
async function onSign() {
const nonce = await exchangeContract.nonces(account)
const deadline = 1739591241
const EIP712Domain = [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' }
]
const domain = {
name: 'Uniswap V2',
version: '1',
chainId: chainId,
verifyingContract: exchange.liquidityToken.address
}
const Permit = [
{ name: 'owner', type: 'address' },
{ name: 'spender', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' }
]
const message = {
owner: account,
spender: routerAddress,
value: parsedAmounts[Field.POOL].raw.toString(),
nonce: nonce.toHexString(),
deadline: deadline
}
const data = JSON.stringify({
types: {
EIP712Domain,
Permit
},
domain,
primaryType: 'Permit',
message
})
library.send('eth_signTypedData_v4', [account, data]).then(_signature => {
const signature = splitSignature(_signature)
setSigInputs([signature.v, signature.r, signature.s])
})
}
async function onRemove() {
// now can structure txn
const args = [
tokens[Field.INPUT].address,
tokens[Field.OUTPUT].address,
parsedAmounts[Field.POOL].raw.toString(),
parsedAmounts[Field.INPUT].raw.toString(),
parsedAmounts[Field.OUTPUT].raw.toString(),
account,
1739591241,
sigInputs[0],
sigInputs[1],
sigInputs[2]
]
const estimatedGasLimit = await router.estimate.removeLiquidityWithPermit(...args, {
value: ethers.constants.Zero
})
const GAS_MARGIN = ethers.utils.bigNumberify(1000)
router
.removeLiquidityWithPermit(...args, {
gasLimit: calculateGasMargin(estimatedGasLimit, GAS_MARGIN)
})
.then(response => {
console.log('success')
setTxHash(response.hash)
addTransaction(response.hash)
})
.catch(e => {
console.log('error trying txn')
})
}
return (
<>
<ConfirmationModal
isOpen={showConfirm}
onDismiss={() => {
toggleConfirm(false)
}}
amount0={parsedAmounts[Field.INPUT]}
amount1={parsedAmounts[Field.OUTPUT]}
price={route?.midPrice}
liquidityAmount={parsedAmounts[Field.POOL]}
transactionType={TRANSACTION_TYPE.REMOVE}
contractCall={() => {}}
pendingConfirmation={pendingConfirmation}
hash={''}
/>
<SearchModal
isOpen={showSearch}
onDismiss={() => {
toggleSearch(false)
}}
/>
<AutoColumn gap="20px">
<ButtonEmpty
padding={'1rem'}
onClick={() => {
toggleSearch(true)
}}
>
<RowBetween>
<DoubleLogo a0={exchange?.token0?.address || ''} a1={exchange?.token1?.address || ''} size={24} />
<Text fontSize={20}>
{exchange?.token0?.symbol && exchange?.token1?.symbol
? exchange?.token0?.symbol + ' / ' + exchange?.token1?.symbol + ' Pool'
: ''}
</Text>
<ChevronDown size={24} />
</RowBetween>
</ButtonEmpty>
<CurrencyInputPanel
field={Field.POOL}
value={formattedAmounts[Field.POOL]}
onUserInput={onUserInput}
onMax={() => {
maxAmountPoolToken && onMaxPool(maxAmountPoolToken.toExact())
}}
atMax={atMaxAmountPoolToken}
onTokenSelection={onTokenSelection}
title={'Deposit'}
error={poolTokenError}
disableTokenSelect
token={exchange?.liquidityToken}
isExchange={true}
exchange={exchange}
/>
<ColumnCenter>
<ArrowDown size="16" color="#888D9B" />
</ColumnCenter>
<CurrencyInputPanel
field={Field.INPUT}
value={formattedAmounts[Field.INPUT]}
onUserInput={onUserInput}
onMax={() => {
maxAmountInput && onMaxInput(maxAmountInput.toExact())
}}
atMax={atMaxAmountInput}
token={tokens[Field.INPUT]}
onTokenSelection={onTokenSelection}
title={'Deposit'}
error={inputError}
disableTokenSelect
customBalance={TokensDeposited[Field.INPUT]}
/>
<ColumnCenter>
<Plus size="16" color="#888D9B" />
</ColumnCenter>
<CurrencyInputPanel
field={Field.OUTPUT}
value={formattedAmounts[Field.OUTPUT]}
onUserInput={onUserInput}
onMax={() => {
maxAmountOutput && onMaxOutput(maxAmountOutput.toExact())
}}
atMax={atMaxAmountOutput}
token={tokens[Field.OUTPUT]}
onTokenSelection={onTokenSelection}
title={'Deposit'}
error={outputError}
disableTokenSelect
customBalance={TokensDeposited[Field.OUTPUT]}
/>
<ColumnCenter>
<ArrowDown size="16" color="#888D9B" />
</ColumnCenter>
<LightCard>
<AutoColumn gap="10px">
<RowBetween>
Pool Tokens Burned:
<div>{formattedAmounts[Field.POOL] ? formattedAmounts[Field.POOL] : '-'}</div>
</RowBetween>
<RowBetween>
{exchange?.token0.symbol} Removed:
<div>{formattedAmounts[Field.INPUT] ? formattedAmounts[Field.INPUT] : '-'}</div>
</RowBetween>
<RowBetween>
{exchange?.token1.symbol} Removed:
<div>{formattedAmounts[Field.OUTPUT] ? formattedAmounts[Field.OUTPUT] : '-'}</div>
</RowBetween>
<RowBetween>
Rate:
<div>
1 {exchange?.token0.symbol} ={' '}
{independentField === Field.INPUT || independentField === Field.POOL
? route?.midPrice.toSignificant(6)
: route?.midPrice.invert().toSignificant(6)}{' '}
{exchange?.token1.symbol}
</div>
</RowBetween>
</AutoColumn>
</LightCard>
<ColumnCenter style={{ height: '20px' }}>
<ErrorText fontSize={12} error={isError}>
{errorText && errorText}
</ErrorText>
</ColumnCenter>
<RowBetween>
<ButtonPrimary
onClick={() => {
// toggleConfirm(true)
onSign()
}}
width="48%"
disabled={!isValid}
>
<Text fontSize={20} fontWeight={500}>
Sign
</Text>
</ButtonPrimary>
<ButtonPrimary
onClick={() => {
// toggleConfirm(true)
onRemove()
}}
width="48%"
disabled={!isValid}
>
<Text fontSize={20} fontWeight={500}>
Remove
</Text>
</ButtonPrimary>
</RowBetween>
</AutoColumn>
</>
)
}

@ -15,7 +15,7 @@ import SearchModal from '../../components/SearchModal'
import { ArrowRight } from 'react-feather'
import { useAllExchanges } from '../../contexts/Exchanges'
import { useAllBalances } from '../../contexts/Balances'
import { useAllBalances, useAccountLPBalances } from '../../contexts/Balances'
import { useWeb3React } from '@web3-react/core'
import { useAllTokens } from '../../contexts/Tokens'
import { useExchangeContract } from '../../hooks'
@ -65,7 +65,7 @@ function ExchangeCard({ exchangeAddress, token0, token1, history, allBalances })
<AutoColumn gap="20px">
<RowBetween>
<RowFixed>
<DoubleLogo a0={token0?.address || ''} a1={token1?.address || ''} margin={true} />
<DoubleLogo a0={token0?.address || ''} a1={token1?.address || ''} margin={true} size={24} />
<Text fontWeight={500} fontSize={20}>
{token0?.symbol}:{token1?.symbol}
</Text>
@ -126,7 +126,7 @@ function ExchangeCard({ exchangeAddress, token0, token1, history, allBalances })
<ButtonSecondary
width="48%"
onClick={() => {
history.push('/remove')
history.push('/remove/' + token0?.address + '-' + token1?.address)
}}
>
Remove
@ -152,38 +152,28 @@ function Supply({ history }) {
const allBalances = useAllBalances()
const filteredPairList = Object.keys(exchanges).map((token0Address, i) => {
return Object.keys(exchanges[token0Address]).map(token1Address => {
const exchangeAddress = exchanges[token0Address][token1Address]
// initiate listener for LP balances
useAccountLPBalances(account)
/**
* we need the users exchnage balance over all exchanges
*
* right now we dont
*
* if they go to supplu page, flip switch to look for balances
*
*
*
*/
// gate on positive address
if (allBalances?.[account]?.[exchangeAddress]) {
const token0 = allTokens[token0Address]
const token1 = allTokens[token1Address]
return (
<ExchangeCard
history={history}
key={i}
exchangeAddress={exchangeAddress}
token0={token0}
token1={token1}
allBalances={allBalances}
/>
)
}
const filteredPairList = Object.keys(exchanges).map((exchangeAddress, i) => {
const exchange = exchanges[exchangeAddress]
// gate on positive address
if (allBalances?.[account]?.[exchangeAddress]) {
const token0 = allTokens[exchange.token0]
const token1 = allTokens[exchange.token1]
return (
<ExchangeCard
history={history}
key={i}
exchangeAddress={exchangeAddress}
token0={token0}
token1={token1}
allBalances={allBalances}
/>
)
} else {
return ''
})
}
})
return (

@ -2,9 +2,19 @@ import { ethers } from 'ethers'
import FACTORY_ABI from '../constants/abis/factory'
import EXCHANGE_ABI from '../constants/abis/exchange'
import ROUTER_ABI from '../constants/abis/router'
import ERC20_ABI from '../constants/abis/erc20'
import ERC20_BYTES32_ABI from '../constants/abis/erc20_bytes32'
import { FACTORY_ADDRESSES, SUPPORTED_THEMES } from '../constants'
import {
BigNumber,
bigNumberify,
getAddress,
keccak256,
defaultAbiCoder,
toUtf8Bytes,
solidityPack
} from 'ethers/utils'
import UncheckedJsonRpcSigner from './signer'
@ -54,52 +64,6 @@ export function getQueryParam(windowLocation, name) {
export function getAllQueryParams() {
let params = {}
params.theme = checkSupportedTheme(getQueryParam(window.location, 'theme'))
params.inputCurrency = isAddress(getQueryParam(window.location, 'inputCurrency'))
? isAddress(getQueryParam(window.location, 'inputCurrency'))
: ''
params.outputCurrency = isAddress(getQueryParam(window.location, 'outputCurrency'))
? isAddress(getQueryParam(window.location, 'outputCurrency'))
: getQueryParam(window.location, 'outputCurrency') === 'ETH'
? 'ETH'
: ''
params.slippage = !isNaN(getQueryParam(window.location, 'slippage')) ? getQueryParam(window.location, 'slippage') : ''
params.exactField = getQueryParam(window.location, 'exactField')
params.exactAmount = !isNaN(getQueryParam(window.location, 'exactAmount'))
? getQueryParam(window.location, 'exactAmount')
: ''
params.theme = checkSupportedTheme(getQueryParam(window.location, 'theme'))
params.recipient = isAddress(getQueryParam(window.location, 'recipient'))
? getQueryParam(window.location, 'recipient')
: ''
// Add Liquidity params
params.ethAmount = !isNaN(getQueryParam(window.location, 'ethAmount'))
? getQueryParam(window.location, 'ethAmount')
: ''
params.tokenAmount = !isNaN(getQueryParam(window.location, 'tokenAmount'))
? getQueryParam(window.location, 'tokenAmount')
: ''
params.token = isAddress(getQueryParam(window.location, 'token'))
? isAddress(getQueryParam(window.location, 'token'))
: ''
// Remove liquidity params
params.poolTokenAmount = !isNaN(getQueryParam(window.location, 'poolTokenAmount'))
? getQueryParam(window.location, 'poolTokenAmount')
: ''
params.poolTokenAddress = isAddress(getQueryParam(window.location, 'poolTokenAddress'))
? isAddress(getQueryParam(window.location, 'poolTokenAddress'))
? isAddress(getQueryParam(window.location, 'poolTokenAddress'))
: ''
: ''
// Create Exchange params
params.tokenAddress = isAddress(getQueryParam(window.location, 'tokenAddress'))
? isAddress(getQueryParam(window.location, 'tokenAddress'))
: ''
return params
}
@ -153,8 +117,11 @@ export function isAddress(value) {
}
export function calculateGasMargin(value, margin) {
const offset = value.mul(margin).div(ethers.utils.bigNumberify(10000))
return value.add(offset)
if (value) {
const offset = value.mul(margin).div(ethers.utils.bigNumberify(10000))
return value.add(offset)
}
return null
}
// account is optional
@ -171,6 +138,12 @@ export function getContract(address, ABI, library, account) {
return new ethers.Contract(address, ABI, getProviderOrSigner(library, account))
}
// account is optional
export function getRouterContract(networkId, library, account) {
const router = getContract('0xd9210Ff5A0780E083BB40e30d005d93a2DcFA4EF', ROUTER_ABI, library, account)
return router
}
// account is optional
export function getFactoryContract(networkId, library, account) {
return getContract(FACTORY_ADDRESSES[networkId], FACTORY_ABI, library, account)
@ -245,20 +218,6 @@ export async function getEtherBalance(address, library) {
return library.getBalance(address)
}
export function formatEthBalance(balance) {
return amountFormatter(balance, 18, 6)
}
export function formatTokenBalance(balance, decimal) {
return !!(balance && Number.isInteger(decimal)) ? amountFormatter(balance, decimal, Math.min(4, decimal)) : 0
}
export function formatToUsd(price) {
const format = { decimalSeparator: '.', groupSeparator: ',', groupSize: 3 }
const usdPrice = 1
return usdPrice
}
// get the token balance of an address
export async function getTokenBalance(tokenAddress, address, library) {
if (!isAddress(tokenAddress) || !isAddress(address)) {
@ -280,61 +239,46 @@ export async function getTokenAllowance(address, tokenAddress, spenderAddress, l
return getContract(tokenAddress, ERC20_ABI, library).allowance(address, spenderAddress)
}
// amount must be a BigNumber, {base,display}Decimals must be Numbers
export function amountFormatter(amount, baseDecimals = 18, displayDecimals = 3, useLessThan = true) {
if (baseDecimals > 18 || displayDecimals > 18 || displayDecimals > baseDecimals) {
throw Error(`Invalid combination of baseDecimals '${baseDecimals}' and displayDecimals '${displayDecimals}.`)
}
const PERMIT_TYPEHASH = keccak256(
toUtf8Bytes('Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)')
)
// if balance is falsy, return undefined
if (!amount) {
return undefined
}
// if amount is 0, return
else if (amount.isZero()) {
return '0'
}
// amount > 0
else {
// amount of 'wei' in 1 'ether'
const baseAmount = ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(baseDecimals))
const minimumDisplayAmount = baseAmount.div(
ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(displayDecimals))
)
// if balance is less than the minimum display amount
if (amount.lt(minimumDisplayAmount)) {
return useLessThan
? `<${ethers.utils.formatUnits(minimumDisplayAmount, baseDecimals)}`
: `${ethers.utils.formatUnits(amount, baseDecimals)}`
}
// if the balance is greater than the minimum display amount
else {
const stringAmount = ethers.utils.formatUnits(amount, baseDecimals)
// if there isn't a decimal portion
if (!stringAmount.match(/\./)) {
return stringAmount
}
// if there is a decimal portion
else {
const [wholeComponent, decimalComponent] = stringAmount.split('.')
const roundedDecimalComponent = ethers.utils
.bigNumberify(decimalComponent.padEnd(baseDecimals, '0'))
.toString()
.padStart(baseDecimals, '0')
.substring(0, displayDecimals)
// decimals are too small to show
if (roundedDecimalComponent === '0'.repeat(displayDecimals)) {
return wholeComponent
}
// decimals are not too small to show
else {
return `${wholeComponent}.${roundedDecimalComponent.toString().replace(/0*$/, '')}`
}
}
}
}
export function expandTo18Decimals(n) {
return bigNumberify(n).mul(bigNumberify(10).pow(18))
}
function getDomainSeparator(name, tokenAddress) {
return keccak256(
defaultAbiCoder.encode(
['bytes32', 'bytes32', 'bytes32', 'uint256', 'address'],
[
keccak256(toUtf8Bytes('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)')),
keccak256(toUtf8Bytes(name)),
keccak256(toUtf8Bytes('1')),
1,
tokenAddress
]
)
)
}
export async function getApprovalDigest(token, approve, nonce, deadline) {
const name = await token.name()
const DOMAIN_SEPARATOR = getDomainSeparator(name, token.address)
return keccak256(
solidityPack(
['bytes1', 'bytes1', 'bytes32', 'bytes32'],
[
'0x19',
'0x01',
DOMAIN_SEPARATOR,
keccak256(
defaultAbiCoder.encode(
['bytes32', 'address', 'address', 'uint256', 'uint256', 'uint256'],
[PERMIT_TYPEHASH, approve.owner, approve.spender, approve.value, nonce, deadline]
)
)
]
)
)
}

@ -2288,10 +2288,10 @@
semver "^6.3.0"
tsutils "^3.17.1"
"@uniswap/sdk@next":
version "2.0.0-beta.15"
resolved "https://registry.yarnpkg.com/@uniswap/sdk/-/sdk-2.0.0-beta.15.tgz#66487b8402fbc5b083c3744a890e64fb4024409c"
integrity sha512-Vj4r0EBr1eHaOV7OfwqVUjkbNsB93xRfJuCpEx2+OitDLWkM+arkSnw1jcixPZ7Dt+GTPXflgXGv3rB4s+ODog==
"@uniswap/sdk@@uniswap/sdk@2.0.0-beta.17":
version "2.0.0-beta.17"
resolved "https://registry.yarnpkg.com/@uniswap/sdk/-/sdk-2.0.0-beta.17.tgz#8f24be0375d5f8137eae75afe75b2356c75bb793"
integrity sha512-Nd3S/VE51z4jsNs9G9hkslUkS862dpslnU86lXEJi7mbbbPIagh31iR0s/uBPnrBFGiktucgvzRn6WJJIvojWA==
dependencies:
"@ethersproject/address" "^5.0.0-beta.134"
"@ethersproject/contracts" "^5.0.0-beta.143"