stable state for swaps, add, and remove eth (no remove token token yet)
This commit is contained in:
parent
753e5f3423
commit
236c3030e1
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
566
src/components/ExchangePage/index.tsx
Normal file
566
src/components/ExchangePage/index.tsx
Normal file
@ -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
929
src/constants/abis/router.json
Normal file
929
src/constants/abis/router.json
Normal file
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
672
src/pages/Supply/RemoveLiquidity.tsx
Normal file
672
src/pages/Supply/RemoveLiquidity.tsx
Normal file
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user