From 236c3030e18c1066c231756bc3defd9be9e803bb Mon Sep 17 00:00:00 2001 From: ianlapham Date: Wed, 4 Mar 2020 13:48:43 -0500 Subject: [PATCH] stable state for swaps, add, and remove eth (no remove token token yet) --- package.json | 2 +- src/components/ConfirmationModal/index.js | 301 +++--- src/components/CurrencyInputPanel/index.js | 151 +-- src/components/ExchangePage/index.jsx | 939 ----------------- src/components/ExchangePage/index.tsx | 566 +++++++++++ src/components/Modal/index.js | 9 +- src/components/NavigationTabs/index.js | 6 +- src/components/SearchModal/index.js | 130 +-- src/components/TokenLogo/index.js | 16 + src/components/TransactionDetails/index.js | 2 - src/constants/abis/exchange.json | 1067 ++++++++++++-------- src/constants/abis/router.json | 929 +++++++++++++++++ src/constants/{index.js => index.ts} | 14 + src/contexts/Allowances.tsx | 15 +- src/contexts/Balances.tsx | 128 ++- src/contexts/Exchanges.tsx | 33 +- src/contexts/Tokens.tsx | 14 +- src/pages/App.js | 19 + src/pages/Supply/AddLiquidity.tsx | 248 +++-- src/pages/Supply/RemoveLiquidity.js | 481 --------- src/pages/Supply/RemoveLiquidity.tsx | 672 ++++++++++++ src/pages/Supply/index.js | 56 +- src/utils/index.js | 180 ++-- yarn.lock | 8 +- 24 files changed, 3606 insertions(+), 2380 deletions(-) delete mode 100644 src/components/ExchangePage/index.jsx create mode 100644 src/components/ExchangePage/index.tsx create mode 100644 src/constants/abis/router.json rename src/constants/{index.js => index.ts} (94%) delete mode 100644 src/pages/Supply/RemoveLiquidity.js create mode 100644 src/pages/Supply/RemoveLiquidity.tsx diff --git a/package.json b/package.json index ab32e234e8..909786c691 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/ConfirmationModal/index.js b/src/components/ConfirmationModal/index.js index 424f8a9874..d464647954 100644 --- a/src/components/ConfirmationModal/index.js +++ b/src/components/ConfirmationModal/index.js @@ -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 ( - + {!confirmed ? (
- You will receive + {transactionType === TRANSACTION_TYPE.SWAP ? 'Confirm Swap' : 'You will receive'} - - - - {liquidityMinted?.toFixed(6)} - - - - - {symbol0 + ':' + symbol1 + ' Pool Tokens'} - - + {transactionType === TRANSACTION_TYPE.SWAP && ( + + + + + {amount0?.toSignificant(6)} + + + + + {symbol0} + + + + + + + + + + + {amount1?.toSignificant(6)} + + + + + {symbol1} + + + + + + )} + {transactionType === TRANSACTION_TYPE.ADD && ( + + + + {liquidityAmount?.toFixed(6)} + + + + + {symbol0 + ':' + symbol1 + ' Pool Tokens'} + + + )} + {transactionType === TRANSACTION_TYPE.REMOVE && ( + + + + + {symbol0} {amount0?.toSignificant(8)} + + + + + + {symbol1} {amount1?.toSignificant(8)} + + + + )}
- {/* - Deposited Tokens - */} - {/* - - - {amountFormatter(amount0, decimals0, 4)} - - - - - {symbol0} - - - - - - - - - - - {amountFormatter(amount1, decimals1, 4)} - - - - - {symbol1} - - - - */} - - - {symbol0} Deposited - - - - - {amount0?.toSignificant(6)} + {transactionType === TRANSACTION_TYPE.ADD && ( + <> + + + {symbol0} Deposited + + + + + {amount0?.toSignificant(6)} + + + + + + {symbol1} Deposited + + + + + {amount1?.toSignificant(6)} + + + + + )} + {transactionType === TRANSACTION_TYPE.REMOVE && ( + + + {'UNI ' + symbol0 + ':' + symbol1} Burned - - - - - {symbol1} Deposited - - - - - {amount1?.toSignificant(6)} + + + + {liquidityAmount?.toSignificant(6)} + + + + )} + {price && price?.adjusted && ( + + + Rate - - - - - Rate - - - {price && `1 ${symbol0} = ${price?.adjusted.toFixed(8)} ${symbol1}`} - - - - - Minted Pool Share: - - - {poolTokenPercentage?.toFixed(6) + '%'} - - + + {`1 ${symbol0} = ${price.adjusted.toFixed(8)} ${symbol1}`} + + + )} + {transactionType === TRANSACTION_TYPE.ADD && poolTokenPercentage && ( + + + Minted Pool Share: + + + {poolTokenPercentage?.toFixed(6) + '%'} + + + )} { - setWaitingForConfirmation(true) - SetConfirmed(true) - fakeCall() + setConfirmed(true) + contractCall() }} > - Confirm Supply + Confirm{' '} + {transactionType === TRANSACTION_TYPE.ADD + ? 'Supply' + : transactionType === TRANSACTION_TYPE.REMOVE + ? 'Remove' + : 'Swap'} - - {`Output is estimated. You will receive at least ${liquidityMinted?.toFixed( - 6 - )} UNI ${symbol0}/${symbol1} or the transaction will revert.`} - + {transactionType === TRANSACTION_TYPE.ADD && ( + + {`Output is estimated. You will receive at least ${liquidityAmount?.toFixed( + 6 + )} UNI ${symbol0}/${symbol1} or the transaction will revert.`} + + )} + {transactionType === TRANSACTION_TYPE.REMOVE && ( + + {`Output is estimated. You will receive at least ${amount0?.toSignificant( + 6 + )} ${symbol0} at least ${amount1?.toSignificant(6)} ${symbol1} or the transaction will revert.`} + + )} + {transactionType === TRANSACTION_TYPE.SWAP && ( + + {`Output is estimated. You will receive at least ${amount1?.toSignificant( + 6 + )} ${symbol1} or the transaction will revert.`} + + )}
@@ -176,25 +243,33 @@ export default function ConfirmationModal({ - {waitingForConfirmation ? : } + {pendingConfirmation ? : } - {!waitingForConfirmation ? 'Transaction Submitted' : 'Waiting For Confirmation'} + {!pendingConfirmation ? 'Transaction Submitted' : 'Waiting For Confirmation'} - Supplied + {transactionType === TRANSACTION_TYPE.ADD + ? 'Supplied' + : transactionType === TRANSACTION_TYPE.REMOVE + ? 'Removed' + : 'Swapped'} - {`${amount0?.toSignificant(6)} ${symbol0} and ${amount1?.toSignificant(6)} ${symbol1}`} + {`${amount0?.toSignificant(6)} ${symbol0} ${ + transactionType === TRANSACTION_TYPE.SWAP ? 'for' : 'and' + } ${amount1?.toSignificant(6)} ${symbol1}`} - {!waitingForConfirmation && ( + {!pendingConfirmation && ( <> - - View on Etherscan - + + + View on Etherscan + + Close @@ -202,9 +277,9 @@ export default function ConfirmationModal({ )} - {waitingForConfirmation &&
} + {pendingConfirmation &&
} - {waitingForConfirmation + {pendingConfirmation ? 'Confirm this transaction in your wallet' : `Estimated time until confirmation: 3 min`} diff --git a/src/components/CurrencyInputPanel/index.js b/src/components/CurrencyInputPanel/index.js index 33c48f1d82..2f8b5d931e 100644 --- a/src/components/CurrencyInputPanel/index.js +++ b/src/components/CurrencyInputPanel/index.js @@ -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 ( - - - - {title} - {}}> - - Balance: {userTokenBalance?.toFixed(2)} - - - + + {!hideBalance && ( + + + {title} + {}}> + + + Balance: {customBalance ? customBalance.toSignificant(4) : userTokenBalance?.toSignificant(4)} + + + + + )} - {!!selectedTokenAddress && !atMax && showMax && MAX} + {!!token?.address && !atMax && showMax && MAX} {renderUnlockButton()} { if (!disableTokenSelect) { - setModalIsOpen(true) + setModalOpen(true) } }} disableTokenSelect={disableTokenSelect} > - {selectedTokenAddress ? : null} - { + {isExchange ? ( + + ) : token?.address ? ( + + ) : null} + {isExchange ? ( - {(allTokens[selectedTokenAddress] && allTokens[selectedTokenAddress].symbol) || t('selectToken')} + {exchange?.token0.symbol}:{exchange?.token1.symbol} - } - {!disableTokenSelect && } + ) : ( + {(token && token.symbol) || t('selectToken')} + )} + {!disableTokenSelect && } {!disableTokenSelect && ( { - setModalIsOpen(false) + setModalOpen(false) }} filterType="tokens" urlAddedTokens={urlAddedTokens} diff --git a/src/components/ExchangePage/index.jsx b/src/components/ExchangePage/index.jsx deleted file mode 100644 index 64587240d7..0000000000 --- a/src/components/ExchangePage/index.jsx +++ /dev/null @@ -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 }) => -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 && ( - { - setShowInputWarning(false) - }} - urlAddedTokens={urlAddedTokens} - currency={inputCurrency} - /> - )} - {showOutputWarning && ( - { - setShowOutputWarning(false) - }} - urlAddedTokens={urlAddedTokens} - currency={outputCurrency} - /> - )} - { - 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 : ''} - /> - - - { - dispatchSwapState({ type: 'FLIP_INDEPENDENT' }) - }} - clickable - alt="swap" - active={isValid} - /> - - - { - 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 ? ( - <> - - - - - - - - ) : ( - '' - )} - - { - setInverted(inverted => !inverted) - }} - > - {t('exchangeRate')} - {inverted ? ( - - {exchangeRate - ? `1 ${inputSymbol} = ${amountFormatter(exchangeRate, 18, 6, false)} ${outputSymbol}` - : ' - '} - - ) : ( - - {exchangeRate - ? `1 ${outputSymbol} = ${amountFormatter(exchangeRateInverted, 18, 6, false)} ${inputSymbol}` - : ' - '} - - )} - - - - - - - - ) -} diff --git a/src/components/ExchangePage/index.tsx b/src/components/ExchangePage/index.tsx new file mode 100644 index 0000000000..b35d467ad1 --- /dev/null +++ b/src/components/ExchangePage/index.tsx @@ -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 ( + <> + { + 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 : ''} + /> + + { + maxAmountInput && onMaxInput(maxAmountInput.toExact()) + }} + atMax={atMaxAmountInput} + token={tokens[Field.INPUT]} + onTokenSelection={onTokenSelection} + title={'Input'} + error={inputError} + exchange={exchange} + showUnlock={showInputUnlock} + /> + + + + + + + { + maxAmountOutput && onMaxOutput(maxAmountOutput.toExact()) + }} + atMax={atMaxAmountOutput} + token={tokens[Field.OUTPUT]} + onTokenSelection={onTokenSelection} + title={'Output'} + error={outputError} + exchange={exchange} + disableUnlock + /> + + Rate: +
+ {exchange + ? `1 ${tokens[Field.INPUT].symbol} = ${route?.midPrice.toSignificant(6)} ${tokens[Field.OUTPUT].symbol}` + : '-'} +
+
+ + + {errorText && errorText} + + + { + setShowConfirm(true) + }} + disabled={!isValid} + > + + Swap + + +
+ + ) +} diff --git a/src/components/Modal/index.js b/src/components/Modal/index.js index eb723c0ea1..1d2f612c8e 100644 --- a/src/components/Modal/index.js +++ b/src/components/Modal/index.js @@ -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 }, diff --git a/src/components/NavigationTabs/index.js b/src/components/NavigationTabs/index.js index 245d69c499..807c5abe0a 100644 --- a/src/components/NavigationTabs/index.js +++ b/src/components/NavigationTabs/index.js @@ -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 ? ( - Add Liquidity + {adding ? 'Add' : 'Remove'} Liquidity @@ -152,7 +153,6 @@ function NavigationTabs({ location: { pathname }, history }) { ))} )} - {showBetaMessage && ( diff --git a/src/components/SearchModal/index.js b/src/components/SearchModal/index.js index d37654414c..027c269ed3 100644 --- a/src/components/SearchModal/index.js +++ b/src/components/SearchModal/index.js @@ -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 ( + + No Pools Found + + ) + } + 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 ( - { - history.push('/add/' + token0.address + '-' + token1.address) - onDismiss() - }} - > - - - {`${token0?.symbol}/${token1?.symbol}`} - - - {balance ? balance.toString() : '-'} - - - ) - }) + return ( + { + history.push('/add/' + token0.address + '-' + token1.address) + onDismiss() + }} + > + + + {`${token0?.symbol}/${token1?.symbol}`} + + + {balance ? balance.toString() : '-'} + + + ) }) ) } @@ -326,7 +331,7 @@ function SearchModal({ history, isOpen, onDismiss, onTokenSelect, field, urlAdde {balance ? ( - {balance && (balance > 0 || balance === '<0.0001') ? balance : '-'} + {balance ? balance.toSignificant(6) : '-'} ) : account ? ( ) : ( @@ -389,6 +394,7 @@ function SearchModal({ history, isOpen, onDismiss, onTokenSelect, field, urlAdde
+ {filterType === 'tokens' ? renderTokenList() : renderPairsList()} diff --git a/src/components/TokenLogo/index.js b/src/components/TokenLogo/index.js index 2460527edc..6029fa8c40 100644 --- a/src/components/TokenLogo/index.js +++ b/src/components/TokenLogo/index.js @@ -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') { diff --git a/src/components/TransactionDetails/index.js b/src/components/TransactionDetails/index.js index 2788f46127..04fb1b707a 100644 --- a/src/components/TransactionDetails/index.js +++ b/src/components/TransactionDetails/index.js @@ -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; diff --git a/src/constants/abis/exchange.json b/src/constants/abis/exchange.json index 25cff7558a..53582c1ed6 100644 --- a/src/constants/abis/exchange.json +++ b/src/constants/abis/exchange.json @@ -1,460 +1,713 @@ [ { - "name": "TokenPurchase", - "inputs": [ - { "type": "address", "name": "buyer", "indexed": true }, - { "type": "uint256", "name": "eth_sold", "indexed": true }, - { "type": "uint256", "name": "tokens_bought", "indexed": true } - ], - "anonymous": false, - "type": "event" + "inputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "constructor" }, { - "name": "EthPurchase", - "inputs": [ - { "type": "address", "name": "buyer", "indexed": true }, - { "type": "uint256", "name": "tokens_sold", "indexed": true }, - { "type": "uint256", "name": "eth_bought", "indexed": true } - ], "anonymous": false, - "type": "event" - }, - { - "name": "AddLiquidity", "inputs": [ - { "type": "address", "name": "provider", "indexed": true }, - { "type": "uint256", "name": "eth_amount", "indexed": true }, - { "type": "uint256", "name": "token_amount", "indexed": true } + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } ], - "anonymous": false, - "type": "event" - }, - { - "name": "RemoveLiquidity", - "inputs": [ - { "type": "address", "name": "provider", "indexed": true }, - { "type": "uint256", "name": "eth_amount", "indexed": true }, - { "type": "uint256", "name": "token_amount", "indexed": true } - ], - "anonymous": false, - "type": "event" - }, - { - "name": "Transfer", - "inputs": [ - { "type": "address", "name": "_from", "indexed": true }, - { "type": "address", "name": "_to", "indexed": true }, - { "type": "uint256", "name": "_value", "indexed": false } - ], - "anonymous": false, - "type": "event" - }, - { "name": "Approval", - "inputs": [ - { "type": "address", "name": "_owner", "indexed": true }, - { "type": "address", "name": "_spender", "indexed": true }, - { "type": "uint256", "name": "_value", "indexed": false } - ], - "anonymous": false, "type": "event" }, { - "name": "setup", - "outputs": [], - "inputs": [{ "type": "address", "name": "token_addr" }], - "constant": false, - "payable": false, - "type": "function", - "gas": 175875 - }, - { - "name": "addLiquidity", - "outputs": [{ "type": "uint256", "name": "out" }], + "anonymous": false, "inputs": [ - { "type": "uint256", "name": "min_liquidity" }, - { "type": "uint256", "name": "max_tokens" }, - { "type": "uint256", "name": "deadline" } + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount0", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount1", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + } ], - "constant": false, - "payable": true, - "type": "function", - "gas": 82605 + "name": "Burn", + "type": "event" }, { - "name": "removeLiquidity", - "outputs": [{ "type": "uint256", "name": "out" }, { "type": "uint256", "name": "out" }], + "anonymous": false, "inputs": [ - { "type": "uint256", "name": "amount" }, - { "type": "uint256", "name": "min_eth" }, - { "type": "uint256", "name": "min_tokens" }, - { "type": "uint256", "name": "deadline" } + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount0", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount1", + "type": "uint256" + } ], - "constant": false, - "payable": false, - "type": "function", - "gas": 116814 - }, - { "name": "__default__", "outputs": [], "inputs": [], "constant": false, "payable": true, "type": "function" }, - { - "name": "ethToTokenSwapInput", - "outputs": [{ "type": "uint256", "name": "out" }], - "inputs": [{ "type": "uint256", "name": "min_tokens" }, { "type": "uint256", "name": "deadline" }], - "constant": false, - "payable": true, - "type": "function", - "gas": 12757 + "name": "Mint", + "type": "event" }, { - "name": "ethToTokenTransferInput", - "outputs": [{ "type": "uint256", "name": "out" }], + "anonymous": false, "inputs": [ - { "type": "uint256", "name": "min_tokens" }, - { "type": "uint256", "name": "deadline" }, - { "type": "address", "name": "recipient" } + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount0In", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount1In", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount0Out", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount1Out", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + } ], - "constant": false, - "payable": true, - "type": "function", - "gas": 12965 + "name": "Swap", + "type": "event" }, { - "name": "ethToTokenSwapOutput", - "outputs": [{ "type": "uint256", "name": "out" }], - "inputs": [{ "type": "uint256", "name": "tokens_bought" }, { "type": "uint256", "name": "deadline" }], - "constant": false, - "payable": true, - "type": "function", - "gas": 50463 - }, - { - "name": "ethToTokenTransferOutput", - "outputs": [{ "type": "uint256", "name": "out" }], + "anonymous": false, "inputs": [ - { "type": "uint256", "name": "tokens_bought" }, - { "type": "uint256", "name": "deadline" }, - { "type": "address", "name": "recipient" } + { + "indexed": false, + "internalType": "uint112", + "name": "reserve0", + "type": "uint112" + }, + { + "indexed": false, + "internalType": "uint112", + "name": "reserve1", + "type": "uint112" + } ], - "constant": false, - "payable": true, - "type": "function", - "gas": 50671 + "name": "Sync", + "type": "event" }, { - "name": "tokenToEthSwapInput", - "outputs": [{ "type": "uint256", "name": "out" }], + "anonymous": false, "inputs": [ - { "type": "uint256", "name": "tokens_sold" }, - { "type": "uint256", "name": "min_eth" }, - { "type": "uint256", "name": "deadline" } + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } ], - "constant": false, - "payable": false, - "type": "function", - "gas": 47503 + "name": "Transfer", + "type": "event" }, { - "name": "tokenToEthTransferInput", - "outputs": [{ "type": "uint256", "name": "out" }], - "inputs": [ - { "type": "uint256", "name": "tokens_sold" }, - { "type": "uint256", "name": "min_eth" }, - { "type": "uint256", "name": "deadline" }, - { "type": "address", "name": "recipient" } - ], - "constant": false, - "payable": false, - "type": "function", - "gas": 47712 - }, - { - "name": "tokenToEthSwapOutput", - "outputs": [{ "type": "uint256", "name": "out" }], - "inputs": [ - { "type": "uint256", "name": "eth_bought" }, - { "type": "uint256", "name": "max_tokens" }, - { "type": "uint256", "name": "deadline" } - ], - "constant": false, - "payable": false, - "type": "function", - "gas": 50175 - }, - { - "name": "tokenToEthTransferOutput", - "outputs": [{ "type": "uint256", "name": "out" }], - "inputs": [ - { "type": "uint256", "name": "eth_bought" }, - { "type": "uint256", "name": "max_tokens" }, - { "type": "uint256", "name": "deadline" }, - { "type": "address", "name": "recipient" } - ], - "constant": false, - "payable": false, - "type": "function", - "gas": 50384 - }, - { - "name": "tokenToTokenSwapInput", - "outputs": [{ "type": "uint256", "name": "out" }], - "inputs": [ - { "type": "uint256", "name": "tokens_sold" }, - { "type": "uint256", "name": "min_tokens_bought" }, - { "type": "uint256", "name": "min_eth_bought" }, - { "type": "uint256", "name": "deadline" }, - { "type": "address", "name": "token_addr" } - ], - "constant": false, - "payable": false, - "type": "function", - "gas": 51007 - }, - { - "name": "tokenToTokenTransferInput", - "outputs": [{ "type": "uint256", "name": "out" }], - "inputs": [ - { "type": "uint256", "name": "tokens_sold" }, - { "type": "uint256", "name": "min_tokens_bought" }, - { "type": "uint256", "name": "min_eth_bought" }, - { "type": "uint256", "name": "deadline" }, - { "type": "address", "name": "recipient" }, - { "type": "address", "name": "token_addr" } - ], - "constant": false, - "payable": false, - "type": "function", - "gas": 51098 - }, - { - "name": "tokenToTokenSwapOutput", - "outputs": [{ "type": "uint256", "name": "out" }], - "inputs": [ - { "type": "uint256", "name": "tokens_bought" }, - { "type": "uint256", "name": "max_tokens_sold" }, - { "type": "uint256", "name": "max_eth_sold" }, - { "type": "uint256", "name": "deadline" }, - { "type": "address", "name": "token_addr" } - ], - "constant": false, - "payable": false, - "type": "function", - "gas": 54928 - }, - { - "name": "tokenToTokenTransferOutput", - "outputs": [{ "type": "uint256", "name": "out" }], - "inputs": [ - { "type": "uint256", "name": "tokens_bought" }, - { "type": "uint256", "name": "max_tokens_sold" }, - { "type": "uint256", "name": "max_eth_sold" }, - { "type": "uint256", "name": "deadline" }, - { "type": "address", "name": "recipient" }, - { "type": "address", "name": "token_addr" } - ], - "constant": false, - "payable": false, - "type": "function", - "gas": 55019 - }, - { - "name": "tokenToExchangeSwapInput", - "outputs": [{ "type": "uint256", "name": "out" }], - "inputs": [ - { "type": "uint256", "name": "tokens_sold" }, - { "type": "uint256", "name": "min_tokens_bought" }, - { "type": "uint256", "name": "min_eth_bought" }, - { "type": "uint256", "name": "deadline" }, - { "type": "address", "name": "exchange_addr" } - ], - "constant": false, - "payable": false, - "type": "function", - "gas": 49342 - }, - { - "name": "tokenToExchangeTransferInput", - "outputs": [{ "type": "uint256", "name": "out" }], - "inputs": [ - { "type": "uint256", "name": "tokens_sold" }, - { "type": "uint256", "name": "min_tokens_bought" }, - { "type": "uint256", "name": "min_eth_bought" }, - { "type": "uint256", "name": "deadline" }, - { "type": "address", "name": "recipient" }, - { "type": "address", "name": "exchange_addr" } - ], - "constant": false, - "payable": false, - "type": "function", - "gas": 49532 - }, - { - "name": "tokenToExchangeSwapOutput", - "outputs": [{ "type": "uint256", "name": "out" }], - "inputs": [ - { "type": "uint256", "name": "tokens_bought" }, - { "type": "uint256", "name": "max_tokens_sold" }, - { "type": "uint256", "name": "max_eth_sold" }, - { "type": "uint256", "name": "deadline" }, - { "type": "address", "name": "exchange_addr" } - ], - "constant": false, - "payable": false, - "type": "function", - "gas": 53233 - }, - { - "name": "tokenToExchangeTransferOutput", - "outputs": [{ "type": "uint256", "name": "out" }], - "inputs": [ - { "type": "uint256", "name": "tokens_bought" }, - { "type": "uint256", "name": "max_tokens_sold" }, - { "type": "uint256", "name": "max_eth_sold" }, - { "type": "uint256", "name": "deadline" }, - { "type": "address", "name": "recipient" }, - { "type": "address", "name": "exchange_addr" } - ], - "constant": false, - "payable": false, - "type": "function", - "gas": 53423 - }, - { - "name": "getEthToTokenInputPrice", - "outputs": [{ "type": "uint256", "name": "out" }], - "inputs": [{ "type": "uint256", "name": "eth_sold" }], "constant": true, - "payable": false, - "type": "function", - "gas": 5542 - }, - { - "name": "getEthToTokenOutputPrice", - "outputs": [{ "type": "uint256", "name": "out" }], - "inputs": [{ "type": "uint256", "name": "tokens_bought" }], - "constant": true, - "payable": false, - "type": "function", - "gas": 6872 - }, - { - "name": "getTokenToEthInputPrice", - "outputs": [{ "type": "uint256", "name": "out" }], - "inputs": [{ "type": "uint256", "name": "tokens_sold" }], - "constant": true, - "payable": false, - "type": "function", - "gas": 5637 - }, - { - "name": "getTokenToEthOutputPrice", - "outputs": [{ "type": "uint256", "name": "out" }], - "inputs": [{ "type": "uint256", "name": "eth_bought" }], - "constant": true, - "payable": false, - "type": "function", - "gas": 6897 - }, - { - "name": "tokenAddress", - "outputs": [{ "type": "address", "name": "out" }], "inputs": [], - "constant": true, - "payable": false, - "type": "function", - "gas": 1413 - }, - { - "name": "factoryAddress", - "outputs": [{ "type": "address", "name": "out" }], - "inputs": [], - "constant": true, - "payable": false, - "type": "function", - "gas": 1443 - }, - { - "name": "balanceOf", - "outputs": [{ "type": "uint256", "name": "out" }], - "inputs": [{ "type": "address", "name": "_owner" }], - "constant": true, - "payable": false, - "type": "function", - "gas": 1645 - }, - { - "name": "transfer", - "outputs": [{ "type": "bool", "name": "out" }], - "inputs": [{ "type": "address", "name": "_to" }, { "type": "uint256", "name": "_value" }], - "constant": false, - "payable": false, - "type": "function", - "gas": 75034 - }, - { - "name": "transferFrom", - "outputs": [{ "type": "bool", "name": "out" }], - "inputs": [ - { "type": "address", "name": "_from" }, - { "type": "address", "name": "_to" }, - { "type": "uint256", "name": "_value" } + "name": "DOMAIN_SEPARATOR", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } ], - "constant": false, "payable": false, - "type": "function", - "gas": 110907 + "stateMutability": "view", + "type": "function" }, { - "name": "approve", - "outputs": [{ "type": "bool", "name": "out" }], - "inputs": [{ "type": "address", "name": "_spender" }, { "type": "uint256", "name": "_value" }], - "constant": false, + "constant": true, + "inputs": [], + "name": "MINIMUM_LIQUIDITY", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], "payable": false, - "type": "function", - "gas": 38769 + "stateMutability": "view", + "type": "function" }, { + "constant": true, + "inputs": [], + "name": "PERMIT_TYPEHASH", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "", + "type": "address" + } + ], "name": "allowance", - "outputs": [{ "type": "uint256", "name": "out" }], - "inputs": [{ "type": "address", "name": "_owner" }, { "type": "address", "name": "_spender" }], - "constant": true, + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], "payable": false, - "type": "function", - "gas": 1925 + "stateMutability": "view", + "type": "function" }, { - "name": "name", - "outputs": [{ "type": "bytes32", "name": "out" }], + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + } + ], + "name": "burn", + "outputs": [ + { + "internalType": "uint256", + "name": "amount0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount1", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, "inputs": [], - "constant": true, - "payable": false, - "type": "function", - "gas": 1623 - }, - { - "name": "symbol", - "outputs": [{ "type": "bytes32", "name": "out" }], - "inputs": [], - "constant": true, - "payable": false, - "type": "function", - "gas": 1653 - }, - { "name": "decimals", - "outputs": [{ "type": "uint256", "name": "out" }], - "inputs": [], - "constant": true, + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], "payable": false, - "type": "function", - "gas": 1683 + "stateMutability": "view", + "type": "function" }, { - "name": "totalSupply", - "outputs": [{ "type": "uint256", "name": "out" }], - "inputs": [], "constant": true, + "inputs": [], + "name": "factory", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], "payable": false, - "type": "function", - "gas": 1713 + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "getReserves", + "outputs": [ + { + "internalType": "uint112", + "name": "_reserve0", + "type": "uint112" + }, + { + "internalType": "uint112", + "name": "_reserve1", + "type": "uint112" + }, + { + "internalType": "uint32", + "name": "_blockTimestampLast", + "type": "uint32" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "_token0", + "type": "address" + }, + { + "internalType": "address", + "name": "_token1", + "type": "address" + } + ], + "name": "initialize", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "kLast", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + } + ], + "name": "mint", + "outputs": [ + { + "internalType": "uint256", + "name": "liquidity", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "nonces", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "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": "permit", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "price0CumulativeLast", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "price1CumulativeLast", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + } + ], + "name": "skim", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "uint256", + "name": "amount0Out", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount1Out", + "type": "uint256" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "swap", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "sync", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "token0", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "token1", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" } ] diff --git a/src/constants/abis/router.json b/src/constants/abis/router.json new file mode 100644 index 0000000000..d854f92e2f --- /dev/null +++ b/src/constants/abis/router.json @@ -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" + } +] diff --git a/src/constants/index.js b/src/constants/index.ts similarity index 94% rename from src/constants/index.js rename to src/constants/index.ts index 7d997d95a2..4b6aaff44c 100644 --- a/src/constants/index.js +++ b/src/constants/index.ts @@ -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, diff --git a/src/contexts/Allowances.tsx b/src/contexts/Allowances.tsx index acfb0df850..d7d7693b86 100644 --- a/src/contexts/Allowances.tsx +++ b/src/contexts/Allowances.tsx @@ -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 diff --git a/src/contexts/Balances.tsx b/src/contexts/Balances.tsx index 11b6a782cf..fa67e26998 100644 --- a/src/contexts/Balances.tsx +++ b/src/contexts/Balances.tsx @@ -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 ( [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 { 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 [] } diff --git a/src/contexts/Exchanges.tsx b/src/contexts/Exchanges.tsx index 5ce8646f19..b86dfb4dcb 100644 --- a/src/contexts/Exchanges.tsx +++ b/src/contexts/Exchanges.tsx @@ -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]) } diff --git a/src/contexts/Tokens.tsx b/src/contexts/Tokens.tsx index c395b89a1f..90bf6ebd09 100644 --- a/src/contexts/Tokens.tsx +++ b/src/contexts/Tokens.tsx @@ -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 } diff --git a/src/pages/App.js b/src/pages/App.js index 40f766818d..f1176ffd88 100644 --- a/src/pages/App.js +++ b/src/pages/App.js @@ -122,6 +122,25 @@ export default function App() { } }} /> + { + 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 + } else { + return + } + }} + /> } /> diff --git a/src/pages/Supply/AddLiquidity.tsx b/src/pages/Supply/AddLiquidity.tsx index e14ef06d16..887a409a3a 100644 --- a/src/pages/Supply/AddLiquidity.tsx +++ b/src/pages/Supply/AddLiquidity.tsx @@ -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() - 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 ( <> { 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 : ''} /> + {noLiquidity && ( + + + + 🥇 + {' '} + You are the first to add liquidity. Make sure you're setting rates correctly. + + + )} { - 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 /> @@ -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 /> @@ -411,7 +535,7 @@ export default function AddLiquidity() { Minted pool tokens: -
{liquidityMinted ? liquidityMinted.toFixed(6) : '-'}
+
{liquidityMinted ? liquidityMinted.toExact() : '-'}
Minted pool share: @@ -420,7 +544,11 @@ export default function AddLiquidity() { Rate:
- 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}
diff --git a/src/pages/Supply/RemoveLiquidity.js b/src/pages/Supply/RemoveLiquidity.js deleted file mode 100644 index 56f9115d72..0000000000 --- a/src/pages/Supply/RemoveLiquidity.js +++ /dev/null @@ -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 => {text} - - function renderTransactionDetails() { - return ( -
-
- {t('youAreRemoving')} {b(`${amountFormatter(ethWithdrawn, 18, 4)} ETH`)} {t('and')}{' '} - {b(`${amountFormatter(tokenWithdrawn, decimals, Math.min(decimals, 4))} ${symbol}`)} {t('outPool')} -
- - {t('youWillRemove')} {b(amountFormatter(valueParsed, 18, 4))} {t('liquidityTokens')} - - - {t('totalSupplyIs')} {b(amountFormatter(totalPoolTokens, 18, 4))} - - - {t('tokenWorth')} {b(amountFormatter(ETHPer.div(totalPoolTokens), 18, 4))} ETH {t('and')}{' '} - {b(amountFormatter(tokenPer.div(totalPoolTokens), decimals, Math.min(4, decimals)))} {symbol} - -
- ) - } - - 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 ( - - ) - } - - 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 && ( - { - setShowCustomTokenWarning(false) - }} - urlAddedTokens={urlAddedTokens} - currency={outputCurrency} - /> - )} - { - 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} - /> - - - - - - - !!(ethWithdrawn && tokenWithdrawn) ? ( - - - {`${amountFormatter(ethWithdrawn, 18, 4, false)} ETH`} - - + - - {`${amountFormatter(tokenWithdrawn, decimals, Math.min(4, decimals))} ${symbol}`} - - - ) : ( - - ) - } - disableTokenSelect - disableUnlock - /> - - - - {t('exchangeRate')} - {marketRate ? {`1 ETH = ${amountFormatter(marketRate, 18, 4)} ${symbol}`} : ' - '} - - - {t('currentPoolSize')} - {exchangeETHBalance && exchangeTokenBalance && (decimals || decimals === 0) ? ( - {`${amountFormatter(exchangeETHBalance, 18, 4)} ETH + ${amountFormatter( - exchangeTokenBalance, - decimals, - Math.min(decimals, 4) - )} ${symbol}`} - ) : ( - ' - ' - )} - - - - {t('yourPoolShare')} ({ownershipPercentageFormatted && ownershipPercentageFormatted}%) - - {ETHOwnShare && TokenOwnShare ? ( - - {`${amountFormatter(ETHOwnShare, 18, 4)} ETH + ${amountFormatter( - TokenOwnShare, - decimals, - Math.min(decimals, 4) - )} ${symbol}`} - - ) : ( - ' - ' - )} - - - - {renderSummary()} - - - - - ) -} diff --git a/src/pages/Supply/RemoveLiquidity.tsx b/src/pages/Supply/RemoveLiquidity.tsx new file mode 100644 index 0000000000..7077cfa279 --- /dev/null +++ b/src/pages/Supply/RemoveLiquidity.tsx @@ -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() + + // 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 ( + <> + { + 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={''} + /> + { + toggleSearch(false) + }} + /> + + { + toggleSearch(true) + }} + > + + + + {exchange?.token0?.symbol && exchange?.token1?.symbol + ? exchange?.token0?.symbol + ' / ' + exchange?.token1?.symbol + ' Pool' + : ''} + + + + + { + maxAmountPoolToken && onMaxPool(maxAmountPoolToken.toExact()) + }} + atMax={atMaxAmountPoolToken} + onTokenSelection={onTokenSelection} + title={'Deposit'} + error={poolTokenError} + disableTokenSelect + token={exchange?.liquidityToken} + isExchange={true} + exchange={exchange} + /> + + + + { + maxAmountInput && onMaxInput(maxAmountInput.toExact()) + }} + atMax={atMaxAmountInput} + token={tokens[Field.INPUT]} + onTokenSelection={onTokenSelection} + title={'Deposit'} + error={inputError} + disableTokenSelect + customBalance={TokensDeposited[Field.INPUT]} + /> + + + + { + maxAmountOutput && onMaxOutput(maxAmountOutput.toExact()) + }} + atMax={atMaxAmountOutput} + token={tokens[Field.OUTPUT]} + onTokenSelection={onTokenSelection} + title={'Deposit'} + error={outputError} + disableTokenSelect + customBalance={TokensDeposited[Field.OUTPUT]} + /> + + + + + + + Pool Tokens Burned: +
{formattedAmounts[Field.POOL] ? formattedAmounts[Field.POOL] : '-'}
+
+ + {exchange?.token0.symbol} Removed: +
{formattedAmounts[Field.INPUT] ? formattedAmounts[Field.INPUT] : '-'}
+
+ + {exchange?.token1.symbol} Removed: +
{formattedAmounts[Field.OUTPUT] ? formattedAmounts[Field.OUTPUT] : '-'}
+
+ + Rate: +
+ 1 {exchange?.token0.symbol} ={' '} + {independentField === Field.INPUT || independentField === Field.POOL + ? route?.midPrice.toSignificant(6) + : route?.midPrice.invert().toSignificant(6)}{' '} + {exchange?.token1.symbol} +
+
+
+
+ + + {errorText && errorText} + + + + { + // toggleConfirm(true) + onSign() + }} + width="48%" + disabled={!isValid} + > + + Sign + + + { + // toggleConfirm(true) + onRemove() + }} + width="48%" + disabled={!isValid} + > + + Remove + + + +
+ + ) +} diff --git a/src/pages/Supply/index.js b/src/pages/Supply/index.js index 223e3896d2..9cc725307d 100644 --- a/src/pages/Supply/index.js +++ b/src/pages/Supply/index.js @@ -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 }) - + {token0?.symbol}:{token1?.symbol} @@ -126,7 +126,7 @@ function ExchangeCard({ exchangeAddress, token0, token1, history, allBalances }) { - 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 ( - - ) - } + 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 ( + + ) + } else { return '' - }) + } }) return ( diff --git a/src/utils/index.js b/src/utils/index.js index f0408d12f0..c8be921ed4 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -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] + ) + ) + ] + ) + ) } diff --git a/yarn.lock b/yarn.lock index 657a5ced32..c170135414 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"