From 6211dff044bb580739ef8fd0f0aeddb2b162b315 Mon Sep 17 00:00:00 2001 From: ianlapham Date: Fri, 20 Mar 2020 16:03:23 -0400 Subject: [PATCH] typed pages --- package.json | 2 +- src/components/AddressInputPanel/index.js | 25 +- src/components/AdvancedSettings/index.js | 150 ++++ src/components/Button/index.js | 34 +- src/components/Card/index.js | 20 + src/components/ContextualInfo/index.js | 127 --- src/components/ContextualInfoNew/index.js | 133 --- src/components/CurrencyInputPanel/index.js | 72 +- src/components/ExchangePage/index.tsx | 523 +++++------- src/components/Header/index.js | 26 +- .../PoolFinder/{index.js => index.tsx} | 42 +- src/components/SearchModal/index.js | 113 ++- src/components/Slider/index.js | 34 +- src/components/TransactionDetails/index.js | 765 ------------------ src/components/Web3ReactManager/index.js | 1 - src/constants/abis/router.json | 162 +--- src/constants/index.ts | 18 +- src/contexts/Application.js | 1 + src/contexts/Balances.tsx | 8 +- src/contexts/Exchanges.tsx | 2 +- src/contexts/Tokens.tsx | 7 +- src/hooks/index.js | 9 +- src/pages/App.js | 4 +- src/pages/Send/index.tsx | 248 +----- src/pages/Supply/AddLiquidity.tsx | 129 ++- src/pages/Supply/CreateExchange.js | 157 ---- src/pages/Supply/RemoveLiquidity.tsx | 57 +- src/theme/index.js | 19 +- src/utils/index.js | 6 +- yarn.lock | 32 +- 30 files changed, 730 insertions(+), 2196 deletions(-) create mode 100644 src/components/AdvancedSettings/index.js delete mode 100644 src/components/ContextualInfo/index.js delete mode 100644 src/components/ContextualInfoNew/index.js rename src/components/PoolFinder/{index.js => index.tsx} (86%) delete mode 100644 src/components/TransactionDetails/index.js delete mode 100644 src/pages/Supply/CreateExchange.js diff --git a/package.json b/package.json index 0c1873888f..c33a04c9e9 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "@types/node": "^13.7.4", "@types/react": "^16.9.21", "@types/react-dom": "^16.9.5", - "@uniswap/sdk": "@uniswap/sdk@2.0.0-beta.17", + "@uniswap/sdk": "@uniswap/sdk@2.0.0-beta.19", "@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/AddressInputPanel/index.js b/src/components/AddressInputPanel/index.js index 76cb6c5c57..bc97c24f5b 100644 --- a/src/components/AddressInputPanel/index.js +++ b/src/components/AddressInputPanel/index.js @@ -3,6 +3,8 @@ import styled from 'styled-components' import { useTranslation } from 'react-i18next' import { transparentize } from 'polished' +import QR from '../../assets/svg/QR.svg' + import { isAddress } from '../../utils' import { useWeb3React, useDebounce } from '../../hooks' @@ -13,6 +15,7 @@ const InputPanel = styled.div` border-radius: 1.25rem; background-color: ${({ theme }) => theme.inputBackground}; z-index: 1; + width: 100%; ` const ContainerRow = styled.div` @@ -49,7 +52,7 @@ const LabelContainer = styled.div` const InputRow = styled.div` ${({ theme }) => theme.flexRowNoWrap} align-items: center; - padding: 0.25rem 0.85rem 0.75rem; + padding: 0.75rem; ` const Input = styled.input` @@ -69,7 +72,17 @@ const Input = styled.input` } ` -export default function AddressInputPanel({ title, initialInput = '', onChange = () => {}, onError = () => {} }) { +const QRWrapper = styled.div` + display: flex; + align-items: center; + justify-content: center; + border: 1px solid ${({ theme }) => theme.outlineGrey}; + background: #fbfbfb; + padding: 4px; + border-radius: 8px; +` + +export default function AddressInputPanel({ title, initialInput = '', onChange, onError}) { const { t } = useTranslation() const { library } = useWeb3React() @@ -166,11 +179,6 @@ export default function AddressInputPanel({ title, initialInput = '', onChange = - - - {title || t('recipientAddress')} - - + + + diff --git a/src/components/AdvancedSettings/index.js b/src/components/AdvancedSettings/index.js new file mode 100644 index 0000000000..73bf1568cc --- /dev/null +++ b/src/components/AdvancedSettings/index.js @@ -0,0 +1,150 @@ +import React, { useState } from 'react' +import styled from 'styled-components' + +import QuestionHelper from '../Question' +import NumericalInput from '../NumericalInput' +import { Link } from '../../theme/components' +import { TYPE } from '../../theme' +import { AutoColumn } from '../../components/Column' +import Row, { RowBetween, RowFixed } from '../../components/Row' +import { ButtonRadio } from '../Button' + +const InputWrapper = styled(RowBetween)` + width: 200px; + background-color: ${({ theme }) => theme.inputBackground}; + border-radius: 8px; + padding: 4px 8px; + border: 1px solid transparent; + border: ${({ active, error, theme }) => + error ? '1px solid ' + theme.salmonRed : active ? '1px solid ' + theme.royalBlue : ''}; +` + +const SLIPPAGE_INDEX = { + 1: 1, + 2: 2, + 3: 3, + 4: 4 +} + +export default function AdvancedSettings({ setIsOpen, setDeadline, setAllowedSlippage }) { + const [deadlineInput, setDeadlineInput] = useState(15) + const [slippageInput, setSlippageInput] = useState() + const [activeIndex, setActiveIndex] = useState(SLIPPAGE_INDEX[3]) + + const [slippageInputError, setSlippageInputError] = useState(null) // error + + function parseCustomInput(val) { + const acceptableValues = [/^$/, /^\d{1,2}$/, /^\d{0,2}\.\d{0,2}$/] + if (val > 5) { + setSlippageInputError('Your transaction may be front-run.') + } else { + setSlippageInputError(null) + } + if (acceptableValues.some(a => a.test(val))) { + setSlippageInput(val) + setAllowedSlippage(val * 100) + } + } + + function parseCustomDeadline(val) { + const acceptableValues = [/^$/, /^\d+$/] + if (acceptableValues.some(re => re.test(val))) { + setDeadlineInput(val) + setDeadline(val * 60) + } + } + + return ( + + { + setIsOpen(false) + }} + > + back + + + Limit additional price impact + + + + { + setActiveIndex(SLIPPAGE_INDEX[1]) + setAllowedSlippage(10) + }} + > + 0.1% + + { + setActiveIndex(SLIPPAGE_INDEX[2]) + setAllowedSlippage(100) + }} + > + 1% + + { + setActiveIndex(SLIPPAGE_INDEX[3]) + setAllowedSlippage(200) + }} + > + 2% (suggested) + + + + + { + parseCustomInput(val) + setActiveIndex(SLIPPAGE_INDEX[4]) + }} + placeHolder="Custom" + onClick={() => { + setActiveIndex(SLIPPAGE_INDEX[4]) + if (slippageInput) { + parseCustomInput(slippageInput) + } + }} + /> + % + + {slippageInputError && ( + + Your transaction may be front-run + + )} + + + Adjust deadline (minutes from now) + + + + { + parseCustomDeadline(val) + }} + /> + + + + ) +} diff --git a/src/components/Button/index.js b/src/components/Button/index.js index 6537a0996a..c2839f3fb0 100644 --- a/src/components/Button/index.js +++ b/src/components/Button/index.js @@ -1,15 +1,14 @@ import React from 'react' -import { Button as RebassButton } from 'rebass/styled-components' import styled from 'styled-components' import { darken, lighten } from 'polished' import { RowBetween } from '../Row' import { ChevronDown } from 'react-feather' +import { Button as RebassButton } from 'rebass/styled-components' const Base = styled(RebassButton)` padding: ${({ padding }) => (padding ? padding : '18px')}; width: ${({ width }) => (width ? width : '100%')}; - font-size: 1rem; font-weight: 500; text-align: center; border-radius: 20px; @@ -39,7 +38,7 @@ export const ButtonPrimary = styled(Base)` } &:disabled { background-color: ${({ theme }) => theme.outlineGrey}; - color: ${({ theme }) => theme.darkGrey} + color: ${({ theme }) => theme.darkGray} cursor: auto; box-shadow: none; } @@ -48,6 +47,7 @@ export const ButtonPrimary = styled(Base)` export const ButtonSecondary = styled(Base)` background-color: #ebf4ff; color: #2172e5; + font-size: 16px; border-radius: 8px; padding: 10px; @@ -69,6 +69,30 @@ export const ButtonSecondary = styled(Base)` } ` +export const ButtonPink = styled(Base)` + background-color: ${({ theme }) => theme.darkPink}; + color: white; + + padding: 10px; + + &:focus { + box-shadow: 0 0 0 1pt ${({ theme }) => darken(0.05, theme.darkPink)}; + background-color: ${({ theme }) => darken(0.05, theme.darkPink)}; + } + &:hover { + background-color: ${({ theme }) => darken(0.05, theme.darkPink)}; + } + &:active { + box-shadow: 0 0 0 1pt ${({ theme }) => darken(0.1, theme.darkPink)}; + background-color: ${({ theme }) => darken(0.1, theme.darkPink)}; + } + &:disabled { + background-color: ${({ theme }) => theme.darkPink}; + opacity: 50%; + cursor: auto; + } +` + export const ButtonEmpty = styled(Base)` border: 1px solid #edeef2; background-color: transparent; @@ -157,7 +181,7 @@ export function ButtonError({ children, error, ...rest }) { } } -export function ButtonDropwdown({ disabled, children, ...rest }) { +export function ButtonDropwdown({ disabled = false, children, ...rest }) { return ( @@ -168,7 +192,7 @@ export function ButtonDropwdown({ disabled, children, ...rest }) { ) } -export function ButtonDropwdownLight({ disabled, children, ...rest }) { +export function ButtonDropwdownLight({ disabled = false, children, ...rest }) { return ( diff --git a/src/components/Card/index.js b/src/components/Card/index.js index 60225d7eac..142d2eeaa2 100644 --- a/src/components/Card/index.js +++ b/src/components/Card/index.js @@ -1,4 +1,6 @@ +import React from 'react' import styled from 'styled-components' +import { Text } from 'rebass' import { Box } from 'rebass/styled-components' const Card = styled(Box)` @@ -18,3 +20,21 @@ export const LightCard = styled(Card)` export const GreyCard = styled(Card)` background-color: rgba(255, 255, 255, 0.9); ` + +const BlueCardStyled = styled(Card)` + background-color: #ebf4ff; + color: #2172e5; + border-radius: 12px; + padding: 8px; + width: fit-content; +` + +export const BlueCard = ({ children }) => { + return ( + + + {children} + + + ) +} diff --git a/src/components/ContextualInfo/index.js b/src/components/ContextualInfo/index.js deleted file mode 100644 index e4c63c07b0..0000000000 --- a/src/components/ContextualInfo/index.js +++ /dev/null @@ -1,127 +0,0 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' -import styled from 'styled-components' - -import ReactGA from 'react-ga' -import { ReactComponent as Dropup } from '../../assets/images/dropup-blue.svg' -import { ReactComponent as Dropdown } from '../../assets/images/dropdown-blue.svg' - -const SummaryWrapper = styled.div` - color: ${({ error, theme }) => (error ? theme.salmonRed : theme.doveGray)}; - font-size: 0.75rem; - text-align: center; - margin-top: 1rem; - padding-top: 1rem; -` - -const Details = styled.div` - background-color: ${({ theme }) => theme.concreteGray}; - padding: 1.5rem; - border-radius: 1rem; - font-size: 0.75rem; - margin-top: 1rem; -` - -const SummaryWrapperContainer = styled.div` - ${({ theme }) => theme.flexRowNoWrap}; - color: ${({ theme }) => theme.royalBlue}; - text-align: center; - margin-top: 1rem; - padding-top: 1rem; - cursor: pointer; - align-items: center; - justify-content: center; - font-size: 0.75rem; - - span { - margin-right: 12px; - } - - img { - height: 0.75rem; - width: 0.75rem; - } -` - -const WrappedDropup = ({ isError, highSlippageWarning, ...rest }) => -const ColoredDropup = styled(WrappedDropup)` - path { - stroke: ${({ theme }) => theme.royalBlue}; - } -` - -const WrappedDropdown = ({ isError, highSlippageWarning, ...rest }) => -const ColoredDropdown = styled(WrappedDropdown)` - path { - stroke: ${({ theme }) => theme.royalBlue}; - } -` - -class ContextualInfo extends Component { - static propTypes = { - openDetailsText: PropTypes.string, - renderTransactionDetails: PropTypes.func, - contextualInfo: PropTypes.string, - isError: PropTypes.bool - } - - static defaultProps = { - openDetailsText: 'Advanced Details', - closeDetailsText: 'Hide Advanced', - renderTransactionDetails() {}, - contextualInfo: '', - isError: false - } - - state = { - showDetails: false - } - - renderDetails() { - if (!this.state.showDetails) { - return null - } - return
{this.props.renderTransactionDetails()}
- } - - render() { - const { openDetailsText, closeDetailsText, contextualInfo, isError } = this.props - - if (contextualInfo) { - return {contextualInfo} - } - - return ( - <> - { - !this.state.showDetails && - ReactGA.event({ - category: 'Advanced Interaction', - action: 'Open Advanced Details', - label: 'Pool Page Details' - }) - this.setState(prevState => { - return { showDetails: !prevState.showDetails } - }) - }} - > - {!this.state.showDetails ? ( - <> - {openDetailsText} - - - ) : ( - <> - {closeDetailsText} - - - )} - - {this.renderDetails()} - - ) - } -} - -export default ContextualInfo diff --git a/src/components/ContextualInfoNew/index.js b/src/components/ContextualInfoNew/index.js deleted file mode 100644 index a9aebdab87..0000000000 --- a/src/components/ContextualInfoNew/index.js +++ /dev/null @@ -1,133 +0,0 @@ -import React, { useState } from 'react' -import styled, { css } from 'styled-components' -import { transparentize } from 'polished' -import ReactGA from 'react-ga' -import { ReactComponent as Dropup } from '../../assets/images/dropup-blue.svg' -import { ReactComponent as Dropdown } from '../../assets/images/dropdown-blue.svg' - -const SummaryWrapper = styled.div` - color: ${({ error, brokenTokenWarning, theme }) => (error || brokenTokenWarning ? theme.salmonRed : theme.doveGray)}; - font-size: 0.75rem; - text-align: center; - margin-top: 1rem; - padding-top: 1rem; -` - -const SummaryWrapperContainer = styled.div` - ${({ theme }) => theme.flexRowNoWrap}; - color: ${({ theme }) => theme.royalBlue}; - text-align: center; - margin-top: 1rem; - padding-top: 1rem; - cursor: pointer; - align-items: center; - justify-content: center; - font-size: 0.75rem; - - img { - height: 0.75rem; - width: 0.75rem; - } -` - -const Details = styled.div` - background-color: ${({ theme }) => theme.concreteGray}; - /* padding: 1.25rem 1.25rem 1rem 1.25rem; */ - border-radius: 1rem; - font-size: 0.75rem; - margin: 1rem 0.5rem 0 0.5rem; -` - -const ErrorSpan = styled.span` - margin-right: 12px; - font-size: 0.75rem; - line-height: 0.75rem; - - color: ${({ isError, theme }) => isError && theme.salmonRed}; - ${({ slippageWarning, highSlippageWarning, theme }) => - highSlippageWarning - ? css` - color: ${theme.salmonRed}; - font-weight: 600; - ` - : slippageWarning && - css` - background-color: ${transparentize(0.6, theme.warningYellow)}; - font-weight: 600; - padding: 0.25rem; - `} -` - -const WrappedDropup = ({ isError, highSlippageWarning, ...rest }) => -const ColoredDropup = styled(WrappedDropup)` - path { - stroke: ${({ isError, theme }) => (isError ? theme.salmonRed : theme.royalBlue)}; - - ${({ highSlippageWarning, theme }) => - highSlippageWarning && - css` - stroke: ${theme.salmonRed}; - `} - } -` - -const WrappedDropdown = ({ isError, highSlippageWarning, ...rest }) => -const ColoredDropdown = styled(WrappedDropdown)` - path { - stroke: ${({ isError, theme }) => (isError ? theme.salmonRed : theme.royalBlue)}; - - ${({ highSlippageWarning, theme }) => - highSlippageWarning && - css` - stroke: ${theme.salmonRed}; - `} - } -` - -export default function ContextualInfo({ - openDetailsText = 'Advanced Details', - closeDetailsText = 'Hide Advanced', - contextualInfo = '', - allowExpand = false, - isError = false, - slippageWarning, - highSlippageWarning, - brokenTokenWarning, - dropDownContent -}) { - const [showDetails, setShowDetails] = useState(false) - return !allowExpand ? ( - {contextualInfo} - ) : ( - <> - { - !showDetails && - ReactGA.event({ - category: 'Advanced Interaction', - action: 'Open Advanced Details', - label: 'Swap/Send Page Details' - }) - setShowDetails(s => !s) - }} - > - <> - - {(slippageWarning || highSlippageWarning) && ( - - ⚠️ - - )} - {contextualInfo ? contextualInfo : showDetails ? closeDetailsText : openDetailsText} - - {showDetails ? ( - - ) : ( - - )} - - - {showDetails &&
{dropDownContent()}
} - - ) -} diff --git a/src/components/CurrencyInputPanel/index.js b/src/components/CurrencyInputPanel/index.js index 280b51e299..7d44407a32 100644 --- a/src/components/CurrencyInputPanel/index.js +++ b/src/components/CurrencyInputPanel/index.js @@ -8,6 +8,7 @@ import { WETH } from '@uniswap/sdk' import TokenLogo from '../TokenLogo' import DoubleLogo from '../DoubleLogo' import SearchModal from '../SearchModal' +import { TYPE } from '../../theme' import { Text } from 'rebass' import { RowBetween } from '../Row' import { ReactComponent as DropDown } from '../../assets/images/dropdown.svg' @@ -20,6 +21,8 @@ import { calculateGasMargin } from '../../utils' import { useAddressBalance } from '../../contexts/Balances' import { useTransactionAdder, usePendingApproval } from '../../contexts/Transactions' +import { ROUTER_ADDRESSES } from '../../constants' + const GAS_MARGIN = ethers.utils.bigNumberify(1000) const SubCurrencySelect = styled.button` @@ -51,26 +54,18 @@ const CurrencySelect = styled.button` font-size: 20px; background-color: ${({ selected, theme }) => (selected ? theme.buttonBackgroundPlain : theme.royalBlue)}; color: ${({ selected, theme }) => (selected ? theme.textColor : theme.white)}; - border: 1px solid - ${({ selected, theme, disableTokenSelect }) => - disableTokenSelect ? theme.buttonBackgroundPlain : selected ? theme.buttonOutlinePlain : theme.royalBlue}; border-radius: 8px; outline: none; cursor: pointer; user-select: none; + border: 1px solid + ${({ selected, theme }) => (selected ? darken(0.1, theme.outlineGrey) : darken(0.1, theme.royalBlue))}; + + :focus, :hover { border: 1px solid - ${({ selected, theme }) => (selected ? darken(0.1, theme.outlineGrey) : darken(0.1, theme.royalBlue))}; - } - - :focus { - border: 1px solid ${({ theme }) => darken(0.1, theme.royalBlue)}; - } - - :active { - background-color: ${({ selected, theme }) => - selected ? darken(0.1, theme.zumthorBlue) : darken(0.1, theme.royalBlue)}; + ${({ selected, theme }) => (selected ? darken(0.2, theme.outlineGrey) : darken(0.2, theme.royalBlue))}; } ` @@ -99,12 +94,8 @@ const InputPanel = styled.div` const Container = styled.div` border-radius: ${({ hideInput }) => (hideInput ? '8px' : '20px')}; - border: 1px solid ${({ error, theme }) => (error ? theme.salmonRed : theme.mercuryGray)}; - + border: 1px solid ${({ error, theme }) => (error ? theme.salmonRed : theme.backgroundColor)}; background-color: ${({ theme }) => theme.inputBackground}; - :focus-within { - border: 1px solid ${({ error, theme }) => (error ? theme.salmonRed : theme.malibuBlue)}; - } ` const LabelRow = styled.div` @@ -113,7 +104,7 @@ const LabelRow = styled.div` color: ${({ theme }) => theme.doveGray}; font-size: 0.75rem; line-height: 1rem; - padding: 0.75rem 1rem 0; + padding: 0.5rem 1rem 1rem 1rem; span:hover { cursor: pointer; color: ${({ theme }) => darken(0.2, theme.doveGray)}; @@ -161,12 +152,12 @@ export default function CurrencyInputPanel({ value, field, onUserInput, - onTokenSelection = null, title, onMax, atMax, error, urlAddedTokens = [], // used + onTokenSelection = null, token = null, showUnlock = false, // used to show unlock if approval needed disableUnlock = false, @@ -176,23 +167,19 @@ export default function CurrencyInputPanel({ exchange = null, // used for double token logo customBalance = null, // used for LP balances instead of token balance hideInput = false, - showSendWithSwap = false, - onTokenSelectSendWithSwap = null + showSendWithSwap = false }) { - const { account, chainId } = useWeb3React() const { t } = useTranslation() - const addTransaction = useTransactionAdder() + const { account, chainId } = useWeb3React() + const routerAddress = ROUTER_ADDRESSES[chainId] + const addTransaction = useTransactionAdder() const [modalOpen, setModalOpen] = 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 || @@ -240,19 +227,6 @@ export default function CurrencyInputPanel({ return ( - {!hideBalance && ( - - - {title} - {}}> - - - Balance: {customBalance ? customBalance?.toSignificant(4) : userTokenBalance?.toSignificant(4)} - - - - - )} {!hideInput && ( <> @@ -267,7 +241,7 @@ export default function CurrencyInputPanel({ )} { if (!disableTokenSelect) { setModalOpen(true) @@ -292,6 +266,19 @@ export default function CurrencyInputPanel({ + {!hideBalance && !!token && ( + + + {'-'} + {}}> + + + Balance: {customBalance ? customBalance?.toSignificant(4) : userTokenBalance?.toSignificant(4)} + + + + + )} {!disableTokenSelect && ( )} diff --git a/src/components/ExchangePage/index.tsx b/src/components/ExchangePage/index.tsx index 0a93a9872f..25a4b582fd 100644 --- a/src/components/ExchangePage/index.tsx +++ b/src/components/ExchangePage/index.tsx @@ -2,33 +2,34 @@ import React, { useState, useReducer, useCallback, useEffect } from 'react' import styled from 'styled-components' import { ethers } from 'ethers' import { parseUnits, parseEther } from '@ethersproject/units' -import { WETH, TradeType, Route, Trade, TokenAmount, JSBI } from '@uniswap/sdk' +import { WETH, TradeType, Route, Exchange, Trade, TokenAmount, JSBI, Percent } from '@uniswap/sdk' -import QR from '../../assets/svg/QR.svg' import TokenLogo from '../TokenLogo' +import AddressInputPanel from '../AddressInputPanel' import QuestionHelper from '../Question' import NumericalInput from '../NumericalInput' +import AdvancedSettings from '../AdvancedSettings' import ConfirmationModal from '../ConfirmationModal' import CurrencyInputPanel from '../CurrencyInputPanel' import { Link } from '../../theme/components' import { Text } from 'rebass' import { TYPE } from '../../theme' -import { GreyCard, LightCard } from '../../components/Card' import { ArrowDown, ArrowUp } from 'react-feather' -import { ButtonPrimary, ButtonError, ButtonRadio } from '../Button' +import { GreyCard, BlueCard } from '../../components/Card' import { AutoColumn, ColumnCenter } from '../../components/Column' -import Row, { RowBetween, RowFixed } from '../../components/Row' +import { ButtonPrimary, ButtonError } from '../Button' +import { RowBetween, RowFixed, AutoRow } from '../../components/Row' -import { usePopups } from '../../contexts/Application' import { useToken } from '../../contexts/Tokens' +import { usePopups } from '../../contexts/Application' import { useExchange } from '../../contexts/Exchanges' -import { useWeb3React, useTokenContract } from '../../hooks' -import { useAddressBalance } from '../../contexts/Balances' import { useTransactionAdder } from '../../contexts/Transactions' import { useAddressAllowance } from '../../contexts/Allowances' +import { useWeb3React, useTokenContract } from '../../hooks' +import { useAddressBalance, useAllBalances } from '../../contexts/Balances' import { ROUTER_ADDRESSES } from '../../constants' -import { getRouterContract, calculateGasMargin, isAddress, getProviderOrSigner } from '../../utils' +import { getRouterContract, calculateGasMargin, getProviderOrSigner } from '../../utils' const Wrapper = styled.div` position: relative; @@ -59,42 +60,11 @@ const ErrorText = styled(Text)` warningHigh ? theme.salmonRed : warningMedium ? theme.warningYellow : theme.textColor}; ` -const InputWrapper = styled(RowBetween)` - width: 200px; - background-color: ${({ theme }) => theme.inputBackground}; - border-radius: 8px; - padding: 4px 8px; - border: 1px solid transparent; - border: ${({ active, error, theme }) => - error ? '1px solid ' + theme.salmonRed : active ? '1px solid ' + theme.royalBlue : ''}; -` - const InputGroup = styled(AutoColumn)` position: relative; padding: 40px 0; ` -const QRWrapper = styled.div` - display: flex; - align-items: center; - justify-content: center; - border: 1px solid ${({ theme }) => theme.outlineGrey}; - background: #fbfbfb; - padding: 4px; - border-radius: 8px; -` - -const StyledInput = styled.input` - width: ${({ width }) => width}; - border: none; - outline: none; - font-size: 20px; - - ::placeholder { - color: #edeef2; - } -` - const StyledNumerical = styled(NumericalInput)` text-align: center; font-size: 48px; @@ -227,13 +197,6 @@ function hex(value: JSBI) { return ethers.utils.bigNumberify(value.toString()) } -const SLIPPAGE_INDEX = { - 1: 1, - 2: 2, - 3: 3, - 4: 4 -} - const SWAP_TYPE = { EXACT_TOKENS_FOR_TOKENS: 'EXACT_TOKENS_FOR_TOKENS', EXACT_TOKENS_FOR_ETH: 'EXACT_TOKENS_FOR_ETH', @@ -257,52 +220,50 @@ const ALLOWED_SLIPPAGE_HIGH = 500 export default function ExchangePage({ sendingInput = false }) { const { chainId, account, library } = useWeb3React() - const routerAddress = ROUTER_ADDRESSES[chainId] + const routerAddress: string = ROUTER_ADDRESSES[chainId] // adding notifications on txns const [, addPopup] = usePopups() const addTransaction = useTransactionAdder() // sending state - const [sending, setSending] = useState(sendingInput) - const [sendingWithSwap, setSendingWithSwap] = useState(false) - const [recipient, setRecipient] = useState('') + const [sending] = useState(sendingInput) + const [sendingWithSwap, setSendingWithSwap] = useState(false) + const [recipient, setRecipient] = useState('') - // input details + // trade details const [state, dispatch] = useReducer(reducer, WETH[chainId].address, initializeSwapState) const { independentField, typedValue, ...fieldData } = state - const dependentField = independentField === Field.INPUT ? Field.OUTPUT : Field.INPUT - const tradeType = independentField === Field.INPUT ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT - const [tradeError, setTradeError] = useState('') // error for things like reserve size or route + const dependentField: Field = independentField === Field.INPUT ? Field.OUTPUT : Field.INPUT + const tradeType: TradeType = independentField === Field.INPUT ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT + const [tradeError, setTradeError] = useState('') // error for things like reserve size or route 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 + const exchange: Exchange = useExchange(tokens[Field.INPUT], tokens[Field.OUTPUT]) + const route: Route = !!exchange ? new Route([exchange], tokens[Field.INPUT]) : undefined + const emptyReserves: boolean = exchange && JSBI.equal(JSBI.BigInt(0), exchange.reserve0.raw) // modal and loading - const [showConfirm, setShowConfirm] = useState(false) - const [pendingConfirmation, setPendingConfirmation] = useState(true) // waiting for user confirmation - const [attemptingTxn, setAttemptingTxn] = useState(false) // clicked confirmed - - // advanced settings - const [showAdvanced, setShowAdvanced] = useState(false) - const [activeIndex, setActiveIndex] = useState(SLIPPAGE_INDEX[3]) - const [customSlippage, setCustomSlippage] = useState() - const [customDeadline, setCustomDeadline] = useState(DEFAULT_DEADLINE_FROM_NOW / 60) - const [slippageInputError, setSlippageInputError] = useState(null) + const [showConfirm, setShowConfirm] = useState(false) + const [showAdvanced, setShowAdvanced] = useState(false) + const [attemptingTxn, setAttemptingTxn] = useState(false) // clicked confirmed + const [pendingConfirmation, setPendingConfirmation] = useState(true) // waiting for user confirmation // txn values - const [txHash, setTxHash] = useState() - const [deadline, setDeadline] = useState(DEFAULT_DEADLINE_FROM_NOW) - const [allowedSlippage, setAllowedSlippage] = useState(INITIAL_ALLOWED_SLIPPAGE) + const [txHash, setTxHash] = useState('') + const [deadline, setDeadline] = useState(DEFAULT_DEADLINE_FROM_NOW) + const [allowedSlippage, setAllowedSlippage] = useState(INITIAL_ALLOWED_SLIPPAGE) // approvals - const inputApproval = useAddressAllowance(account, tokens[Field.INPUT], routerAddress) - const outputApproval = useAddressAllowance(account, tokens[Field.OUTPUT], routerAddress) + const inputApproval: TokenAmount = useAddressAllowance(account, tokens[Field.INPUT], routerAddress) + const outputApproval: TokenAmount = useAddressAllowance(account, tokens[Field.OUTPUT], routerAddress) + + // all balances for detecting a swap with send + const allBalances: TokenAmount[] = useAllBalances() // get user- and token-specific lookup data const userBalances = { @@ -311,18 +272,13 @@ export default function ExchangePage({ sendingInput = false }) { } 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?) - /** - * @todo reserve limit error here - */ - console.error('found error here ') + console.error(error) } } @@ -335,12 +291,11 @@ export default function ExchangePage({ sendingInput = false }) { : undefined } catch (error) {} - const slippageFromTrade = trade && trade.slippage + const slippageFromTrade: Percent = trade && trade.slippage 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) : '' @@ -384,8 +339,8 @@ export default function ExchangePage({ sendingInput = false }) { }) }, []) - const MIN_ETHER = chainId && new TokenAmount(WETH[chainId], JSBI.BigInt(parseEther('.01'))) - const maxAmountInput = + const MIN_ETHER: TokenAmount = chainId && new TokenAmount(WETH[chainId], JSBI.BigInt(parseEther('.01'))) + const maxAmountInput: TokenAmount = !!userBalances[Field.INPUT] && JSBI.greaterThan( userBalances[Field.INPUT].raw, @@ -395,22 +350,22 @@ export default function ExchangePage({ sendingInput = false }) { ? userBalances[Field.INPUT].subtract(MIN_ETHER) : userBalances[Field.INPUT] : undefined - const atMaxAmountInput = + const atMaxAmountInput: boolean = !!maxAmountInput && !!parsedAmounts[Field.INPUT] ? JSBI.equal(maxAmountInput.raw, parsedAmounts[Field.INPUT].raw) : undefined - const maxAmountOutput = + const maxAmountOutput: TokenAmount = !!userBalances[Field.OUTPUT] && JSBI.greaterThan(userBalances[Field.OUTPUT].raw, JSBI.BigInt(0)) ? userBalances[Field.OUTPUT] : undefined - const atMaxAmountOutput = + const atMaxAmountOutput: boolean = !!maxAmountOutput && !!parsedAmounts[Field.OUTPUT] ? JSBI.equal(maxAmountOutput.raw, parsedAmounts[Field.OUTPUT].raw) : undefined - function getSwapType() { + function getSwapType(): string { if (tradeType === TradeType.EXACT_INPUT) { if (tokens[Field.INPUT] === WETH[chainId]) { return SWAP_TYPE.EXACT_ETH_FOR_TOKENS @@ -438,7 +393,7 @@ export default function ExchangePage({ sendingInput = false }) { return null } - const slippageAdjustedAmounts = { + const slippageAdjustedAmounts: { [field: number]: TokenAmount } = { [Field.INPUT]: Field.INPUT === independentField ? parsedAmounts[Field.INPUT] @@ -451,37 +406,15 @@ export default function ExchangePage({ sendingInput = false }) { new TokenAmount(tokens[Field.INPUT], calculateSlippageAmount(parsedAmounts[Field.OUTPUT])?.[0]) } - const showInputUnlock = + const showInputUnlock: boolean = parsedAmounts[Field.INPUT] && inputApproval && JSBI.greaterThan(parsedAmounts[Field.INPUT].raw, inputApproval.raw) - const showOutputUnlock = + const showOutputUnlock: boolean = parsedAmounts[Field.OUTPUT] && outputApproval && JSBI.greaterThan(parsedAmounts[Field.OUTPUT].raw, outputApproval.raw) - // parse the input for custom slippage - function parseCustomInput(val) { - const acceptableValues = [/^$/, /^\d{1,2}$/, /^\d{0,2}\.\d{0,2}$/] - if (val > 5) { - setSlippageInputError('Your transaction may be front-run.') - } else { - setSlippageInputError(null) - } - if (acceptableValues.some(a => a.test(val))) { - setCustomSlippage(val) - setAllowedSlippage(val * 100) - } - } - - function parseCustomDeadline(val) { - const acceptableValues = [/^$/, /^\d+$/] - if (acceptableValues.some(re => re.test(val))) { - setCustomDeadline(val) - setDeadline(val * 60) - } - } - - const tokenContract = useTokenContract(tokens[Field.INPUT]?.address) + const tokenContract: ethers.Contract = useTokenContract(tokens[Field.INPUT]?.address) // function for a pure send async function onSend() { @@ -494,7 +427,6 @@ export default function ExchangePage({ sendingInput = false }) { signer .sendTransaction({ to: recipient.toString(), value: hex(parsedAmounts[Field.INPUT].raw) }) .then(response => { - console.log(response) setTxHash(response.hash) addTransaction(response) setPendingConfirmation(false) @@ -537,17 +469,17 @@ export default function ExchangePage({ sendingInput = false }) { } } + // covers swap or swap with send async function onSwap() { - const routerContract = getRouterContract(chainId, library, account) - setAttemptingTxn(true) + const routerContract: ethers.Contract = getRouterContract(chainId, library, account) + + setAttemptingTxn(true) // mark that user is attempting transaction const path = Object.keys(route.path).map(key => { return route.path[key].address }) - - let estimate: Function, method: Function, args, value - - const deadlineFromNow = Math.ceil(Date.now() / 1000) + deadline + let estimate: Function, method: Function, args: any[], value: ethers.utils.BigNumber + const deadlineFromNow: number = Math.ceil(Date.now() / 1000) + deadline const swapType = getSwapType() switch (swapType) { @@ -558,7 +490,7 @@ export default function ExchangePage({ sendingInput = false }) { slippageAdjustedAmounts[Field.INPUT].raw.toString(), slippageAdjustedAmounts[Field.OUTPUT].raw.toString(), path, - account, + sending ? recipient : account, deadlineFromNow ] value = ethers.constants.Zero @@ -570,7 +502,7 @@ export default function ExchangePage({ sendingInput = false }) { slippageAdjustedAmounts[Field.OUTPUT].raw.toString(), slippageAdjustedAmounts[Field.INPUT].raw.toString(), path, - account, + sending ? recipient : account, deadlineFromNow ] value = ethers.constants.Zero @@ -578,7 +510,12 @@ export default function ExchangePage({ sendingInput = false }) { case SWAP_TYPE.EXACT_ETH_FOR_TOKENS: estimate = routerContract.estimate.swapExactETHForTokens method = routerContract.swapExactETHForTokens - args = [slippageAdjustedAmounts[Field.OUTPUT].raw.toString(), path, account, deadlineFromNow] + args = [ + slippageAdjustedAmounts[Field.OUTPUT].raw.toString(), + path, + sending ? recipient : account, + deadlineFromNow + ] value = hex(slippageAdjustedAmounts[Field.INPUT].raw) break case SWAP_TYPE.TOKENS_FOR_EXACT_ETH: @@ -588,7 +525,7 @@ export default function ExchangePage({ sendingInput = false }) { slippageAdjustedAmounts[Field.OUTPUT].raw.toString(), slippageAdjustedAmounts[Field.INPUT].raw.toString(), path, - account, + sending ? recipient : account, deadlineFromNow ] value = ethers.constants.Zero @@ -600,7 +537,7 @@ export default function ExchangePage({ sendingInput = false }) { slippageAdjustedAmounts[Field.INPUT].raw.toString(), slippageAdjustedAmounts[Field.OUTPUT].raw.toString(), path, - account, + sending ? recipient : account, deadlineFromNow ] value = ethers.constants.Zero @@ -608,13 +545,18 @@ export default function ExchangePage({ sendingInput = false }) { case SWAP_TYPE.ETH_FOR_EXACT_TOKENS: estimate = routerContract.estimate.swapETHForExactTokens method = routerContract.swapETHForExactTokens - args = [slippageAdjustedAmounts[Field.OUTPUT].raw.toString(), path, account, deadlineFromNow] + args = [ + slippageAdjustedAmounts[Field.OUTPUT].raw.toString(), + path, + sending ? recipient : account, + deadlineFromNow + ] value = hex(slippageAdjustedAmounts[Field.INPUT].raw) break } const estimatedGasLimit = await estimate(...args, { value }).catch(e => { - console.log('error getting gas limit') + console.log(e) }) method(...args, { @@ -626,7 +568,7 @@ export default function ExchangePage({ sendingInput = false }) { addTransaction(response) setPendingConfirmation(false) }) - .catch(e => { + .catch(() => { addPopup( Transaction Failed: try again. @@ -638,13 +580,13 @@ export default function ExchangePage({ sendingInput = false }) { } // errors - const [generalError, setGeneralError] = useState('') - const [inputError, setInputError] = useState('') - const [outputError, setOutputError] = useState('') - const [recipientError, setRecipientError] = useState('') - const [isValid, setIsValid] = useState(false) + const [generalError, setGeneralError] = useState('') + const [inputError, setInputError] = useState('') + const [outputError, setOutputError] = useState('') + const [recipientError, setRecipientError] = useState('') + const [isValid, setIsValid] = useState(false) - const ignoreOutput = sending ? !sendingWithSwap : false + const ignoreOutput: boolean = sending ? !sendingWithSwap : false useEffect(() => { // reset errors @@ -652,11 +594,9 @@ export default function ExchangePage({ sendingInput = false }) { setInputError(null) setOutputError(null) setTradeError(null) - setRecipientError(null) setIsValid(true) - if (!isAddress(recipient) && sending) { - setRecipientError('Invalid Recipient') + if (recipientError) { setIsValid(false) } @@ -675,7 +615,7 @@ export default function ExchangePage({ sendingInput = false }) { exchange && JSBI.greaterThan(parsedAmounts[Field.INPUT].raw, exchange.reserveOf(tokens[Field.INPUT]).raw) ) { - setTradeError('Low Liquidity Error') + setTradeError('Insufficient Liquidity') setIsValid(false) } @@ -685,7 +625,7 @@ export default function ExchangePage({ sendingInput = false }) { exchange && JSBI.greaterThan(parsedAmounts[Field.OUTPUT].raw, exchange.reserveOf(tokens[Field.OUTPUT]).raw) ) { - setTradeError('Low Liquidity Error') + setTradeError('Insufficient Liquidity') setIsValid(false) } @@ -712,6 +652,7 @@ export default function ExchangePage({ sendingInput = false }) { ignoreOutput, parsedAmounts, recipient, + recipientError, sending, sendingWithSwap, showInputUnlock, @@ -721,9 +662,12 @@ export default function ExchangePage({ sendingInput = false }) { ]) // warnings on slippage - const warningMedium = slippageFromTrade && parseFloat(slippageFromTrade.toFixed(4)) > ALLOWED_SLIPPAGE_MEDIUM / 100 - const warningHigh = slippageFromTrade && parseFloat(slippageFromTrade.toFixed(4)) > ALLOWED_SLIPPAGE_HIGH / 100 + const warningMedium: boolean = + slippageFromTrade && parseFloat(slippageFromTrade.toFixed(4)) > ALLOWED_SLIPPAGE_MEDIUM / 100 + const warningHigh: boolean = + slippageFromTrade && parseFloat(slippageFromTrade.toFixed(4)) > ALLOWED_SLIPPAGE_HIGH / 100 + // reset modal state when closed function resetModal() { setPendingConfirmation(true) setAttemptingTxn(false) @@ -736,11 +680,11 @@ export default function ExchangePage({ sendingInput = false }) { - {parsedAmounts[Field.INPUT]?.toFixed(8)} + {parsedAmounts[Field.INPUT]?.toFixed(8)} {tokens[Field.INPUT]?.symbol} - + To {recipient?.slice(0, 6)}...{recipient?.slice(36, 42)} @@ -749,6 +693,27 @@ export default function ExchangePage({ sendingInput = false }) { } if (sending && sendingWithSwap) { + return ( + + + + + + {slippageAdjustedAmounts[Field.OUTPUT]?.toSignificant(4)} {tokens[Field.OUTPUT]?.symbol} + + + + Via {parsedAmounts[Field.INPUT]?.toSignificant(4)} {tokens[Field.INPUT]?.symbol} swap + + + + To + + {recipient?.slice(0, 6)}...{recipient?.slice(36, 42)} + + + + ) } if (!sending) { @@ -796,119 +761,29 @@ export default function ExchangePage({ sendingInput = false }) { ) } - - if (sending && sendingWithSwap) { - } - if (showAdvanced) { return ( - - { - setShowAdvanced(false) - }} - > - back - - - Limit additional price impact - - - - { - setActiveIndex(SLIPPAGE_INDEX[1]) - setAllowedSlippage(10) - }} - > - 0.1% - - { - setActiveIndex(SLIPPAGE_INDEX[2]) - setAllowedSlippage(100) - }} - > - 1% - - { - setActiveIndex(SLIPPAGE_INDEX[3]) - setAllowedSlippage(200) - }} - > - 2% (suggested) - - - - - { - parseCustomInput(val) - setActiveIndex(SLIPPAGE_INDEX[4]) - }} - placeHolder="Custom" - onClick={() => { - setActiveIndex(SLIPPAGE_INDEX[4]) - if (customSlippage) { - parseCustomInput(customSlippage) - } - }} - /> - % - - {slippageInputError && ( - - Your transaction may be front-run - - )} - - - Adjust deadline (minutes from now) - - - - { - parseCustomDeadline(val) - }} - /> - - - + ) } - if (!sending) { + if (!sending || (sending && sendingWithSwap)) { return ( <> - - - Price - - - {`1 ${tokens[Field.INPUT]?.symbol} = ${route && route.midPrice && route.midPrice.adjusted.toFixed(8)} ${ - tokens[Field.OUTPUT]?.symbol - }`} - - + {route && route.midPrice && !emptyReserves && ( + + + Price + + + {`1 ${tokens[Field.INPUT]?.symbol} = ${route.midPrice.toFixed(6)} ${tokens[Field.OUTPUT]?.symbol}`} + + + )} Slippage setShowAdvanced(true)}>(edit limits) @@ -919,7 +794,7 @@ export default function ExchangePage({ sendingInput = false }) { - {warningHigh ? 'Swap Anyway' : 'Swap'} + {warningHigh ? (sending ? 'Send Anyway' : 'Swap Anyway') : sending ? 'Confirm Send' : 'Confirm Swap'} @@ -933,7 +808,7 @@ export default function ExchangePage({ sendingInput = false }) { setShowAdvanced(true) }} > - Advanced Options + Advanced @@ -941,27 +816,48 @@ export default function ExchangePage({ sendingInput = false }) { } } - const pendingText = sending - ? `Sending ${parsedAmounts[Field.INPUT]?.toSignificant(6)} ${tokens[Field.INPUT]?.symbol} to ${recipient}` + // text to show while loading + const pendingText: string = sending + ? sendingWithSwap + ? `Sending ${parsedAmounts[Field.OUTPUT]?.toSignificant(6)} ${tokens[Field.OUTPUT]?.symbol} to ${recipient}` + : `Sending ${parsedAmounts[Field.INPUT]?.toSignificant(6)} ${tokens[Field.INPUT]?.symbol} to ${recipient}` : ` Swapped ${parsedAmounts[Field.INPUT]?.toSignificant(6)} ${tokens[Field.INPUT]?.symbol} for ${parsedAmounts[ Field.OUTPUT ]?.toSignificant(6)} ${tokens[Field.OUTPUT]?.symbol}` + function _onTokenSelect(address: string) { + const balance = allBalances?.[account]?.[address] + // if no user balance - switch view to a send with swap + const hasBalance = balance && JSBI.greaterThan(JSBI.BigInt(0), balance.raw) + if (!hasBalance && sending) { + onTokenSelection(Field.OUTPUT, address) + setSendingWithSwap(true) + } else { + onTokenSelection(Field.INPUT, address) + } + } + + function _onRecipient(result) { + if (result.address) { + setRecipient(result.address) + } + } + return ( { resetModal() setShowConfirm(false) }} attemptingTxn={attemptingTxn} pendingConfirmation={pendingConfirmation} - hash={txHash ? txHash : ''} + hash={txHash} topContent={modalHeader} bottomContent={modalBottom} pendingText={pendingText} - title={sendingWithSwap ? 'Confirm swap and send' : sending ? 'Confirm Send' : 'Confirm Swap'} /> {sending && !sendingWithSwap && ( @@ -987,11 +883,7 @@ export default function ExchangePage({ sendingInput = false }) { }} atMax={atMaxAmountInput} token={tokens[Field.INPUT]} - onTokenSelection={address => onTokenSelection(Field.INPUT, address)} - onTokenSelectSendWithSwap={address => { - onTokenSelection(Field.OUTPUT, address) - setSendingWithSwap(true) - }} + onTokenSelection={address => _onTokenSelect(address)} title={'Input'} error={inputError} exchange={exchange} @@ -1010,17 +902,17 @@ export default function ExchangePage({ sendingInput = false }) { { - maxAmountInput && onMaxInput(maxAmountInput.toExact()) - }} atMax={atMaxAmountInput} token={tokens[Field.INPUT]} - onTokenSelection={address => onTokenSelection(Field.INPUT, address)} title={'Input'} error={inputError} exchange={exchange} showUnlock={showInputUnlock} + onUserInput={onUserInput} + onMax={() => { + maxAmountInput && onMaxInput(maxAmountInput.toExact()) + }} + onTokenSelection={address => onTokenSelection(Field.INPUT, address)} /> @@ -1043,18 +935,20 @@ export default function ExchangePage({ sendingInput = false }) { exchange={exchange} showUnlock={showOutputUnlock} /> - - - Price - - - {exchange - ? `1 ${tokens[Field.INPUT].symbol} = ${route?.midPrice.toSignificant(6)} ${ - tokens[Field.OUTPUT].symbol - }` - : '-'} - - + {!emptyReserves && ( // hide price if new exchange + + + Price + + + {exchange + ? `1 ${tokens[Field.INPUT].symbol} = ${route?.midPrice.toSignificant(6)} ${ + tokens[Field.OUTPUT].symbol + }` + : '-'} + + + )} {warningMedium && ( @@ -1070,39 +964,54 @@ export default function ExchangePage({ sendingInput = false }) { {sending && ( - - - setRecipient(e.target.value)} /> - - - - - + { + if (error) { + setRecipientError('Inavlid Recipient') + } else { + setRecipientError(null) + } + }} + /> )} - { - setShowConfirm(true) - }} - disabled={!isValid} - error={!!warningHigh} - > - - {generalError - ? generalError - : inputError - ? inputError - : outputError - ? outputError - : recipientError - ? recipientError - : tradeError - ? tradeError - : warningHigh - ? 'Swap Anyway' - : 'Swap'} - - + + {emptyReserves ? ( + + No exchange for this pair. + Create one now + + ) : ( + { + setShowConfirm(true) + }} + disabled={!isValid} + error={!!warningHigh} + > + + {generalError + ? generalError + : inputError + ? inputError + : outputError + ? outputError + : recipientError + ? recipientError + : tradeError + ? tradeError + : warningHigh + ? sendingWithSwap + ? 'Send Anyway' + : 'Swap Anyway' + : sending + ? 'Send' + : 'Swap'} + + + )} {warningHigh && ( diff --git a/src/components/Header/index.js b/src/components/Header/index.js index 1a227c0b05..664616717e 100644 --- a/src/components/Header/index.js +++ b/src/components/Header/index.js @@ -1,15 +1,14 @@ import React from 'react' import styled from 'styled-components' +import Row from '../Row' import Menu from '../Menu' import Logo from '../../assets/svg/logo.svg' -import Row from '../Row' +import Card from '../Card' import Web3Status from '../Web3Status' -import { CloseIcon } from '../../theme/components' +import { X } from 'react-feather' import { Link } from '../../theme' import { Text } from 'rebass' -import Card from '../Card' -import { X } from 'react-feather' import { WETH } from '@uniswap/sdk' import { isMobile } from 'react-device-detect' @@ -46,9 +45,7 @@ const Title = styled.div` const TitleText = styled.div` font-size: 24px; font-weight: 700; - background: linear-gradient(119.64deg, #fb1868 -5.55%, #ff00f3 154.46%); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; + color: ${({ theme }) => theme.black}; margin-left: 12px; ` @@ -58,22 +55,21 @@ const AccountElement = styled.div` display: flex; flex-direction: row; align-items: center; - background-color: ${({ theme }) => theme.outlineGrey}; + background-color: ${({ theme, active }) => (!active ? theme.white : theme.outlineGrey)}; border: 1px solid ${({ theme }) => theme.outlineGrey}; border-radius: 8px; - padding-left: 8px; + padding-left: ${({ active }) => (active ? '8px' : 0)}; :focus { border: 1px solid blue; } - /* width: 100%; */ ` const FixedPopupColumn = styled(AutoColumn)` position: absolute; top: 80px; right: 20px - width: 340px; + width: 380px; ` const StyledClose = styled(X)` @@ -86,12 +82,10 @@ const StyledClose = styled(X)` ` const Popup = styled(Card)` - box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.04), 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 16px 24px rgba(0, 0, 0, 0.04), - 0px 24px 32px rgba(0, 0, 0, 0.04); z-index: 9999; border-radius: 8px; padding: 1rem; - background: ${theme => theme.white}; + background-color: white; ` export default function Header() { @@ -115,8 +109,8 @@ export default function Header() { - - {!isMobile ? ( + + {!isMobile && account ? ( {userEthBalance && userEthBalance?.toFixed(4) + ' ETH'} diff --git a/src/components/PoolFinder/index.js b/src/components/PoolFinder/index.tsx similarity index 86% rename from src/components/PoolFinder/index.js rename to src/components/PoolFinder/index.tsx index f54a1cc135..1056aafed8 100644 --- a/src/components/PoolFinder/index.js +++ b/src/components/PoolFinder/index.tsx @@ -1,24 +1,24 @@ import React, { useState } from 'react' -import { JSBI } from '@uniswap/sdk' import { withRouter } from 'react-router-dom' - -import { useToken } from '../../contexts/Tokens' -import { useExchange } from '../../contexts/Exchanges' -import { useWeb3React } from '@web3-react/core' -import { useAddressBalance } from '../../contexts/Balances' -import { usePopups } from '../../contexts/Application' +import { TokenAmount, JSBI, Token, Exchange } from '@uniswap/sdk' import Row from '../Row' import TokenLogo from '../TokenLogo' import SearchModal from '../SearchModal' import PositionCard from '../PositionCard' +import DoubleTokenLogo from '../DoubleLogo' import { Link } from '../../theme' import { Text } from 'rebass' import { Plus } from 'react-feather' import { LightCard } from '../Card' -import Column, { AutoColumn, ColumnCenter } from '../Column' +import { AutoColumn, ColumnCenter } from '../Column' import { ButtonPrimary, ButtonDropwdown, ButtonDropwdownLight } from '../Button' -import DoubleTokenLogo from '../DoubleLogo' + +import { useToken } from '../../contexts/Tokens' +import { usePopups } from '../../contexts/Application' +import { useExchange } from '../../contexts/Exchanges' +import { useWeb3React } from '@web3-react/core' +import { useAddressBalance } from '../../contexts/Balances' function PoolFinder({ history }) { const Fields = { @@ -27,27 +27,25 @@ function PoolFinder({ history }) { } const { account } = useWeb3React() - const [showSearch, setShowSearch] = useState(false) - const [activeField, setActiveField] = useState(Fields.TOKEN0) + const [showSearch, setShowSearch] = useState(false) + const [activeField, setActiveField] = useState(Fields.TOKEN0) const [, addPopup] = usePopups() - const [token0Address, setToken0Address] = useState() - const [token1Address, setToken1Address] = useState() + const [token0Address, setToken0Address] = useState() + const [token1Address, setToken1Address] = useState() - const token0 = useToken(token0Address) - const token1 = useToken(token1Address) + const token0: Token = useToken(token0Address) + const token1: Token = useToken(token1Address) - const exchange = useExchange(token0, token1) + const exchange: Exchange = useExchange(token0, token1) + const position: TokenAmount = useAddressBalance(account, exchange?.liquidityToken) - const position = useAddressBalance(account, exchange?.liquidityToken) - - const newExchange = exchange && JSBI.equal(exchange.reserve0.raw, JSBI.BigInt(0)) - - const allowImport = position && JSBI.greaterThan(position.raw, JSBI.BigInt(0)) + const newExchange: boolean = exchange && JSBI.equal(exchange.reserve0.raw, JSBI.BigInt(0)) + const allowImport: boolean = position && JSBI.greaterThan(position.raw, JSBI.BigInt(0)) function endSearch() { - history.goBack() + history.goBack() // return to previous page addPopup( diff --git a/src/components/SearchModal/index.js b/src/components/SearchModal/index.js index 4045211c91..360d2d385a 100644 --- a/src/components/SearchModal/index.js +++ b/src/components/SearchModal/index.js @@ -2,12 +2,11 @@ import React, { useState, useRef, useMemo, useEffect } from 'react' import '@reach/tooltip/styles.css' import styled from 'styled-components' import escapeStringRegex from 'escape-string-regexp' +import { JSBI } from '@uniswap/sdk' import { Link } from 'react-router-dom' import { ethers } from 'ethers' import { isMobile } from 'react-device-detect' import { withRouter } from 'react-router-dom' -import { JSBI } from '@uniswap/sdk' - import { Link as StyledLink } from '../../theme/components' import Modal from '../Modal' @@ -107,11 +106,17 @@ const PaddedItem = styled(RowBetween)` const MenuItem = styled(PaddedItem)` cursor: pointer; - :hover { background-color: ${({ theme }) => theme.tokenRowHover}; } ` +// filters on results +const FILTERS = { + VOLUME: 'VOLUME', + LIQUIDITY: 'LIQUIDITY', + BALANCES: 'BALANCES' +} + function SearchModal({ history, isOpen, @@ -120,28 +125,26 @@ function SearchModal({ urlAddedTokens, filterType, hiddenToken, - showSendWithSwap, - onTokenSelectSendWithSwap + showSendWithSwap }) { const { t } = useTranslation() - const { account, chainId } = useWeb3React() - const [searchQuery, setSearchQuery] = useState('') - - // get all exchanges + const allTokens = useAllTokens() const allExchanges = useAllExchanges() - const token = useToken(searchQuery) + const allBalances = useAllBalances() + const [searchQuery, setSearchQuery] = useState('') + const [sortDirection, setSortDirection] = useState(true) + + const token = useToken(searchQuery) const tokenAddress = token && token.address - // get all tokens - const allTokens = useAllTokens() + // amount of tokens to display at once + const [, setTokensShown] = useState(0) + const [, setPairsShown] = useState(0) - // all balances for both account and exchanges - let allBalances = useAllBalances() - - const [sortDirection, setSortDirection] = useState(true) + const [activeFilter, setActiveFilter] = useState(FILTERS.BALANCES) const tokenList = useMemo(() => { return Object.keys(allTokens) @@ -149,12 +152,10 @@ function SearchModal({ if (allTokens[a].symbol && allTokens[b].symbol) { const aSymbol = allTokens[a].symbol.toLowerCase() const bSymbol = allTokens[b].symbol.toLowerCase() - // pin ETH to top if (aSymbol === 'ETH'.toLowerCase() || bSymbol === 'ETH'.toLowerCase()) { return aSymbol === bSymbol ? 0 : aSymbol === 'ETH'.toLowerCase() ? -1 : 1 } - // sort by balance const balanceA = allBalances?.[account]?.[a] const balanceB = allBalances?.[account]?.[b] @@ -162,16 +163,12 @@ function SearchModal({ if (balanceA && !balanceB) { return sortDirection } - if (!balanceA && balanceB) { return sortDirection * -1 } - if (balanceA && balanceB) { return sortDirection * parseFloat(balanceA.toExact()) > parseFloat(balanceB.toExact()) ? -1 : 1 } - - // sort alphabetically return aSymbol < bSymbol ? -1 : aSymbol > bSymbol ? 1 : 0 } else { return 0 @@ -181,16 +178,11 @@ function SearchModal({ if (k === hiddenToken) { return false } - - let balance - // only update if we have data - balance = allBalances?.[account]?.[k] - return { name: allTokens[k].name, symbol: allTokens[k].symbol, address: k, - balance: balance + balance: allBalances?.[account]?.[k] } }) }, [allTokens, allBalances, account, sortDirection, hiddenToken]) @@ -198,10 +190,7 @@ function SearchModal({ const filteredTokenList = useMemo(() => { return tokenList.filter(tokenEntry => { const inputIsAddress = searchQuery.slice(0, 2) === '0x' - - // check the regex for each field const regexMatches = Object.keys(tokenEntry).map(tokenEntryKey => { - // if address field only search if input starts with 0x if (tokenEntryKey === 'address') { return ( inputIsAddress && @@ -218,21 +207,14 @@ function SearchModal({ }) }, [tokenList, searchQuery]) - function _onTokenSelect(address, sendWithSwap = false) { - if (sendWithSwap) { - setSearchQuery('') - onTokenSelectSendWithSwap(address) - onDismiss() - } else { - setSearchQuery('') - onTokenSelect(address) - onDismiss() - } + function _onTokenSelect(address) { + setSearchQuery('') + onTokenSelect(address) + onDismiss() } // manage focus on modal show const inputRef = useRef() - function onInput(event) { const input = event.target.value const checksummedInput = isAddress(input) @@ -244,34 +226,39 @@ function SearchModal({ onDismiss() } - // amount of tokens to display at once - const [, setTokensShown] = useState(0) - const [, setPairsShown] = useState(0) - - // filters on results - const FILTERS = { - VOLUME: 'VOLUME', - LIQUIDITY: 'LIQUIDITY', - BALANCES: 'BALANCES' - } - const [activeFilter, setActiveFilter] = useState(FILTERS.BALANCES) - // sort tokens const escapeStringRegexp = string => string - // sort pairs - 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] + const sortedPairList = useMemo(() => { + return Object.keys(allExchanges).sort((a, b) => { + // sort by balance + const balanceA = allBalances?.[account]?.[a] + const balanceB = allBalances?.[account]?.[b] + if (balanceA && !balanceB) { + return sortDirection + } + if (!balanceA && balanceB) { + return sortDirection * -1 + } + if (balanceA && balanceB) { + const order = sortDirection * (parseFloat(balanceA.toExact()) > parseFloat(balanceB.toExact()) ? -1 : 1) + return order ? 1 : -1 + } else { + return 0 + } + }) + }, [account, allBalances, allExchanges, sortDirection]) + + const filteredPairList = useMemo(() => { + const isAddress = searchQuery.slice(0, 2) === '0x' + return sortedPairList.filter(exchangeAddress => { + const exchange = allExchanges[exchangeAddress] 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) || @@ -288,7 +275,7 @@ function SearchModal({ return regexMatches.some(m => m) }) - }, [allExchanges, allTokens, searchQuery]) + }, [account, allBalances, allExchanges, allTokens, searchQuery, sortDirection]) // update the amount shown as filtered list changes useEffect(() => { @@ -312,9 +299,7 @@ function SearchModal({ filteredPairList.map((exchangeAddress, i) => { const token0 = allTokens[allExchanges[exchangeAddress].token0] const token1 = allTokens[allExchanges[exchangeAddress].token1] - const balance = allBalances?.[account]?.[exchangeAddress]?.toSignificant(6) - return ( -const SlippageRow = styled(WrappedSlippageRow)` - position: relative; - flex-wrap: ${({ wrap }) => wrap && 'wrap'}; - flex-direction: row; - justify-content: flex-start; - align-items: center; - width: 100%; - padding: 0; - padding-top: ${({ wrap }) => wrap && '0.25rem'}; -` - -const QuestionWrapper = styled.button` - display: flex; - align-items: center; - justify-content: center; - margin: 0; - padding: 0; - margin-left: 0.4rem; - padding: 0.2rem; - border: none; - background: none; - outline: none; - cursor: default; - border-radius: 36px; - - :hover, - :focus { - opacity: 0.7; - } -` - -const HelpCircleStyled = styled.img` - height: 18px; - width: 18px; -` - -const fadeIn = keyframes` - from { - opacity : 0; - } - - to { - opacity : 1; - } -` - -const Popup = styled(Flex)` - position: absolute; - width: 228px; - left: -78px; - top: -94px; - flex-direction: column; - align-items: center; - padding: 0.6rem 1rem; - line-height: 150%; - background: ${({ theme }) => theme.inputBackground}; - border: 1px solid ${({ theme }) => theme.mercuryGray}; - - border-radius: 8px; - - animation: ${fadeIn} 0.15s linear; - - color: ${({ theme }) => theme.textColor}; - font-style: italic; - - ${({ theme }) => theme.mediaWidth.upToSmall` - left: -20px; - `} -` - -const FancyButton = styled.button` - color: ${({ theme }) => theme.textColor}; - align-items: center; - min-width: 55px; - border-radius: 36px; - font-size: 12px; - border: 1px solid ${({ theme }) => theme.mercuryGray}; - outline: none; - background: ${({ theme }) => theme.inputBackground}; - - :hover { - cursor: inherit; - border: 1px solid ${({ theme }) => theme.chaliceGray}; - } - :focus { - border: 1px solid ${({ theme }) => theme.royalBlue}; - } -` - -const Option = styled(FancyButton)` - margin-right: 8px; - margin-top: 6px; - - :hover { - cursor: pointer; - } - - ${({ active, theme }) => - active && - css` - background-color: ${({ theme }) => theme.royalBlue}; - color: ${({ theme }) => theme.white}; - border: none; - - :hover { - border: none; - box-shadow: none; - background-color: ${({ theme }) => darken(0.05, theme.royalBlue)}; - } - - :focus { - border: none; - box-shadow: none; - background-color: ${({ theme }) => lighten(0.05, theme.royalBlue)}; - } - - :active { - background-color: ${({ theme }) => darken(0.05, theme.royalBlue)}; - } - - :hover:focus { - background-color: ${({ theme }) => theme.royalBlue}; - } - :hover:focus:active { - background-color: ${({ theme }) => darken(0.05, theme.royalBlue)}; - } - `} -` - -const OptionLarge = styled(Option)` - width: 120px; -` - -const Input = styled.input` - background: ${({ theme }) => theme.inputBackground}; - flex-grow: 1; - font-size: 12px; - min-width: 20px; - - outline: none; - box-sizing: border-box; - - &::-webkit-outer-spin-button, - &::-webkit-inner-spin-button { - -webkit-appearance: none; - } - - cursor: inherit; - - color: ${({ theme }) => theme.doveGray}; - text-align: left; - ${({ active }) => - active && - css` - color: initial; - cursor: initial; - text-align: right; - `} - - ${({ placeholder }) => - placeholder !== 'Custom' && - css` - text-align: right; - color: ${({ theme }) => theme.textColor}; - `} - - ${({ color }) => - color === 'red' && - css` - color: ${({ theme }) => theme.salmonRed}; - `} -` - -const BottomError = styled.div` - ${({ show }) => - show && - css` - padding-top: 12px; - `} - color: ${({ theme }) => theme.doveGray}; - ${({ color }) => - color === 'red' && - css` - color: ${({ theme }) => theme.salmonRed}; - `} -` - -const OptionCustom = styled(FancyButton)` - position: relative; - width: 120px; - margin-top: 6px; - padding: 0 0.75rem; - - ${({ active }) => - active && - css` - border: 1px solid ${({ theme }) => theme.royalBlue}; - :hover { - border: 1px solid ${({ theme }) => darken(0.1, theme.royalBlue)}; - } - `} - - ${({ color }) => - color === 'red' && - css` - border: 1px solid ${({ theme }) => theme.salmonRed}; - `} - - input { - width: 100%; - height: 100%; - border: 0px; - border-radius: 2rem; - } -` - -const Bold = styled.span` - font-weight: 500; -` - -const LastSummaryText = styled.div` - padding-top: 0.5rem; -` - -const SlippageSelector = styled.div` - background-color: ${({ theme }) => darken(0.04, theme.concreteGray)}; - padding: 1rem 1.25rem 1rem 1.25rem; - border-radius: 12px 12px 0 0; -` - -const Percent = styled.div` - color: inherit; - font-size: 0, 8rem; - flex-grow: 0; - - ${({ color, theme }) => - (color === 'faded' && - css` - color: ${theme.doveGray}; - `) || - (color === 'red' && - css` - color: ${theme.salmonRed}; - `)}; -` - -const Faded = styled.span` - opacity: 0.7; -` - -const TransactionInfo = styled.div` - padding: 1.25rem 1.25rem 1rem 1.25rem; -` - -const ValueWrapper = styled.span` - padding: 0.125rem 0.3rem 0.1rem 0.3rem; - background-color: ${({ theme }) => darken(0.04, theme.concreteGray)}; - border-radius: 12px; - font-variant: tabular-nums; -` - -const DeadlineSelector = styled.div` - background-color: ${({ theme }) => darken(0.04, theme.concreteGray)}; - padding: 1rem 1.25rem 1rem 1.25rem; - border-radius: 0 0 12px 12px; -` -const DeadlineRow = SlippageRow -const DeadlineInput = OptionCustom - -export default function TransactionDetails(props) { - const { t } = useTranslation() - - const [activeIndex, setActiveIndex] = useState(3) - - const [warningType, setWarningType] = useState(WARNING_TYPE.none) - - const inputRef = useRef() - - const [showPopup, setPopup] = useState(false) - - const [userInput, setUserInput] = useState('') - const debouncedInput = useDebounce(userInput, 150) - - useEffect(() => { - if (activeIndex === 4) { - checkBounds(debouncedInput) - } - }) - - const [deadlineInput, setDeadlineInput] = useState('') - - function renderSummary() { - let contextualInfo = '' - let isError = false - if (props.brokenTokenWarning) { - contextualInfo = t('brokenToken') - isError = true - } else if (props.inputError || props.independentError) { - contextualInfo = props.inputError || props.independentError - isError = true - } else if (!props.inputCurrency || !props.outputCurrency) { - contextualInfo = t('selectTokenCont') - } else if (!props.independentValue) { - contextualInfo = t('enterValueCont') - } else if (props.sending && !props.recipientAddress) { - contextualInfo = t('noRecipient') - } else if (props.sending && !isAddress(props.recipientAddress)) { - contextualInfo = t('invalidRecipient') - } else if (!props.account) { - contextualInfo = t('noWallet') - isError = true - } - - const slippageWarningText = props.highSlippageWarning - ? t('highSlippageWarning') - : props.slippageWarning - ? t('slippageWarning') - : '' - - return ( - - ) - } - - const dropDownContent = () => { - return ( - <> - {renderTransactionDetails()} - - - Limit additional price slippage - { - setPopup(!showPopup) - }} - onMouseEnter={() => { - setPopup(true) - }} - onMouseLeave={() => { - setPopup(false) - }} - > - - - {showPopup ? ( - - Lowering this limit decreases your risk of frontrunning. However, this makes it more likely that your - transaction will fail due to normal price movements. - - ) : ( - '' - )} - - - - { - setFromFixed(2, 0.5) - }} - active={activeIndex === 2} - > - 0.5% (suggested) - - - { - setFromCustom() - }} - > - - {!(warningType === WARNING_TYPE.none || warningType === WARNING_TYPE.emptyInput) && ( - - ⚠️ - - )} - - - % - - - - - - - {activeIndex === 4 && warningType.toString() === 'none' && 'Custom slippage value'} - {warningType === WARNING_TYPE.emptyInput && 'Enter a slippage percentage'} - {warningType === WARNING_TYPE.invalidEntryBound && 'Please select a value no greater than 50%'} - {warningType === WARNING_TYPE.riskyEntryHigh && 'Your transaction may be frontrun'} - {warningType === WARNING_TYPE.riskyEntryLow && 'Your transaction may fail'} - - - - - Set swap deadline (minutes from now) - - - - - - - - ) - } - - const setFromCustom = () => { - setActiveIndex(4) - inputRef.current.focus() - // if there's a value, evaluate the bounds - checkBounds(debouncedInput) - } - - // destructure props for to limit effect callbacks - const setRawSlippage = props.setRawSlippage - const setRawTokenSlippage = props.setRawTokenSlippage - const setcustomSlippageError = props.setcustomSlippageError - const setDeadline = props.setDeadline - - const updateSlippage = useCallback( - newSlippage => { - // round to 2 decimals to prevent ethers error - let numParsed = parseInt(newSlippage * 100) - - // set both slippage values in parents - setRawSlippage(numParsed) - setRawTokenSlippage(numParsed) - }, - [setRawSlippage, setRawTokenSlippage] - ) - - // used for slippage presets - const setFromFixed = useCallback( - (index, slippage) => { - // update slippage in parent, reset errors and input state - updateSlippage(slippage) - setWarningType(WARNING_TYPE.none) - setActiveIndex(index) - setcustomSlippageError('valid`') - }, - [setcustomSlippageError, updateSlippage] - ) - - /** - * @todo - * Breaks without useState here, able to - * break input parsing if typing is faster than - * debounce time - */ - - const [initialSlippage] = useState(props.rawSlippage) - - useEffect(() => { - switch (Number.parseInt(initialSlippage)) { - case 10: - setFromFixed(1, 0.1) - break - case 50: - setFromFixed(2, 0.5) - break - case 100: - setFromFixed(3, 1) - break - default: - // restrict to 2 decimal places - let acceptableValues = [/^$/, /^\d{1,2}$/, /^\d{0,2}\.\d{0,2}$/] - // if its within accepted decimal limit, update the input state - if (acceptableValues.some(val => val.test(initialSlippage / 100))) { - setUserInput(initialSlippage / 100) - setActiveIndex(4) - } - } - }, [initialSlippage, setFromFixed]) - - const checkBounds = useCallback( - slippageValue => { - setWarningType(WARNING_TYPE.none) - setcustomSlippageError('valid') - - if (slippageValue === '' || slippageValue === '.') { - setcustomSlippageError('invalid') - return setWarningType(WARNING_TYPE.emptyInput) - } - - // check bounds and set errors - if (Number(slippageValue) < 0 || Number(slippageValue) > 50) { - setcustomSlippageError('invalid') - return setWarningType(WARNING_TYPE.invalidEntryBound) - } - if (Number(slippageValue) >= 0 && Number(slippageValue) < 0.1) { - setcustomSlippageError('valid') - setWarningType(WARNING_TYPE.riskyEntryLow) - } - if (Number(slippageValue) > 5) { - setcustomSlippageError('warning') - setWarningType(WARNING_TYPE.riskyEntryHigh) - } - //update the actual slippage value in parent - updateSlippage(Number(slippageValue)) - }, - [setcustomSlippageError, updateSlippage] - ) - - // check that the theyve entered number and correct decimal - const parseInput = e => { - let input = e.target.value - - // restrict to 2 decimal places - let acceptableValues = [/^$/, /^\d{1,2}$/, /^\d{0,2}\.\d{0,2}$/] - // if its within accepted decimal limit, update the input state - if (acceptableValues.some(a => a.test(input))) { - setUserInput(input) - } - } - - const [initialDeadline] = useState(props.deadline) - - useEffect(() => { - setDeadlineInput(initialDeadline / 60) - }, [initialDeadline]) - - const parseDeadlineInput = e => { - const input = e.target.value - - const acceptableValues = [/^$/, /^\d+$/] - if (acceptableValues.some(re => re.test(input))) { - setDeadlineInput(input) - setDeadline(parseInt(input) * 60) - } - } - - const b = text => {text} - - const renderTransactionDetails = () => { - if (props.independentField === props.INPUT) { - return props.sending ? ( - -
- {t('youAreSelling')}{' '} - - {b( - `${amountFormatter( - props.independentValueParsed, - props.independentDecimals, - Math.min(4, props.independentDecimals) - )} ${props.inputSymbol}` - )} - -
- - {b(props.recipientAddress)} {t('willReceive')}{' '} - - {b( - `${amountFormatter( - props.dependentValueMinumum, - props.dependentDecimals, - Math.min(4, props.dependentDecimals) - )} ${props.outputSymbol}` - )} - {' '} - - - {t('priceChange')} {b(`${props.percentSlippageFormatted}%`)} - -
- ) : ( - -
- {t('youAreSelling')}{' '} - - {b( - `${amountFormatter( - props.independentValueParsed, - props.independentDecimals, - Math.min(4, props.independentDecimals) - )} ${props.inputSymbol}` - )} - {' '} - {t('forAtLeast')} - - {b( - `${amountFormatter( - props.dependentValueMinumum, - props.dependentDecimals, - Math.min(4, props.dependentDecimals) - )} ${props.outputSymbol}` - )} - -
- - {t('priceChange')} {b(`${props.percentSlippageFormatted}%`)} - -
- ) - } else { - return props.sending ? ( - -
- {t('youAreSending')}{' '} - - {b( - `${amountFormatter( - props.independentValueParsed, - props.independentDecimals, - Math.min(4, props.independentDecimals) - )} ${props.outputSymbol}` - )} - {' '} - {t('to')} {b(props.recipientAddress)} {t('forAtMost')}{' '} - - {b( - `${amountFormatter( - props.dependentValueMaximum, - props.dependentDecimals, - Math.min(4, props.dependentDecimals) - )} ${props.inputSymbol}` - )} - {' '} -
- - {t('priceChange')} {b(`${props.percentSlippageFormatted}%`)} - -
- ) : ( - - {t('youAreBuying')}{' '} - - {b( - `${amountFormatter( - props.independentValueParsed, - props.independentDecimals, - Math.min(4, props.independentDecimals) - )} ${props.outputSymbol}` - )} - {' '} - {t('forAtMost')}{' '} - - {b( - `${amountFormatter( - props.dependentValueMaximum, - props.dependentDecimals, - Math.min(4, props.dependentDecimals) - )} ${props.inputSymbol}` - )} - {' '} - - {t('priceChange')} {b(`${props.percentSlippageFormatted}%`)} - - - ) - } - } - return <>{renderSummary()} -} diff --git a/src/components/Web3ReactManager/index.js b/src/components/Web3ReactManager/index.js index 642f677c8a..3df8d6febd 100644 --- a/src/components/Web3ReactManager/index.js +++ b/src/components/Web3ReactManager/index.js @@ -39,7 +39,6 @@ export default function Web3ReactManager({ children }) { const triedEager = useEagerConnect() // after eagerly trying injected, if the network connect ever isn't active or in an error state, activate itd - // TODO think about not doing this at all useEffect(() => { if (triedEager && !networkActive && !networkError && !active) { activateNetwork(network) diff --git a/src/constants/abis/router.json b/src/constants/abis/router.json index d854f92e2f..c78afc8802 100644 --- a/src/constants/abis/router.json +++ b/src/constants/abis/router.json @@ -1,21 +1,4 @@ [ - { - "inputs": [ - { - "internalType": "address", - "name": "_WETH", - "type": "address" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "constructor" - }, - { - "payable": true, - "stateMutability": "payable", - "type": "fallback" - }, { "constant": true, "inputs": [], @@ -153,32 +136,6 @@ "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": [], @@ -191,7 +148,7 @@ } ], "payable": false, - "stateMutability": "view", + "stateMutability": "pure", "type": "function" }, { @@ -308,52 +265,6 @@ "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": [ @@ -525,6 +436,11 @@ "name": "deadline", "type": "uint256" }, + { + "internalType": "bool", + "name": "approveMax", + "type": "bool" + }, { "internalType": "uint8", "name": "v", @@ -596,6 +512,11 @@ "name": "deadline", "type": "uint256" }, + { + "internalType": "bool", + "name": "approveMax", + "type": "bool" + }, { "internalType": "uint8", "name": "v", @@ -629,37 +550,6 @@ "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": [ @@ -895,35 +785,5 @@ "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.ts b/src/constants/index.ts index 5dd60f8147..ef8def3153 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -1,16 +1,16 @@ import { injected, walletconnect, walletlink, fortmatic, portis } from '../connectors' export const FACTORY_ADDRESSES = { - 1: '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95', - 3: '0x9c83dCE8CA20E9aAF9D3efc003b2ea62aBC08351', - 4: '0xf5D915570BC477f9B8D6C0E980aA81757A3AaC36', - 42: '0xD3E51Ef092B2845f10401a0159B2B96e8B6c3D30' + 1: '', + 3: '', + 4: '0xe2f197885abe8ec7c866cFf76605FD06d4576218', + 42: '' } export const ROUTER_ADDRESSES = { 1: '', 3: '', - 4: '0xd9210Ff5A0780E083BB40e30d005d93a2DcFA4EF', + 4: '0xcDbE04934d89e97a24BCc07c3562DC8CF17d8167', 42: '' } @@ -100,12 +100,4 @@ export const SUPPORTED_WALLETS = } } -// list of tokens that lock fund on adding liquidity - used to disable button -export const brokenTokens = [ - '0xB8c77482e45F1F44dE1745F52C74426C631bDD52', - '0x95dAaaB98046846bF4B2853e23cba236fa394A31', - '0x55296f69f40Ea6d20E478533C15A6B08B654E758', - '0xc3761EB917CD790B30dAD99f6Cc5b4Ff93C4F9eA' -] - export const NetworkContextName = 'NETWORK' diff --git a/src/contexts/Application.js b/src/contexts/Application.js index 96c8e03548..6b0e1101d4 100644 --- a/src/contexts/Application.js +++ b/src/contexts/Application.js @@ -162,6 +162,7 @@ export function usePopups() { if (key === item.key) { item.show = false } + return true }) setPopups(currentPopups) } diff --git a/src/contexts/Balances.tsx b/src/contexts/Balances.tsx index cdcffdc2cb..b168aadb11 100644 --- a/src/contexts/Balances.tsx +++ b/src/contexts/Balances.tsx @@ -427,6 +427,7 @@ export function useAllBalances(): Array { } } } + return true }) }) return newBalances @@ -443,6 +444,9 @@ export function useAddressBalance(address: string, token: Token): TokenAmount | const { chainId } = useWeb3React() const [state, { startListening, stopListening }] = useBalancesContext() + const value = typeof chainId === 'number' ? state?.[chainId]?.[address]?.[token?.address]?.value : undefined + const formattedValue = value && token && new TokenAmount(token, value) + /** * @todo * when catching for token, causes infinite rerender @@ -457,9 +461,6 @@ export function useAddressBalance(address: string, token: Token): TokenAmount | } }, [chainId, address, startListening, stopListening]) - const value = typeof chainId === 'number' ? state?.[chainId]?.[address]?.[token?.address]?.value : undefined - const formattedValue = value && token && new TokenAmount(token, value) - return useMemo(() => formattedValue, [formattedValue]) } @@ -476,6 +477,7 @@ export function useAccountLPBalances(account: string) { stopListening(chainId, account, exchangeAddress) } } + return true }) }, [account, allExchanges, chainId, startListening, stopListening]) } diff --git a/src/contexts/Exchanges.tsx b/src/contexts/Exchanges.tsx index 7ae968eba2..bcf84f692d 100644 --- a/src/contexts/Exchanges.tsx +++ b/src/contexts/Exchanges.tsx @@ -11,7 +11,7 @@ const UPDATE = 'UPDATE' 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'] //dai ], [ INITIAL_TOKENS_CONTEXT[ChainId.RINKEBY]['0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735'], diff --git a/src/contexts/Tokens.tsx b/src/contexts/Tokens.tsx index ada5e3215a..82438fde90 100644 --- a/src/contexts/Tokens.tsx +++ b/src/contexts/Tokens.tsx @@ -8,7 +8,7 @@ const UPDATE = 'UPDATE' export const ALL_TOKENS = [ WETH[ChainId.RINKEBY], new Token(ChainId.RINKEBY, '0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735', 18, 'DAI', 'Dai Stablecoin'), - new Token(ChainId.RINKEBY, '0x8ab15C890E5C03B5F240f2D146e3DF54bEf3Df44', 18, 'IANV2', 'IAn V2 Coin') + new Token(ChainId.RINKEBY, '0x8ab15C890E5C03B5F240f2D146e3DF54bEf3Df44', 18, 'IANV2', 'IAn V2 /Coin') ] // only meant to be used in exchanges.ts! @@ -112,6 +112,11 @@ export function useAllTokens(): string[] { const [state] = useTokensContext() return useMemo(() => { + // hardcode overide weth as ETH + if (state && state[chainId]) { + state[chainId][WETH[chainId].address].symbol = 'ETH' + state[chainId][WETH[chainId].address].name = 'ETH' + } return state?.[chainId] || {} }, [state, chainId]) } diff --git a/src/hooks/index.js b/src/hooks/index.js index a5a494ff98..1e5e804935 100644 --- a/src/hooks/index.js +++ b/src/hooks/index.js @@ -1,23 +1,21 @@ import { useState, useMemo, useCallback, useEffect, useRef } from 'react' import { useWeb3React as useWeb3ReactCore } from '@web3-react/core' -import copy from 'copy-to-clipboard' import { isMobile } from 'react-device-detect' +import copy from 'copy-to-clipboard' -import { NetworkContextName } from '../constants' import ERC20_ABI from '../constants/abis/erc20' -import { getContract, getFactoryContract, getExchangeContract, isAddress } from '../utils' import { injected } from '../connectors' +import { NetworkContextName } from '../constants' +import { getContract, getFactoryContract, getExchangeContract, isAddress } from '../utils' export function useWeb3React() { const context = useWeb3ReactCore() const contextNetwork = useWeb3ReactCore(NetworkContextName) - return context.active ? context : contextNetwork } export function useEagerConnect() { const { activate, active } = useWeb3ReactCore() // specifically using useWeb3ReactCore because of what this hook does - const [tried, setTried] = useState(false) useEffect(() => { @@ -265,6 +263,5 @@ export function usePrevious(value) { export function useToggle(initialState = false) { const [state, setState] = useState(initialState) const toggle = useCallback(() => setState(state => !state), []) - return [state, toggle] } diff --git a/src/pages/App.js b/src/pages/App.js index a5c176b849..a8d307a35e 100644 --- a/src/pages/App.js +++ b/src/pages/App.js @@ -2,10 +2,9 @@ import React, { Suspense, lazy } from 'react' import styled from 'styled-components' import { BrowserRouter, Redirect, Route, Switch } from 'react-router-dom' -import Web3ReactManager from '../components/Web3ReactManager' import Header from '../components/Header' - import NavigationTabs from '../components/NavigationTabs' +import Web3ReactManager from '../components/Web3ReactManager' import { isAddress, getAllQueryParams } from '../utils' const Swap = lazy(() => import('./Swap')) @@ -65,6 +64,7 @@ export default function App() { {/* this Suspense is for route code-splitting */} + } /> } /> } /> (selected ? '128px' : '180px')} - padding: 8px 12px; - background-color: ${({ selected, theme }) => (selected ? theme.buttonBackgroundPlain : theme.royalBlue)}; - color: ${({ selected, theme }) => (selected ? theme.textColor : theme.white)}; - border: 1px solid - ${({ selected, theme }) => (selected ? theme.outlineGrey : theme.royalBlue)}; - border-radius: 8px; - outline: none; - cursor: pointer; - user-select: none; - - :hover { - border: 1px solid - ${({ selected, theme }) => (selected ? darken(0.1, theme.outlineGrey) : darken(0.1, theme.royalBlue))}; - } - - :focus { - border: 1px solid ${({ selected, theme }) => - selected ? darken(0.1, theme.outlineGrey) : darken(0.1, theme.royalBlue)}; - } - - :active { - background-color: ${({ selected, theme }) => (selected ? theme.buttonBackgroundPlain : theme.royalBlue)}; - } -` - -const StyledDropDown = styled(DropDown)` - height: 35%; - - path { - stroke: ${({ selected, theme }) => (selected ? theme.textColor : theme.white)}; - } -` - -const InputGroup = styled(AutoColumn)` - position: relative; - padding: 40px 0; -` - -const QRWrapper = styled.div` - display: flex; - align-items: center; - justify-content: center; - border: 1px solid ${({ theme }) => theme.outlineGrey}; - background: #fbfbfb; - padding: 4px; - border-radius: 8px; -` - -const StyledInput = styled.input` - width: ${({ width }) => width}; - border: none; - outline: none; - font-size: 20px; - - ::placeholder { - color: #edeef2; - } -` - -const StyledNumerical = styled(NumericalInput)` - text-align: center; - font-size: 48px; - font-weight: 500px; - width: 100%; - - ::placeholder { - color: #edeef2; - } -` - -const MaxButton = styled.button` - position: absolute; - right: 70px; - padding: 0.5rem 1rem; - background-color: ${({ theme }) => theme.zumthorBlue}; - border: 1px solid ${({ theme }) => theme.zumthorBlue}; - border-radius: 0.5rem; - font-size: 1rem; - font-weight: 500; - cursor: pointer; - margin-right: 0.5rem; - color: ${({ theme }) => theme.royalBlue}; - :hover { - border: 1px solid ${({ theme }) => theme.royalBlue}; - } - :focus { - border: 1px solid ${({ theme }) => theme.royalBlue}; - outline: none; - } -` export default function Send() { - const { account } = useWeb3React() - - // setting for send with swap or regular swap - const [withSwap, setWithSwap] = useState(true) - - // modals - const [modalOpen, setModalOpen] = useState(false) - const [showConfirm, setShowConfirm] = useState(false) - - // token selected - const [activeTokenAddress, setActiveTokenAddress] = useState() - const token = useToken(activeTokenAddress) - - // user inputs - const [typedValue, setTypedValue] = useState('') - const [amount, setAmount] = useState(null) - const [recipient, setRecipient] = useState('0x74Aa01d162E6dC6A657caC857418C403D48E2D77') - - //ENS - const recipientENS = useENSName(recipient) - - // balances - const userBalance = useAddressBalance(account, token) - - //errors - const [generalError, setGeneralError] = useState('') - const [amountError, setAmountError] = useState('') - const [recipientError, setRecipientError] = useState('') - - function parseInputAmount(newtypedValue) { - setTypedValue(newtypedValue) - if (!!token && newtypedValue !== '' && newtypedValue !== '.') { - const typedValueParsed = parseUnits(newtypedValue, token.decimals).toString() - setAmount(new TokenAmount(token, typedValueParsed)) - } - } - - function onMax() { - if (userBalance) { - setTypedValue(userBalance.toExact()) - setAmount(userBalance) - } - } - - const atMax = amount && userBalance && JSBI.equal(amount.raw, userBalance.raw) ? true : false - - //error detection - useEffect(() => { - setGeneralError('') - setRecipientError('') - setAmountError('') - - if (!amount) { - setGeneralError('Enter an amount') - } - if (!isAddress(recipient)) { - setRecipientError('Enter a valid address') - } - if (!!!token) { - setGeneralError('Select a token') - } - if (amount && userBalance && JSBI.greaterThan(amount.raw, userBalance.raw)) { - setAmountError('Insufficient Balance') - } - }, [recipient, token, amount, userBalance]) - - const TopContent = () => { - return ( - - - - {amount?.toFixed(8)} - - - - - - {recipient?.slice(0, 6)}...{recipient?.slice(36, 42)} - - - ) - } - - const BottomContent = () => { - return ( - - - - Confirm send - - - - ) - } - - const [attemptedSend, setAttemptedSend] = useState(false) // clicke confirm - const [pendingConfirmation, setPendingConfirmation] = useState(true) // waiting for - - return withSwap ? ( - - ) : ( - <> - { - setModalOpen(false) - }} - filterType="tokens" - onTokenSelect={tokenAddress => setActiveTokenAddress(tokenAddress)} - /> - setShowConfirm(false)} - hash="" - title="Confirm Send" - topContent={TopContent} - bottomContent={BottomContent} - attemptingTxn={attemptedSend} - pendingConfirmation={pendingConfirmation} - pendingText="" - /> - - ) + return } diff --git a/src/pages/Supply/AddLiquidity.tsx b/src/pages/Supply/AddLiquidity.tsx index e77abde77c..286b491586 100644 --- a/src/pages/Supply/AddLiquidity.tsx +++ b/src/pages/Supply/AddLiquidity.tsx @@ -2,10 +2,10 @@ import React, { useReducer, useState, useCallback, useEffect } from 'react' 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 { WETH, TokenAmount, JSBI, Percent, Route, Token, Exchange } from '@uniswap/sdk' -import DoubleLogo from '../../components/DoubleLogo' import TokenLogo from '../../components/TokenLogo' +import DoubleLogo from '../../components/DoubleLogo' import SearchModal from '../../components/SearchModal' import PositionCard from '../../components/PositionCard' import ConfirmationModal from '../../components/ConfirmationModal' @@ -17,8 +17,8 @@ import { AutoColumn, ColumnCenter } from '../../components/Column' import Row, { RowBetween, RowFlat, RowFixed } from '../../components/Row' import { useToken } from '../../contexts/Tokens' -import { useWeb3React } from '../../hooks' import { usePopups } from '../../contexts/Application' +import { useWeb3React } from '../../hooks' import { useAddressBalance } from '../../contexts/Balances' import { useAddressAllowance } from '../../contexts/Allowances' import { useTransactionAdder } from '../../contexts/Transactions' @@ -27,6 +27,7 @@ import { useExchange, useTotalSupply } from '../../contexts/Exchanges' import { BigNumber } from 'ethers/utils' import { ROUTER_ADDRESSES } from '../../constants' import { getRouterContract, calculateGasMargin } from '../../utils' +import { TYPE } from '../../theme' // denominated in bips const ALLOWED_SLIPPAGE = 200 @@ -134,12 +135,9 @@ function reducer( } } -/** - * @todo should we ever not have prepopulated tokens? - * - */ export default function AddLiquidity({ token0, token1 }) { const { account, chainId, library } = useWeb3React() + const [, addPopup] = usePopups() const routerAddress: string = ROUTER_ADDRESSES[chainId] @@ -152,28 +150,28 @@ export default function AddLiquidity({ token0, token1 }) { // input state const [state, dispatch] = useReducer(reducer, initializeAddState(token0, token1)) const { independentField, typedValue, ...fieldData } = state - const dependentField = independentField === Field.INPUT ? Field.OUTPUT : Field.INPUT + const dependentField: Field = independentField === Field.INPUT ? Field.OUTPUT : Field.INPUT // get basic SDK entities - const tokens = { + const tokens: { [field: number]: Token } = { [Field.INPUT]: useToken(fieldData[Field.INPUT].address), [Field.OUTPUT]: useToken(fieldData[Field.OUTPUT].address) } // exhchange data - const exchange = useExchange(tokens[Field.INPUT], tokens[Field.OUTPUT]) - const route = exchange ? new Route([exchange], tokens[independentField]) : undefined - const totalSupply = useTotalSupply(exchange) - const [noLiquidity, setNoLiquidity] = useState(false) + const exchange: Exchange = useExchange(tokens[Field.INPUT], tokens[Field.OUTPUT]) + const route: Route = exchange ? new Route([exchange], tokens[independentField]) : undefined + const totalSupply: TokenAmount = useTotalSupply(exchange) + const [noLiquidity, setNoLiquidity] = useState(false) // used to detect new exchange // state for amount approvals - const inputApproval = useAddressAllowance(account, tokens[Field.INPUT], routerAddress) - const outputApproval = useAddressAllowance(account, tokens[Field.OUTPUT], routerAddress) + const inputApproval: TokenAmount = useAddressAllowance(account, tokens[Field.INPUT], routerAddress) + const outputApproval: TokenAmount = useAddressAllowance(account, tokens[Field.OUTPUT], routerAddress) const [showInputUnlock, setShowInputUnlock] = useState(false) const [showOutputUnlock, setShowOutputUnlock] = useState(false) // get user-pecific and token-specific lookup data - const userBalances = { + const userBalances: { [field: number]: TokenAmount } = { [Field.INPUT]: useAddressBalance(account, tokens[Field.INPUT]), [Field.OUTPUT]: useAddressBalance(account, tokens[Field.OUTPUT]) } @@ -219,7 +217,6 @@ export default function AddLiquidity({ token0, token1 }) { 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) } } @@ -240,12 +237,16 @@ export default function AddLiquidity({ token0, token1 }) { } // check for estimated liquidity minted - const liquidityMinted = - !!exchange && !!parsedAmounts[Field.INPUT] && !!parsedAmounts[Field.OUTPUT] && !!totalSupply - ? exchange.getLiquidityMinted(totalSupply, parsedAmounts[Field.INPUT], parsedAmounts[Field.OUTPUT]) + const liquidityMinted: TokenAmount = + !!exchange && !!parsedAmounts[Field.INPUT] && !!parsedAmounts[Field.OUTPUT] + ? exchange.getLiquidityMinted( + totalSupply ? totalSupply : new TokenAmount(exchange?.liquidityToken, JSBI.BigInt(0)), + parsedAmounts[Field.INPUT], + parsedAmounts[Field.OUTPUT] + ) : undefined - const poolTokenPercentage = + const poolTokenPercentage: Percent = !!liquidityMinted && !!totalSupply ? new Percent(liquidityMinted.raw, totalSupply.add(liquidityMinted).raw) : undefined @@ -271,10 +272,10 @@ export default function AddLiquidity({ token0, token1 }) { }) }, []) - const MIN_ETHER = new TokenAmount(WETH[chainId], JSBI.BigInt(parseEther('.01'))) + const MIN_ETHER: TokenAmount = new TokenAmount(WETH[chainId], JSBI.BigInt(parseEther('.01'))) // get the max amounts user can add - const [maxAmountInput, maxAmountOutput] = [Field.INPUT, Field.OUTPUT].map(index => { + const [maxAmountInput, maxAmountOutput]: TokenAmount[] = [Field.INPUT, Field.OUTPUT].map(index => { const field = Field[index] return !!userBalances[Field[field]] && JSBI.greaterThan( @@ -287,7 +288,7 @@ export default function AddLiquidity({ token0, token1 }) { : undefined }) - const [atMaxAmountInput, atMaxAmountOutput] = [Field.INPUT, Field.OUTPUT].map(index => { + const [atMaxAmountInput, atMaxAmountOutput]: boolean[] = [Field.INPUT, Field.OUTPUT].map(index => { const field = Field[index] const maxAmount = index === Field.INPUT ? maxAmountInput : maxAmountOutput return !!maxAmount && !!parsedAmounts[Field[field]] @@ -357,22 +358,23 @@ export default function AddLiquidity({ token0, token1 }) { // state for txn const addTransaction = useTransactionAdder() - const [txHash, setTxHash] = useState() + const [txHash, setTxHash] = useState('') + // format ETH value for transaction function hex(value: JSBI) { return ethers.utils.bigNumberify(value.toString()) } + // calculate slippage bounds based on current reserves 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)] + } else { + return null } - return null } - const [, addPopup] = usePopups() - async function onAdd() { setAttemptingTxn(true) const router = getRouterContract(chainId, library, account) @@ -387,6 +389,7 @@ export default function AddLiquidity({ token0, token1 }) { if (tokens[Field.INPUT] === WETH[chainId] || tokens[Field.OUTPUT] === WETH[chainId]) { method = router.addLiquidityETH estimate = router.estimate.addLiquidityETH + args = [ tokens[Field.OUTPUT] === WETH[chainId] ? tokens[Field.INPUT].address : tokens[Field.OUTPUT].address, // token tokens[Field.OUTPUT] === WETH[chainId] // token desired @@ -466,60 +469,48 @@ export default function AddLiquidity({ token0, token1 }) { return ( <> - - {tokens[Field.INPUT]?.symbol} Deposited - + {tokens[Field.INPUT]?.symbol} Deposited - - {!!parsedAmounts[Field.INPUT] && parsedAmounts[Field.INPUT].toSignificant(6)} - + {!!parsedAmounts[Field.INPUT] && parsedAmounts[Field.INPUT].toSignificant(6)} - - {tokens[Field.OUTPUT]?.symbol} Deposited - + {tokens[Field.OUTPUT]?.symbol} Deposited - - {!!parsedAmounts[Field.OUTPUT] && parsedAmounts[Field.OUTPUT].toSignificant(6)} - + {!!parsedAmounts[Field.OUTPUT] && parsedAmounts[Field.OUTPUT].toSignificant(6)} + {route && !JSBI.equal(route?.midPrice?.raw?.denominator, JSBI.BigInt(0)) && ( + + Rate + + {`1 ${tokens[Field.INPUT]?.symbol} = ${route?.midPrice && + route?.midPrice?.raw?.denominator && + route?.midPrice?.adjusted?.toFixed(8)} ${tokens[Field.OUTPUT]?.symbol}`} + + + )} - - Rate - - - {`1 ${tokens[Field.INPUT]?.symbol} = ${route?.midPrice && - route?.midPrice?.raw?.denominator && - route.midPrice.adjusted.toFixed(8)} ${tokens[Field.OUTPUT]?.symbol}`} - - - - - Minted Pool Share: - - - {poolTokenPercentage?.toFixed(6) + '%'} - + Minted Pool Share: + {noLiquidity ? '100%' : poolTokenPercentage?.toFixed(6) + '%'} Confirm Supply - + {`Output is estimated. You will receive at least ${liquidityMinted?.toFixed(6)} UNI ${ tokens[Field.INPUT]?.symbol }/${tokens[Field.OUTPUT]?.symbol} or the transaction will revert.`} - + ) } - const pendingText = `Supplied ${parsedAmounts[Field.INPUT]?.toSignificant(6)} ${ + const pendingText: string = `Supplied ${parsedAmounts[Field.INPUT]?.toSignificant(6)} ${ tokens[Field.INPUT]?.symbol } ${'and'} ${parsedAmounts[Field.OUTPUT]?.toSignificant(6)} ${tokens[Field.OUTPUT]?.symbol}` @@ -549,12 +540,12 @@ export default function AddLiquidity({ token0, token1 }) { {noLiquidity && ( - + 🥇 {' '} You are the first to add liquidity. Make sure you're setting rates correctly. - + )} - - Rate: -
- 1 {tokens[independentField].symbol} = {route?.midPrice?.toSignificant(6)} - {tokens[dependentField].symbol} -
-
+ {!noLiquidity && ( + + Rate: +
+ 1 {tokens[independentField].symbol} = {route?.midPrice?.toSignificant(6)} + {tokens[dependentField].symbol} +
+
+ )} { setShowConfirm(true) diff --git a/src/pages/Supply/CreateExchange.js b/src/pages/Supply/CreateExchange.js deleted file mode 100644 index efcc9f8d9c..0000000000 --- a/src/pages/Supply/CreateExchange.js +++ /dev/null @@ -1,157 +0,0 @@ -import React, { useState, useEffect } from 'react' -import { withRouter } from 'react-router' -import { createBrowserHistory } from 'history' -import { ethers } from 'ethers' -import styled from 'styled-components' -import { useTranslation } from 'react-i18next' -import ReactGA from 'react-ga' - -import { useWeb3React, useFactoryContract } from '../../hooks' -import { Button } from '../../theme' -import AddressInputPanel from '../../components/AddressInputPanel' -import OversizedPanel from '../../components/OversizedPanel' -import { useTokenDetails } from '../../contexts/Tokens' -import { useTransactionAdder } from '../../contexts/Transactions' - -const SummaryPanel = styled.div` - ${({ theme }) => theme.flexColumnNoWrap}; - padding: 1rem 0; -` - -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 CreateExchangeWrapper = styled.div` - color: ${({ theme }) => theme.doveGray}; - text-align: center; - margin-top: 1rem; - padding-top: 1rem; -` - -const SummaryText = styled.div` - font-size: 0.75rem; - color: ${({ error, theme }) => error && theme.salmonRed}; -` - -const Flex = styled.div` - display: flex; - justify-content: center; - padding: 2rem; - - button { - max-width: 20rem; - } -` - -function CreateExchange({ location, params }) { - const { t } = useTranslation() - const { account } = useWeb3React() - - const factory = useFactoryContract() - - const [tokenAddress, setTokenAddress] = useState({ - address: params.tokenAddress ? params.tokenAddress : '', - name: '' - }) - const [tokenAddressError, setTokenAddressError] = useState() - - const { name, symbol, decimals, exchangeAddress } = useTokenDetails(tokenAddress.address) - const addTransaction = useTransactionAdder() - - // clear url of query - useEffect(() => { - const history = createBrowserHistory() - history.push(window.location.pathname + '') - }, []) - - // validate everything - const [errorMessage, setErrorMessage] = useState(!account && t('noWallet')) - useEffect(() => { - if (tokenAddressError) { - setErrorMessage(t('invalidTokenAddress')) - } else if (symbol === undefined || decimals === undefined || exchangeAddress === undefined) { - setErrorMessage() - } else if (symbol === null) { - setErrorMessage(t('invalidSymbol')) - } else if (decimals === null) { - setErrorMessage(t('invalidDecimals')) - } else if (exchangeAddress !== ethers.constants.AddressZero) { - setErrorMessage(t('exchangeExists')) - } else if (!account) { - setErrorMessage(t('noWallet')) - } else { - setErrorMessage(null) - } - - return () => { - setErrorMessage() - } - }, [tokenAddress.address, symbol, decimals, exchangeAddress, account, t, tokenAddressError]) - - async function createExchange() { - const estimatedGasLimit = await factory.estimate.createExchange(tokenAddress.address) - - factory.createExchange(tokenAddress.address, { gasLimit: estimatedGasLimit }).then(response => { - ReactGA.event({ - category: 'Transaction', - action: 'Create Exchange' - }) - - addTransaction(response) - }) - } - - const isValid = errorMessage === null - - return ( - <> - - - - - {t('name')} - {name ? name : ' - '} - - - {t('symbol')} - {symbol ? symbol : ' - '} - - - {t('decimals')} - {decimals || decimals === 0 ? decimals : ' - '} - - - - - {errorMessage ? errorMessage : t('enterTokenCont')} - - - - - - ) -} - -export default withRouter(CreateExchange) diff --git a/src/pages/Supply/RemoveLiquidity.tsx b/src/pages/Supply/RemoveLiquidity.tsx index 39092c552d..4242a07634 100644 --- a/src/pages/Supply/RemoveLiquidity.tsx +++ b/src/pages/Supply/RemoveLiquidity.tsx @@ -2,7 +2,7 @@ import React, { useReducer, useState, useCallback, useEffect } from 'react' import styled from 'styled-components' import { ethers } from 'ethers' import { parseUnits } from '@ethersproject/units' -import { TokenAmount, JSBI, Route, WETH, Percent } from '@uniswap/sdk' +import { TokenAmount, JSBI, Route, WETH, Percent, Token, Exchange } from '@uniswap/sdk' import Slider from '../../components/Slider' import TokenLogo from '../../components/TokenLogo' @@ -21,8 +21,8 @@ import Row, { RowBetween, RowFixed } from '../../components/Row' import { useToken } from '../../contexts/Tokens' import { useWeb3React } from '../../hooks' import { useAllBalances } from '../../contexts/Balances' -import { useTransactionAdder } from '../../contexts/Transactions' import { useExchangeContract } from '../../hooks' +import { useTransactionAdder } from '../../contexts/Transactions' import { useExchange, useTotalSupply } from '../../contexts/Exchanges' import { BigNumber } from 'ethers/utils' @@ -145,34 +145,34 @@ const ConfirmedText = styled(Text)` export default function RemoveLiquidity({ token0, token1 }) { const { account, chainId, library } = useWeb3React() - const routerAddress = ROUTER_ADDRESSES[chainId] + const routerAddress: string = ROUTER_ADDRESSES[chainId] - const [showConfirm, setShowConfirm] = useState(false) - const [showAdvanced, setShowAdvanced] = useState(false) + const [showConfirm, setShowConfirm] = useState(false) + const [showAdvanced, setShowAdvanced] = useState(false) - const inputToken = useToken(token0) - const outputToken = useToken(token1) + const inputToken: Token = useToken(token0) + const outputToken: Token = useToken(token1) // get basic SDK entities - const tokens = { + const tokens: { [field: number]: Token } = { [Field.TOKEN0]: inputToken, [Field.TOKEN1]: outputToken } - const exchange = useExchange(inputToken, outputToken) - const exchangeContract = useExchangeContract(exchange?.liquidityToken.address) + const exchange: Exchange = useExchange(inputToken, outputToken) + const exchangeContract: ethers.Contract = useExchangeContract(exchange?.liquidityToken.address) // pool token data - const totalPoolTokens = useTotalSupply(exchange) + const totalPoolTokens: TokenAmount = useTotalSupply(exchange) - const allBalances = useAllBalances() - const userLiquidity = allBalances?.[account]?.[exchange?.liquidityToken?.address] + const allBalances: TokenAmount[] = useAllBalances() + const userLiquidity: TokenAmount = allBalances?.[account]?.[exchange?.liquidityToken?.address] // input state const [state, dispatch] = useReducer(reducer, initializeRemoveState(userLiquidity?.toExact(), token0, token1)) const { independentField, typedValue } = state - const TokensDeposited = { + const TokensDeposited: { [field: number]: TokenAmount } = { [Field.TOKEN0]: exchange && totalPoolTokens && @@ -185,7 +185,7 @@ export default function RemoveLiquidity({ token0, token1 }) { exchange.getLiquidityValue(tokens[Field.TOKEN1], totalPoolTokens, userLiquidity, false) } - const route = exchange + const route: Route = exchange ? new Route([exchange], independentField !== Field.LIQUIDITY ? tokens[independentField] : tokens[Field.TOKEN1]) : undefined @@ -307,11 +307,11 @@ export default function RemoveLiquidity({ token0, token1 }) { : false // errors - const [generalError, setGeneralError] = useState('') - const [inputError, setInputError] = useState('') - const [outputError, setOutputError] = useState('') - const [poolTokenError, setPoolTokenError] = useState('') - const [isValid, setIsValid] = useState(false) + const [generalError, setGeneralError] = useState('') + const [inputError, setInputError] = useState('') + const [outputError, setOutputError] = useState('') + const [poolTokenError, setPoolTokenError] = useState('') + const [isValid, setIsValid] = useState(false) // update errors live useEffect(() => { @@ -351,7 +351,7 @@ export default function RemoveLiquidity({ token0, token1 }) { const addTransaction = useTransactionAdder() const [txHash, setTxHash] = useState() const [sigInputs, setSigInputs] = useState([]) - const [deadline, setDeadline] = useState() + const [deadline, setDeadline] = useState(null) const [signed, setSigned] = useState(false) // waiting for signature sign const [attemptedRemoval, setAttemptedRemoval] = useState(false) // clicke confirm const [pendingConfirmation, setPendingConfirmation] = useState(true) // waiting for @@ -359,7 +359,7 @@ export default function RemoveLiquidity({ token0, token1 }) { async function onSign() { const nonce = await exchangeContract.nonces(account) - const newDeadline = Math.ceil(Date.now() / 1000) + DEADLINE_FROM_NOW + const newDeadline: number = Math.ceil(Date.now() / 1000) + DEADLINE_FROM_NOW setDeadline(newDeadline) const EIP712Domain = [ @@ -428,6 +428,7 @@ export default function RemoveLiquidity({ token0, token1 }) { : parsedAmounts[Field.TOKEN0].raw.toString(), account, deadline, + false, sigInputs[0], sigInputs[1], sigInputs[2] @@ -445,6 +446,7 @@ export default function RemoveLiquidity({ token0, token1 }) { parsedAmounts[Field.TOKEN1].raw.toString(), account, deadline, + false, sigInputs[0], sigInputs[1], sigInputs[2] @@ -466,18 +468,13 @@ export default function RemoveLiquidity({ token0, token1 }) { setTxHash(response.hash) addTransaction(response) }) - .catch(() => { + .catch(e => { + console.log(e) resetModalState() setShowConfirm(false) }) } - /** - * @todo - * if the input values stay the same, - * we should probably not reset the signature values, - * move to an effect - */ function resetModalState() { setSigned(false) setSigInputs(null) @@ -562,7 +559,7 @@ export default function RemoveLiquidity({ token0, token1 }) { ) } - const pendingText = `Removed ${parsedAmounts[Field.TOKEN0]?.toSignificant(6)} ${ + const pendingText: string = `Removed ${parsedAmounts[Field.TOKEN0]?.toSignificant(6)} ${ tokens[Field.TOKEN0]?.symbol } and ${parsedAmounts[Field.TOKEN1]?.toSignificant(6)} ${tokens[Field.TOKEN1]?.symbol}` diff --git a/src/theme/index.js b/src/theme/index.js index 13b5457ea4..d477433e39 100644 --- a/src/theme/index.js +++ b/src/theme/index.js @@ -72,7 +72,7 @@ const theme = darkMode => ({ tokenRowHover: darkMode ? '#404040' : '#F2F2F2', outlineGrey: darkMode ? '#292C2F' : '#EDEEF2', - darkGrey: darkMode ? '#888D9B' : '#888D9B', + darkGray: darkMode ? '#888D9B' : '#888D9B', //blacks charcoalBlack: darkMode ? '#F2F2F2' : '#404040', @@ -85,6 +85,7 @@ const theme = darkMode => ({ // purples wisteriaPurple: '#DC6BE5', + // reds salmonRed: '#FF6871', // orange @@ -93,6 +94,7 @@ const theme = darkMode => ({ warningYellow: '#FFE270', // pink uniswapPink: '#DC6BE5', + darkPink: '#ff007a', //green connectedGreen: '#27AE60', @@ -124,6 +126,16 @@ export const TYPE = { {children} ), + largeHeader: ({ children, ...rest }) => ( + + {children} + + ), + body: ({ children, ...rest }) => ( + + {children} + + ), blue: ({ children, ...rest }) => ( {children} @@ -134,6 +146,11 @@ export const TYPE = { {children} ), + darkGray: ({ children, ...rest }) => ( + + {children} + + ), italic: ({ children, ...rest }) => ( {children} diff --git a/src/utils/index.js b/src/utils/index.js index 1d07fea0f3..afbc2a26b1 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -5,7 +5,7 @@ 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 { FACTORY_ADDRESSES, SUPPORTED_THEMES, ROUTER_ADDRESSES } from '../constants' import { bigNumberify, keccak256, defaultAbiCoder, toUtf8Bytes, solidityPack } from 'ethers/utils' import UncheckedJsonRpcSigner from './signer' @@ -131,8 +131,8 @@ export function getContract(address, ABI, library, account) { } // account is optional -export function getRouterContract(networkId, library, account) { - const router = getContract('0xd9210Ff5A0780E083BB40e30d005d93a2DcFA4EF', ROUTER_ABI, library, account) +export function getRouterContract(chainId, library, account) { + const router = getContract(ROUTER_ADDRESSES[chainId], ROUTER_ABI, library, account) return router } diff --git a/yarn.lock b/yarn.lock index bdb1539d00..c5aeab6692 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1319,7 +1319,7 @@ "@ethersproject/logger" ">=5.0.0-beta.129" "@ethersproject/strings" ">=5.0.0-beta.130" -"@ethersproject/keccak256@>=5.0.0-beta.127", "@ethersproject/keccak256@^5.0.0-beta.130", "@ethersproject/keccak256@^5.0.0-beta.131": +"@ethersproject/keccak256@>=5.0.0-beta.127", "@ethersproject/keccak256@^5.0.0-beta.130": version "5.0.0-beta.131" resolved "https://registry.yarnpkg.com/@ethersproject/keccak256/-/keccak256-5.0.0-beta.131.tgz#b5778723ee75208065b9b9ad30c71d480f41bb31" integrity sha512-KQnqMwGV0IMOjAr/UTFO8DuLrmN1uaMvcV3zh9hiXhh3rCuY+WXdeUh49w1VQ94kBKmaP0qfGb7z4SdhUWUHjw== @@ -1382,6 +1382,15 @@ dependencies: "@ethersproject/bytes" ">=5.0.0-beta.129" +"@ethersproject/sha2@>=5.0.0-beta.129": + version "5.0.0-beta.135" + resolved "https://registry.yarnpkg.com/@ethersproject/sha2/-/sha2-5.0.0-beta.135.tgz#e597572ba991fe044d50f8d75704bb4a2b2c64b4" + integrity sha512-DK/cUT5ilCVLtf1xk7XDPB9xGHsXiU3TsULKsEg823cTBIhFl2l0IiHAGqu9uiMlSJRpb2BwrWQuMgmFe/vMwQ== + dependencies: + "@ethersproject/bytes" ">=5.0.0-beta.129" + "@ethersproject/logger" ">=5.0.0-beta.129" + hash.js "1.1.3" + "@ethersproject/signing-key@>=5.0.0-beta.129": version "5.0.0-beta.135" resolved "https://registry.yarnpkg.com/@ethersproject/signing-key/-/signing-key-5.0.0-beta.135.tgz#f739e800aad9e01b77a8ec2c353b9b66ce5738fa" @@ -1392,6 +1401,17 @@ "@ethersproject/properties" ">=5.0.0-beta.131" elliptic "6.5.2" +"@ethersproject/solidity@^5.0.0-beta.131": + version "5.0.0-beta.131" + resolved "https://registry.yarnpkg.com/@ethersproject/solidity/-/solidity-5.0.0-beta.131.tgz#7d826e98cc0a29e25f0ff52ae17c07483f5d93d4" + integrity sha512-i5vuj2CXGMkVPo08bmElC2cvhjRDNRZZ8nzvx2WCi75Zh42xD0XNV77E9ZLYgS0WoZSiAi/F71nXSBnM7FAqJg== + dependencies: + "@ethersproject/bignumber" ">=5.0.0-beta.130" + "@ethersproject/bytes" ">=5.0.0-beta.129" + "@ethersproject/keccak256" ">=5.0.0-beta.127" + "@ethersproject/sha2" ">=5.0.0-beta.129" + "@ethersproject/strings" ">=5.0.0-beta.130" + "@ethersproject/strings@>=5.0.0-beta.130": version "5.0.0-beta.136" resolved "https://registry.yarnpkg.com/@ethersproject/strings/-/strings-5.0.0-beta.136.tgz#053cbf4f9f96a7537cbc50300597f2d707907f51" @@ -2365,16 +2385,16 @@ semver "^6.3.0" tsutils "^3.17.1" -"@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== +"@uniswap/sdk@@uniswap/sdk@2.0.0-beta.19": + version "2.0.0-beta.19" + resolved "https://registry.yarnpkg.com/@uniswap/sdk/-/sdk-2.0.0-beta.19.tgz#1f0228a1d5451d62f209e09c48cd1d6bea5ffe01" + integrity sha512-mqDZkeX2TU7e3yOOKHSeUryv94//mJ6dsU6dCv6FExrfOY9yi6+O5ZWpe43rVi0XFVdQGRIRhJapdWKWaGsung== dependencies: "@ethersproject/address" "^5.0.0-beta.134" "@ethersproject/contracts" "^5.0.0-beta.143" - "@ethersproject/keccak256" "^5.0.0-beta.131" "@ethersproject/networks" "^5.0.0-beta.135" "@ethersproject/providers" "^5.0.0-beta.153" + "@ethersproject/solidity" "^5.0.0-beta.131" big.js "^5.2.2" decimal.js-light "^2.5.0" jsbi "^3.1.1"