From 6be569a2c6520fe7e51d3c552bdee808c64e1c92 Mon Sep 17 00:00:00 2001 From: ian-jh Date: Wed, 17 Jul 2019 10:10:55 -0400 Subject: [PATCH 01/12] fix i18n bug --- .env.local.example | 2 -- src/components/CurrencyInputPanel/index.js | 2 +- src/i18n.js | 7 +++---- src/index.js | 7 +++++-- src/pages/App.js | 1 + 5 files changed, 10 insertions(+), 9 deletions(-) delete mode 100644 .env.local.example diff --git a/.env.local.example b/.env.local.example deleted file mode 100644 index f2103acece..0000000000 --- a/.env.local.example +++ /dev/null @@ -1,2 +0,0 @@ -REACT_APP_NETWORK_ID="1" -REACT_APP_NETWORK_URL="" diff --git a/src/components/CurrencyInputPanel/index.js b/src/components/CurrencyInputPanel/index.js index b3147869a5..4bad35d3d1 100644 --- a/src/components/CurrencyInputPanel/index.js +++ b/src/components/CurrencyInputPanel/index.js @@ -207,7 +207,7 @@ export default function CurrencyInputPanel({ showUnlock, value }) { - const { t } = useTranslation() + const { t } = useTranslation(); const [modalIsOpen, setModalIsOpen] = useState(false) diff --git a/src/i18n.js b/src/i18n.js index 5166f9967d..51b5b83a4f 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -9,12 +9,11 @@ i18next .use(initReactI18next) .init({ backend: { - loadPath: '/locales/{{lng}}.json' + loadPath: '/locales/{{lng}}.json', }, react: { - useSuspense: true - }, - lng: 'en', + useSuspense: true, + }, fallbackLng: 'en', preload: ['en'], keySeparator: false, diff --git a/src/index.js b/src/index.js index 04b4a122a2..552e86affe 100644 --- a/src/index.js +++ b/src/index.js @@ -10,6 +10,7 @@ import TokensContextProvider from './contexts/Tokens' import BalancesContextProvider from './contexts/Balances' import AllowancesContextProvider from './contexts/Allowances' + import App from './pages/App' import InjectedConnector from './InjectedConnector' @@ -33,7 +34,9 @@ function ContextProviders({ children }) { - {children} + + {children} + @@ -57,7 +60,7 @@ ReactDOM.render( - + diff --git a/src/pages/App.js b/src/pages/App.js index bc4ba97e1a..f81d4c7d61 100644 --- a/src/pages/App.js +++ b/src/pages/App.js @@ -83,6 +83,7 @@ export default function App() { /> + From 0bb6bdcd379457ce03854f2b318969f952623e25 Mon Sep 17 00:00:00 2001 From: ian-jh Date: Wed, 17 Jul 2019 10:18:17 -0400 Subject: [PATCH 02/12] fix styles --- src/components/CurrencyInputPanel/index.js | 2 +- src/i18n.js | 4 ++-- src/index.js | 7 ++----- src/pages/App.js | 1 - 4 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/components/CurrencyInputPanel/index.js b/src/components/CurrencyInputPanel/index.js index 4bad35d3d1..b3147869a5 100644 --- a/src/components/CurrencyInputPanel/index.js +++ b/src/components/CurrencyInputPanel/index.js @@ -207,7 +207,7 @@ export default function CurrencyInputPanel({ showUnlock, value }) { - const { t } = useTranslation(); + const { t } = useTranslation() const [modalIsOpen, setModalIsOpen] = useState(false) diff --git a/src/i18n.js b/src/i18n.js index 51b5b83a4f..4ab8376030 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -9,10 +9,10 @@ i18next .use(initReactI18next) .init({ backend: { - loadPath: '/locales/{{lng}}.json', + loadPath: '/locales/{{lng}}.json' }, react: { - useSuspense: true, + useSuspense: true }, fallbackLng: 'en', preload: ['en'], diff --git a/src/index.js b/src/index.js index 552e86affe..04b4a122a2 100644 --- a/src/index.js +++ b/src/index.js @@ -10,7 +10,6 @@ import TokensContextProvider from './contexts/Tokens' import BalancesContextProvider from './contexts/Balances' import AllowancesContextProvider from './contexts/Allowances' - import App from './pages/App' import InjectedConnector from './InjectedConnector' @@ -34,9 +33,7 @@ function ContextProviders({ children }) { - - {children} - + {children} @@ -60,7 +57,7 @@ ReactDOM.render( - + diff --git a/src/pages/App.js b/src/pages/App.js index f81d4c7d61..bc4ba97e1a 100644 --- a/src/pages/App.js +++ b/src/pages/App.js @@ -83,7 +83,6 @@ export default function App() { /> - From 2a6b1e63c477acf7059edf13f5085b1f0fd3dfd6 Mon Sep 17 00:00:00 2001 From: ian-jh Date: Mon, 22 Jul 2019 15:00:45 -0400 Subject: [PATCH 03/12] add initial changes --- .env.local.example | 2 + public/locales/en.json | 3 +- src/assets/images/question.svg | 4 + src/components/ContextualInfoNew/index.js | 6 +- src/components/Modal/index.js | 2 +- src/components/TransactionDetails/index.js | 357 +++++++++++++++++++++ src/i18n.js | 2 +- src/pages/Swap/index.js | 159 +++------ 8 files changed, 410 insertions(+), 125 deletions(-) create mode 100644 .env.local.example create mode 100644 src/assets/images/question.svg create mode 100644 src/components/TransactionDetails/index.js diff --git a/.env.local.example b/.env.local.example new file mode 100644 index 0000000000..d623d46c40 --- /dev/null +++ b/.env.local.example @@ -0,0 +1,2 @@ +REACT_APP_NETWORK_ID="1" +REACT_APP_NETWORK_URL="" \ No newline at end of file diff --git a/public/locales/en.json b/public/locales/en.json index 81d36251ee..61de6b81d2 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -80,5 +80,6 @@ "symbol": "Symbol", "decimals": "Decimals", "enterTokenCont": "Enter a token address to continue", - "priceChange": "This trade will cause the price to change by" + "priceChange": "Expected price slippage", + "forAtLeast" : "for at least " } diff --git a/src/assets/images/question.svg b/src/assets/images/question.svg new file mode 100644 index 0000000000..ae879eddad --- /dev/null +++ b/src/assets/images/question.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/components/ContextualInfoNew/index.js b/src/components/ContextualInfoNew/index.js index 75888f0cd4..b22cfb491b 100644 --- a/src/components/ContextualInfoNew/index.js +++ b/src/components/ContextualInfoNew/index.js @@ -92,10 +92,10 @@ export default function ContextualInfo({ renderTransactionDetails = () => {}, isError = false, slippageWarning, - highSlippageWarning + highSlippageWarning, + dropDownContent }) { const [showDetails, setShowDetails] = useState(false) - return !allowExpand ? ( {contextualInfo} ) : ( @@ -117,7 +117,7 @@ export default function ContextualInfo({ )} - {showDetails &&
{renderTransactionDetails()}
} + {showDetails &&
{dropDownContent()}
} ) } diff --git a/src/components/Modal/index.js b/src/components/Modal/index.js index dd392fe912..73f5a393f1 100644 --- a/src/components/Modal/index.js +++ b/src/components/Modal/index.js @@ -6,7 +6,7 @@ import '@reach/dialog/styles.css' const AnimatedDialogOverlay = animated(DialogOverlay) const StyledDialogOverlay = styled(AnimatedDialogOverlay).attrs({ - suppressClassNameWarning: true + suppressclassnamewarning: 'true' })` &[data-reach-dialog-overlay] { z-index: 2; diff --git a/src/components/TransactionDetails/index.js b/src/components/TransactionDetails/index.js new file mode 100644 index 0000000000..d867c5ce73 --- /dev/null +++ b/src/components/TransactionDetails/index.js @@ -0,0 +1,357 @@ +import React, { useState } from 'react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' +import { amountFormatter } from '../../utils' + +import NewContextualInfo from '../../components/ContextualInfoNew' + +const Flex = styled.div` + display: flex; + justify-content: center; + padding: 14px 0; + button { + max-width: 20rem; + } +` + +const SlippageRow = styled(Flex)` + flex-direction: row; + width: 100%; + justify-content: flex-start; + font-size: 0.8rem; + padding: 0; + height: 24px; + margin-bottom: 14px; +` + +const Option = styled(Flex)` + align-items: center; + min-width: 55px; + margin-right: 4px; + border-radius: 36px; + border: 1px solid #f2f2f2; + + ${({ active }) => + active && + ` + background-color: #2f80ed; + color: white; + border: 1px solid #2f80ed; + `} + + &:hover { + cursor: pointer; + } +` + +const Input = styled.input` + width: 123.27px; + background: #ffffff; + height: 2rem; + outline: none; + margin-left: 20px; + border: 1px solid #f2f2f2; + box-sizing: border-box; + border-radius: 36px; + color: #aeaeae; + + &:focus { + } + + text-align: left; + padding-left: 0.9rem; + + &::-webkit-outer-spin-button, + &::-webkit-inner-spin-button { + -webkit-appearance: none; + } + + ${({ active }) => + active && + ` + border: 1px solid #2f80ed; + text-align: right; + padding-right 1.5rem; + padding-left 0rem; + color : inherit; + `} + + ${({ warning }) => + warning && + ` + color : #FF6871; + border: 1px solid #FF6871; + `} +` + +const BottomError = styled.div` + margin-top: 1rem; + color: #aeaeae; + + ${({ warning }) => + warning && + ` + color : #FF6871; +`} +` + +const OptionLarge = styled(Option)` + width: 120px; +` + +const Bold = styled.span` + font-weight: 500; +` + +const LastSummaryText = styled.div` + margin-top: 0.6rem; +` + +const SlippageSelector = styled.div` + margin-top: 28px; +` + +const InputGroup = styled.div` + position: relative; +` + +const Percent = styled.div` + right: 14px; + top: 8px; + position: absolute; + color: inherit; + font-size: 0, 8rem; + + ${({ color }) => + (color === 'faded' && + ` + color : #AEAEAE + `) || + (color === 'red' && + ` + color : #FF6871 + `)} +` + +const Faded = styled.span` + opacity: 0.7; +` + +const ErrorEmoji = styled.span` + left: 30px; + top: 4px; + position: absolute; +` + +export default function TransactionDetails(props) { + const { t } = useTranslation() + + function renderSummary() { + let contextualInfo = '' + let isError = false + + 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.account) { + contextualInfo = t('noWallet') + isError = true + } + + const slippageWarningText = props.highSlippageWarning + ? t('highSlippageWarning') + : props.slippageWarning + ? t('slippageWarning') + : '' + + return ( + + ) + } + + const [activeIndex, setActiveIndex] = useState(3) + + const [placeHolder, setplaceHolder] = useState('Custom') + + const [warningType, setWarningType] = useState('none') + + const dropDownContent = () => { + return ( + <> + {renderTransactionDetails()} + + Limit addtional price slippage + + + + { + checkAcceptablePercentValue(2) + setActiveIndex(3) + setplaceHolder('Custom') + }} + active={activeIndex === 3 ? true : false} + > + 2% + (suggested) + + + {warningType !== 'none' ? ⚠️ : ''} + { + setActiveIndex(4) + setplaceHolder('') + parseInput(e) + }} + active={activeIndex === 4 ? true : false} + warning={warningType !== 'none'} + /> + % + + + + + {warningType === 'invalidEntry' ? 'Please input a valid percentage' : ''} + {warningType === 'invalidEntryBound' ? 'Only choose between 0% and 50%' : ''} + {warningType === 'riskyEntry' ? 'Youre at risk of being front-run ' : ''} + + + + + ) + } + + const [userInput, setUserInput] = useState() + + const parseInput = e => { + let input = e.target.value + //check for decimal + var isValid = /^[+]?\d*\.?\d{1,2}$/.test(input) || /^[+]?\d*\.$/.test(input) + var decimalLimit = /^\d+\.?\d{0,2}$/.test(input) || input === '' + if (decimalLimit) { + setUserInput(input) + } else { + return + } + if (isValid) { + checkAcceptablePercentValue(input) + } else { + setWarningType('invalidEntry') + } + } + + const checkAcceptablePercentValue = input => { + setWarningType('none') + if (input < 0 || input > 50) { + return setWarningType('invalidEntryBound') + } + if (input >= 0 && input < 0.1) { + setWarningType('riskyEntry') + } + let num = parseFloat((input * 100).toFixed(2)) + props.setRawSlippage(num) + props.setRawTokenSlippage(num) + } + + const b = text => {text} + + const renderTransactionDetails = () => { + if (props.independentField === props.INPUT) { + return ( +
+
+ {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 ( +
+
+ {t('youAreBuying')}{' '} + {b( + `${amountFormatter( + props.independentValueParsed, + props.independentDecimals, + Math.min(4, props.independentDecimals) + )} ${props.outputSymbol}` + )} + . +
+ + {t('itWillCost')}{' '} + {b( + `${amountFormatter( + props.dependentValueMaximum, + props.dependentDecimals, + Math.min(4, props.dependentDecimals) + )} ${props.inputSymbol}` + )}{' '} + {t('orTransFail')} + + + {t('priceChange')} {b(`${props.percentSlippageFormatted}%`)}. + +
+ ) + } + } + + return <>{renderSummary()} +} diff --git a/src/i18n.js b/src/i18n.js index 4ab8376030..95f0636424 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -13,7 +13,7 @@ i18next }, react: { useSuspense: true - }, + }, fallbackLng: 'en', preload: ['en'], keySeparator: false, diff --git a/src/pages/Swap/index.js b/src/pages/Swap/index.js index da34645ebf..1ef5fd72af 100644 --- a/src/pages/Swap/index.js +++ b/src/pages/Swap/index.js @@ -9,6 +9,7 @@ import { Button } from '../../theme' import CurrencyInputPanel from '../../components/CurrencyInputPanel' import NewContextualInfo from '../../components/ContextualInfoNew' import OversizedPanel from '../../components/OversizedPanel' +import TransactionDetails from '../../components/TransactionDetails' import ArrowDownBlue from '../../assets/images/arrow-down-blue.svg' import ArrowDownGrey from '../../assets/images/arrow-down-grey.svg' import { amountFormatter, calculateGasMargin } from '../../utils' @@ -26,8 +27,8 @@ const TOKEN_TO_ETH = 1 const TOKEN_TO_TOKEN = 2 // denominated in bips -const ALLOWED_SLIPPAGE = ethers.utils.bigNumberify(200) -const TOKEN_ALLOWED_SLIPPAGE = ethers.utils.bigNumberify(400) +const ALLOWED_SLIPPAGE_DEFAULT = 150 +const TOKEN_ALLOWED_SLIPPAGE_DEFAULT = 200 // denominated in seconds const DEADLINE_FROM_NOW = 60 * 15 @@ -81,9 +82,9 @@ const Flex = styled.div` } ` -function calculateSlippageBounds(value, token = false) { +function calculateSlippageBounds(value, token = false, tokenAllowedSlippage, allowedSlippage) { if (value) { - const offset = value.mul(token ? TOKEN_ALLOWED_SLIPPAGE : ALLOWED_SLIPPAGE).div(ethers.utils.bigNumberify(10000)) + const offset = value.mul(token ? tokenAllowedSlippage : allowedSlippage).div(ethers.utils.bigNumberify(10000)) const minimum = value.sub(offset) const maximum = value.add(offset) return { @@ -244,12 +245,18 @@ export default function Swap({ initialCurrency }) { const addTransaction = useTransactionAdder() + const [rawSlippage, setRawSlippage] = useState(ALLOWED_SLIPPAGE_DEFAULT) + const [rawTokenSlippage, setRawTokenSlippage] = useState(TOKEN_ALLOWED_SLIPPAGE_DEFAULT) + + let allowedSlippageBig = ethers.utils.bigNumberify(rawSlippage) + let tokenAllowedSlippageBig = ethers.utils.bigNumberify(rawTokenSlippage) + // analytics useEffect(() => { ReactGA.pageview(window.location.pathname + window.location.search) }, []) - // core swap state + // core swap state- const [swapState, dispatchSwapState] = useReducer(swapStateReducer, initialCurrency, getInitialSwapState) const { independentValue, dependentValue, independentField, inputCurrency, outputCurrency } = swapState @@ -326,7 +333,9 @@ export default function Swap({ initialCurrency }) { // calculate slippage from target rate const { minimum: dependentValueMinumum, maximum: dependentValueMaximum } = calculateSlippageBounds( dependentValue, - swapType === TOKEN_TO_TOKEN + swapType === TOKEN_TO_TOKEN, + tokenAllowedSlippageBig, + allowedSlippageBig ) // validate input allowance + balance @@ -496,118 +505,6 @@ export default function Swap({ initialCurrency }) { return `Balance: ${value}` } - function renderTransactionDetails() { - ReactGA.event({ - category: 'TransactionDetail', - action: 'Open' - }) - - const b = text => {text} - - if (independentField === INPUT) { - return ( -
-
- {t('youAreSelling')}{' '} - {b( - `${amountFormatter( - independentValueParsed, - independentDecimals, - Math.min(4, independentDecimals) - )} ${inputSymbol}` - )} - . -
- - {t('youWillReceive')}{' '} - {b( - `${amountFormatter( - dependentValueMinumum, - dependentDecimals, - Math.min(4, dependentDecimals) - )} ${outputSymbol}` - )}{' '} - {t('orTransFail')} - - - {(slippageWarning || highSlippageWarning) && ( - - ⚠️ - - )} - {t('priceChange')} {b(`${percentSlippageFormatted}%`)}. - -
- ) - } else { - return ( -
-
- {t('youAreBuying')}{' '} - {b( - `${amountFormatter( - independentValueParsed, - independentDecimals, - Math.min(4, independentDecimals) - )} ${outputSymbol}` - )} - . -
- - {t('itWillCost')}{' '} - {b( - `${amountFormatter( - dependentValueMaximum, - dependentDecimals, - Math.min(4, dependentDecimals) - )} ${inputSymbol}` - )}{' '} - {t('orTransFail')} - - - {t('priceChange')} {b(`${percentSlippageFormatted}%`)}. - -
- ) - } - } - - function renderSummary() { - let contextualInfo = '' - let isError = false - - if (inputError || independentError) { - contextualInfo = inputError || independentError - isError = true - } else if (!inputCurrency || !outputCurrency) { - contextualInfo = t('selectTokenCont') - } else if (!independentValue) { - contextualInfo = t('enterValueCont') - } else if (!account) { - contextualInfo = t('noWallet') - isError = true - } - - const slippageWarningText = highSlippageWarning - ? t('highSlippageWarning') - : slippageWarning - ? t('slippageWarning') - : '' - - return ( - - ) - } - async function onSwap() { const deadline = Math.ceil(Date.now() / 1000) + DEADLINE_FROM_NOW @@ -743,7 +640,31 @@ export default function Swap({ initialCurrency }) { )} - {renderSummary()} + From 1dcec9be3876ebf76d2ddcee7b167196a239aced Mon Sep 17 00:00:00 2001 From: ian-jh Date: Wed, 24 Jul 2019 15:00:03 -0400 Subject: [PATCH 07/12] fix button state bug --- src/components/TransactionDetails/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/TransactionDetails/index.js b/src/components/TransactionDetails/index.js index 72bb4e1f92..972d06634e 100644 --- a/src/components/TransactionDetails/index.js +++ b/src/components/TransactionDetails/index.js @@ -379,6 +379,7 @@ export default function TransactionDetails(props) { const checkAcceptablePercentValue = input => { setTimeout(function() { setWarningType('none') + props.setcustomSlippageError('valid') if (input < 0 || input > 50) { props.setcustomSlippageError('invalid') return setWarningType('invalidEntryBound') From 2fef97d0f32d82dd3b66af040b64fe5bd8c84870 Mon Sep 17 00:00:00 2001 From: ian-jh Date: Wed, 24 Jul 2019 16:33:28 -0400 Subject: [PATCH 08/12] setup initital component merging --- src/components/ExchangePage/ExchangePage.jsx | 6 ++++++ src/components/TransactionDetails/index.js | 1 - src/pages/App.js | 2 +- src/pages/Swap/index.js | 22 ++++++++++++++++++-- 4 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 src/components/ExchangePage/ExchangePage.jsx diff --git a/src/components/ExchangePage/ExchangePage.jsx b/src/components/ExchangePage/ExchangePage.jsx new file mode 100644 index 0000000000..78d2696346 --- /dev/null +++ b/src/components/ExchangePage/ExchangePage.jsx @@ -0,0 +1,6 @@ +import React from 'react' + +export default function ExchangePage({ sending }) { + + +} \ No newline at end of file diff --git a/src/components/TransactionDetails/index.js b/src/components/TransactionDetails/index.js index 972d06634e..367fb64238 100644 --- a/src/components/TransactionDetails/index.js +++ b/src/components/TransactionDetails/index.js @@ -389,7 +389,6 @@ export default function TransactionDetails(props) { setWarningType('riskyEntryLow') } if (input >= 5) { - console.log('doing it') props.setcustomSlippageError('warning') setWarningType('riskyEntryHigh') } diff --git a/src/pages/App.js b/src/pages/App.js index bc4ba97e1a..96f3d0bbfe 100644 --- a/src/pages/App.js +++ b/src/pages/App.js @@ -46,7 +46,7 @@ export default function App() { {/* this Suspense is for route code-splitting */} - + } /> + {sending ? ( + <> + + + + + + + + ) : ( + '' + )} { From e19e150f01077a8af26ed0b67928c2d0f98076f9 Mon Sep 17 00:00:00 2001 From: ian-jh Date: Mon, 29 Jul 2019 15:35:28 -0400 Subject: [PATCH 09/12] combine swap and send pages into one component --- src/components/ExchangePage/ExchangePage.jsx | 6 - src/components/ExchangePage/index.jsx | 722 +++++++++++++++++ src/components/TransactionDetails/index.js | 79 +- src/pages/App.js | 4 +- src/pages/Send/index.js | 785 +------------------ src/pages/Swap/index.js | 702 +---------------- 6 files changed, 806 insertions(+), 1492 deletions(-) delete mode 100644 src/components/ExchangePage/ExchangePage.jsx create mode 100644 src/components/ExchangePage/index.jsx diff --git a/src/components/ExchangePage/ExchangePage.jsx b/src/components/ExchangePage/ExchangePage.jsx deleted file mode 100644 index 78d2696346..0000000000 --- a/src/components/ExchangePage/ExchangePage.jsx +++ /dev/null @@ -1,6 +0,0 @@ -import React from 'react' - -export default function ExchangePage({ sending }) { - - -} \ No newline at end of file diff --git a/src/components/ExchangePage/index.jsx b/src/components/ExchangePage/index.jsx new file mode 100644 index 0000000000..19291e1af1 --- /dev/null +++ b/src/components/ExchangePage/index.jsx @@ -0,0 +1,722 @@ +import React, { useState, useReducer, useEffect } from 'react' +import ReactGA from 'react-ga' +import { useTranslation } from 'react-i18next' +import { useWeb3Context } from 'web3-react' +import { ethers } from 'ethers' +import styled from 'styled-components' + +import { Button } from '../../theme' +import CurrencyInputPanel from '../CurrencyInputPanel' +import AddressInputPanel from '../AddressInputPanel' +import OversizedPanel from '../OversizedPanel' +import TransactionDetails from '../TransactionDetails' +import ArrowDownBlue from '../../assets/images/arrow-down-blue.svg' +import ArrowDownGrey from '../../assets/images/arrow-down-grey.svg' +import { amountFormatter, calculateGasMargin } from '../../utils' +import { useExchangeContract } from '../../hooks' +import { useTokenDetails } from '../../contexts/Tokens' +import { useTransactionAdder } from '../../contexts/Transactions' +import { useAddressBalance, useExchangeReserves } from '../../contexts/Balances' +import { useAddressAllowance } from '../../contexts/Allowances' + +const INPUT = 0 +const OUTPUT = 1 + +const ETH_TO_TOKEN = 0 +const TOKEN_TO_ETH = 1 +const TOKEN_TO_TOKEN = 2 + +// denominated in bips +const ALLOWED_SLIPPAGE_DEFAULT = 150 +const TOKEN_ALLOWED_SLIPPAGE_DEFAULT = 200 + +// denominated in seconds +const DEADLINE_FROM_NOW = 60 * 15 + +// denominated in bips +const GAS_MARGIN = ethers.utils.bigNumberify(1000) + + +const DownArrowBackground = styled.div` + ${({ theme }) => theme.flexRowNoWrap} + justify-content: center; + align-items: center; +` + +const DownArrow = styled.img` + width: 0.625rem; + height: 0.625rem; + position: relative; + padding: 0.875rem; + cursor: ${({ clickable }) => clickable && 'pointer'}; +` + +const ExchangeRateWrapper = styled.div` + ${({ theme }) => theme.flexRowNoWrap}; + align-items: center; + color: ${({ theme }) => theme.doveGray}; + font-size: 0.75rem; + padding: 0.5rem 1rem; +` + +const ExchangeRate = styled.span` + flex: 1 1 auto; + width: 0; + color: ${({ theme }) => theme.chaliceGray}; +` + +const Flex = styled.div` + display: flex; + justify-content: center; + padding: 2rem; + + button { + max-width: 20rem; + } +` + +function calculateSlippageBounds(value, token = false, tokenAllowedSlippage, allowedSlippage) { + if (value) { + const offset = value.mul(token ? tokenAllowedSlippage : allowedSlippage).div(ethers.utils.bigNumberify(10000)) + const minimum = value.sub(offset) + const maximum = value.add(offset) + return { + minimum: minimum.lt(ethers.constants.Zero) ? ethers.constants.Zero : minimum, + maximum: maximum.gt(ethers.constants.MaxUint256) ? ethers.constants.MaxUint256 : maximum + } + } else { + return {} + } +} + +function getSwapType(inputCurrency, outputCurrency) { + if (!inputCurrency || !outputCurrency) { + return null + } else if (inputCurrency === 'ETH') { + return ETH_TO_TOKEN + } else if (outputCurrency === 'ETH') { + return TOKEN_TO_ETH + } else { + return TOKEN_TO_TOKEN + } +} + +// this mocks the getInputPrice function, and calculates the required output +function calculateEtherTokenOutputFromInput(inputAmount, inputReserve, outputReserve) { + const inputAmountWithFee = inputAmount.mul(ethers.utils.bigNumberify(997)) + const numerator = inputAmountWithFee.mul(outputReserve) + const denominator = inputReserve.mul(ethers.utils.bigNumberify(1000)).add(inputAmountWithFee) + return numerator.div(denominator) +} + +// this mocks the getOutputPrice function, and calculates the required input +function calculateEtherTokenInputFromOutput(outputAmount, inputReserve, outputReserve) { + const numerator = inputReserve.mul(outputAmount).mul(ethers.utils.bigNumberify(1000)) + const denominator = outputReserve.sub(outputAmount).mul(ethers.utils.bigNumberify(997)) + return numerator.div(denominator).add(ethers.constants.One) +} + +function getInitialSwapState(outputCurrency) { + return { + independentValue: '', // this is a user input + dependentValue: '', // this is a calculated number + independentField: INPUT, + inputCurrency: 'ETH', + outputCurrency: outputCurrency ? outputCurrency : '' + } +} + +function swapStateReducer(state, action) { + switch (action.type) { + case 'FLIP_INDEPENDENT': { + const { independentField, inputCurrency, outputCurrency } = state + return { + ...state, + dependentValue: '', + independentField: independentField === INPUT ? OUTPUT : INPUT, + inputCurrency: outputCurrency, + outputCurrency: inputCurrency + } + } + case 'SELECT_CURRENCY': { + const { inputCurrency, outputCurrency } = state + const { field, currency } = action.payload + + const newInputCurrency = field === INPUT ? currency : inputCurrency + const newOutputCurrency = field === OUTPUT ? currency : outputCurrency + + if (newInputCurrency === newOutputCurrency) { + return { + ...state, + inputCurrency: field === INPUT ? currency : '', + outputCurrency: field === OUTPUT ? currency : '' + } + } else { + return { + ...state, + inputCurrency: newInputCurrency, + outputCurrency: newOutputCurrency + } + } + } + case 'UPDATE_INDEPENDENT': { + const { field, value } = action.payload + const { dependentValue, independentValue } = state + return { + ...state, + independentValue: value, + dependentValue: value === independentValue ? dependentValue : '', + independentField: field + } + } + case 'UPDATE_DEPENDENT': { + return { + ...state, + dependentValue: action.payload + } + } + default: { + return getInitialSwapState() + } + } +} + +function getExchangeRate(inputValue, inputDecimals, outputValue, outputDecimals, invert = false) { + try { + if ( + inputValue && + (inputDecimals || inputDecimals === 0) && + outputValue && + (outputDecimals || outputDecimals === 0) + ) { + const factor = ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(18)) + + if (invert) { + return inputValue + .mul(factor) + .div(outputValue) + .mul(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(outputDecimals))) + .div(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(inputDecimals))) + } else { + return outputValue + .mul(factor) + .div(inputValue) + .mul(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(inputDecimals))) + .div(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(outputDecimals))) + } + } + } catch {} +} + +function getMarketRate( + swapType, + inputReserveETH, + inputReserveToken, + inputDecimals, + outputReserveETH, + outputReserveToken, + outputDecimals, + invert = false +) { + if (swapType === ETH_TO_TOKEN) { + return getExchangeRate(outputReserveETH, 18, outputReserveToken, outputDecimals, invert) + } else if (swapType === TOKEN_TO_ETH) { + return getExchangeRate(inputReserveToken, inputDecimals, inputReserveETH, 18, invert) + } else if (swapType === TOKEN_TO_TOKEN) { + const factor = ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(18)) + const firstRate = getExchangeRate(inputReserveToken, inputDecimals, inputReserveETH, 18) + const secondRate = getExchangeRate(outputReserveETH, 18, outputReserveToken, outputDecimals) + try { + return !!(firstRate && secondRate) ? firstRate.mul(secondRate).div(factor) : undefined + } catch {} + } +} + +export default function ExchangePage({ initialCurrency, sending }) { + const { t } = useTranslation() + const { account } = useWeb3Context() + + const addTransaction = useTransactionAdder() + + const [rawSlippage, setRawSlippage] = useState(ALLOWED_SLIPPAGE_DEFAULT) + const [rawTokenSlippage, setRawTokenSlippage] = useState(TOKEN_ALLOWED_SLIPPAGE_DEFAULT) + + let allowedSlippageBig = ethers.utils.bigNumberify(rawSlippage) + let tokenAllowedSlippageBig = ethers.utils.bigNumberify(rawTokenSlippage) + + // analytics + useEffect(() => { + ReactGA.pageview(window.location.pathname + window.location.search) + }, []) + + // core swap state- + const [swapState, dispatchSwapState] = useReducer(swapStateReducer, initialCurrency, getInitialSwapState) + const { independentValue, dependentValue, independentField, inputCurrency, outputCurrency } = swapState + + const [recipient, setRecipient] = useState({ address: '', name: '' }) + const [recipientError, setRecipientError] = useState() + + // get swap type from the currency types + const swapType = getSwapType(inputCurrency, outputCurrency) + + // get decimals and exchange addressfor each of the currency types + const { symbol: inputSymbol, decimals: inputDecimals, exchangeAddress: inputExchangeAddress } = useTokenDetails( + inputCurrency + ) + const { symbol: outputSymbol, decimals: outputDecimals, exchangeAddress: outputExchangeAddress } = useTokenDetails( + outputCurrency + ) + + const inputExchangeContract = useExchangeContract(inputExchangeAddress) + const outputExchangeContract = useExchangeContract(outputExchangeAddress) + const contract = swapType === ETH_TO_TOKEN ? outputExchangeContract : inputExchangeContract + + // get input allowance + const inputAllowance = useAddressAllowance(account, inputCurrency, inputExchangeAddress) + + // fetch reserves for each of the currency types + const { reserveETH: inputReserveETH, reserveToken: inputReserveToken } = useExchangeReserves(inputCurrency) + const { reserveETH: outputReserveETH, reserveToken: outputReserveToken } = useExchangeReserves(outputCurrency) + + // get balances for each of the currency types + const inputBalance = useAddressBalance(account, inputCurrency) + const outputBalance = useAddressBalance(account, outputCurrency) + const inputBalanceFormatted = !!(inputBalance && Number.isInteger(inputDecimals)) + ? amountFormatter(inputBalance, inputDecimals, Math.min(4, inputDecimals)) + : '' + const outputBalanceFormatted = !!(outputBalance && Number.isInteger(outputDecimals)) + ? amountFormatter(outputBalance, outputDecimals, Math.min(4, outputDecimals)) + : '' + + // compute useful transforms of the data above + const independentDecimals = independentField === INPUT ? inputDecimals : outputDecimals + const dependentDecimals = independentField === OUTPUT ? inputDecimals : outputDecimals + + // declare/get parsed and formatted versions of input/output values + const [independentValueParsed, setIndependentValueParsed] = useState() + const dependentValueFormatted = !!(dependentValue && (dependentDecimals || dependentDecimals === 0)) + ? amountFormatter(dependentValue, dependentDecimals, Math.min(4, dependentDecimals), false) + : '' + const inputValueParsed = independentField === INPUT ? independentValueParsed : dependentValue + const inputValueFormatted = independentField === INPUT ? independentValue : dependentValueFormatted + const outputValueParsed = independentField === OUTPUT ? independentValueParsed : dependentValue + const outputValueFormatted = independentField === OUTPUT ? independentValue : dependentValueFormatted + + // validate + parse independent value + const [independentError, setIndependentError] = useState() + useEffect(() => { + if (independentValue && (independentDecimals || independentDecimals === 0)) { + try { + const parsedValue = ethers.utils.parseUnits(independentValue, independentDecimals) + + if (parsedValue.lte(ethers.constants.Zero) || parsedValue.gte(ethers.constants.MaxUint256)) { + throw Error() + } else { + setIndependentValueParsed(parsedValue) + setIndependentError(null) + } + } catch { + setIndependentError(t('inputNotValid')) + } + + return () => { + setIndependentValueParsed() + setIndependentError() + } + } + }, [independentValue, independentDecimals, t]) + + // calculate slippage from target rate + const { minimum: dependentValueMinumum, maximum: dependentValueMaximum } = calculateSlippageBounds( + dependentValue, + swapType === TOKEN_TO_TOKEN, + tokenAllowedSlippageBig, + allowedSlippageBig + ) + + // validate input allowance + balance + const [inputError, setInputError] = useState() + const [showUnlock, setShowUnlock] = useState(false) + useEffect(() => { + const inputValueCalculation = independentField === INPUT ? independentValueParsed : dependentValueMaximum + if (inputBalance && (inputAllowance || inputCurrency === 'ETH') && inputValueCalculation) { + if (inputBalance.lt(inputValueCalculation)) { + setInputError(t('insufficientBalance')) + } else if (inputCurrency !== 'ETH' && inputAllowance.lt(inputValueCalculation)) { + setInputError(t('unlockTokenCont')) + setShowUnlock(true) + } else { + setInputError(null) + setShowUnlock(false) + } + + return () => { + setInputError() + setShowUnlock(false) + } + } + }, [independentField, independentValueParsed, dependentValueMaximum, inputBalance, inputCurrency, inputAllowance, t]) + + // calculate dependent value + useEffect(() => { + const amount = independentValueParsed + + if (swapType === ETH_TO_TOKEN) { + const reserveETH = outputReserveETH + const reserveToken = outputReserveToken + + if (amount && reserveETH && reserveToken) { + try { + const calculatedDependentValue = + independentField === INPUT + ? calculateEtherTokenOutputFromInput(amount, reserveETH, reserveToken) + : calculateEtherTokenInputFromOutput(amount, reserveETH, reserveToken) + + if (calculatedDependentValue.lte(ethers.constants.Zero)) { + throw Error() + } + + dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: calculatedDependentValue }) + } catch { + setIndependentError(t('insufficientLiquidity')) + } + return () => { + dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: '' }) + } + } + } else if (swapType === TOKEN_TO_ETH) { + const reserveETH = inputReserveETH + const reserveToken = inputReserveToken + + if (amount && reserveETH && reserveToken) { + try { + const calculatedDependentValue = + independentField === INPUT + ? calculateEtherTokenOutputFromInput(amount, reserveToken, reserveETH) + : calculateEtherTokenInputFromOutput(amount, reserveToken, reserveETH) + + if (calculatedDependentValue.lte(ethers.constants.Zero)) { + throw Error() + } + + dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: calculatedDependentValue }) + } catch { + setIndependentError(t('insufficientLiquidity')) + } + return () => { + dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: '' }) + } + } + } else if (swapType === TOKEN_TO_TOKEN) { + const reserveETHFirst = inputReserveETH + const reserveTokenFirst = inputReserveToken + + const reserveETHSecond = outputReserveETH + const reserveTokenSecond = outputReserveToken + + if (amount && reserveETHFirst && reserveTokenFirst && reserveETHSecond && reserveTokenSecond) { + try { + if (independentField === INPUT) { + const intermediateValue = calculateEtherTokenOutputFromInput(amount, reserveTokenFirst, reserveETHFirst) + if (intermediateValue.lte(ethers.constants.Zero)) { + throw Error() + } + const calculatedDependentValue = calculateEtherTokenOutputFromInput( + intermediateValue, + reserveETHSecond, + reserveTokenSecond + ) + if (calculatedDependentValue.lte(ethers.constants.Zero)) { + throw Error() + } + dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: calculatedDependentValue }) + } else { + const intermediateValue = calculateEtherTokenInputFromOutput(amount, reserveETHSecond, reserveTokenSecond) + if (intermediateValue.lte(ethers.constants.Zero)) { + throw Error() + } + const calculatedDependentValue = calculateEtherTokenInputFromOutput( + intermediateValue, + reserveTokenFirst, + reserveETHFirst + ) + if (calculatedDependentValue.lte(ethers.constants.Zero)) { + throw Error() + } + dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: calculatedDependentValue }) + } + } catch { + setIndependentError(t('insufficientLiquidity')) + } + return () => { + dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: '' }) + } + } + } + }, [ + independentValueParsed, + swapType, + outputReserveETH, + outputReserveToken, + inputReserveETH, + inputReserveToken, + independentField, + t + ]) + + const [inverted, setInverted] = useState(false) + const exchangeRate = getExchangeRate(inputValueParsed, inputDecimals, outputValueParsed, outputDecimals) + const exchangeRateInverted = getExchangeRate(inputValueParsed, inputDecimals, outputValueParsed, outputDecimals, true) + + const marketRate = getMarketRate( + swapType, + inputReserveETH, + inputReserveToken, + inputDecimals, + outputReserveETH, + outputReserveToken, + outputDecimals + ) + + const percentSlippage = + exchangeRate && marketRate + ? exchangeRate + .sub(marketRate) + .abs() + .mul(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(18))) + .div(marketRate) + .sub(ethers.utils.bigNumberify(3).mul(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(15)))) + : undefined + const percentSlippageFormatted = percentSlippage && amountFormatter(percentSlippage, 16, 2) + const slippageWarning = + percentSlippage && + percentSlippage.gte(ethers.utils.parseEther('.05')) && + percentSlippage.lt(ethers.utils.parseEther('.2')) // [5% - 20%) + const highSlippageWarning = percentSlippage && percentSlippage.gte(ethers.utils.parseEther('.2')) // [20+% + + const isValid = sending + ? exchangeRate && inputError === null && independentError === null && recipientError === null + : exchangeRate && inputError === null && independentError === null + + const estimatedText = `(${t('estimated')})` + function formatBalance(value) { + return `Balance: ${value}` + } + + async function onSwap() { + const deadline = Math.ceil(Date.now() / 1000) + DEADLINE_FROM_NOW + + let estimate, method, args, value + if (independentField === INPUT) { + ReactGA.event({ + category: `${swapType}`, + action: sending ? 'TransferInput' : 'SwapInput' + }) + + if (swapType === ETH_TO_TOKEN) { + estimate = sending ? contract.estimate.ethToTokenTransferInput : contract.estimate.ethToTokenSwapInput + method = sending ? contract.ethToTokenTransferInput : contract.ethToTokenSwapInput + args = sending ? [dependentValueMinumum, deadline, recipient.address] : [dependentValueMinumum, deadline] + value = independentValueParsed + } else if (swapType === TOKEN_TO_ETH) { + estimate = sending ? contract.estimate.tokenToEthTransferInput : contract.estimate.tokenToEthSwapInput + method = sending ? contract.tokenToEthTransferInput : contract.tokenToEthSwapInput + args = sending + ? [independentValueParsed, dependentValueMinumum, deadline, recipient.address] + : [independentValueParsed, dependentValueMinumum, deadline] + value = ethers.constants.Zero + } else if (swapType === TOKEN_TO_TOKEN) { + estimate = sending ? contract.estimate.tokenToTokenTransferInput : contract.estimate.tokenToTokenSwapInput + method = sending ? contract.tokenToTokenTransferInput : contract.tokenToTokenSwapInput + args = sending + ? [ + independentValueParsed, + dependentValueMinumum, + ethers.constants.One, + deadline, + recipient.address, + outputCurrency + ] + : [independentValueParsed, dependentValueMinumum, ethers.constants.One, deadline, outputCurrency] + value = ethers.constants.Zero + } + } else if (independentField === OUTPUT) { + ReactGA.event({ + category: `${swapType}`, + action: sending ? 'TransferOutput' : 'SwapOutput' + }) + + if (swapType === ETH_TO_TOKEN) { + estimate = sending ? contract.estimate.ethToTokenTransferOutput : contract.estimate.ethToTokenSwapOutput + method = sending ? contract.ethToTokenTransferOutput : contract.ethToTokenSwapOutput + args = sending ? [independentValueParsed, deadline, recipient.address] : [independentValueParsed, deadline] + value = dependentValueMaximum + } else if (swapType === TOKEN_TO_ETH) { + estimate = sending ? contract.estimate.tokenToEthTransferOutput : contract.estimate.tokenToEthSwapOutput + method = sending ? contract.tokenToEthTransferOutput : contract.tokenToEthSwapOutput + args = sending + ? [independentValueParsed, dependentValueMaximum, deadline, recipient.address] + : [independentValueParsed, dependentValueMaximum, deadline] + value = ethers.constants.Zero + } else if (swapType === TOKEN_TO_TOKEN) { + estimate = sending ? contract.estimate.tokenToTokenTransferOutput : contract.estimate.tokenToTokenSwapOutput + method = sending ? contract.tokenToTokenTransferOutput : contract.tokenToTokenSwapOutput + args = sending + ? [ + independentValueParsed, + dependentValueMaximum, + ethers.constants.MaxUint256, + deadline, + recipient.address, + outputCurrency + ] + : [independentValueParsed, dependentValueMaximum, ethers.constants.MaxUint256, deadline, outputCurrency] + value = ethers.constants.Zero + } + } + + const estimatedGasLimit = await estimate(...args, { value }) + method(...args, { value, gasLimit: calculateGasMargin(estimatedGasLimit, GAS_MARGIN) }).then(response => { + addTransaction(response) + }) + } + + const [customSlippageError, setcustomSlippageError] = useState('') + + return ( + <> + { + if (inputBalance && inputDecimals) { + const valueToSet = inputCurrency === 'ETH' ? inputBalance.sub(ethers.utils.parseEther('.1')) : inputBalance + if (valueToSet.gt(ethers.constants.Zero)) { + dispatchSwapState({ + type: 'UPDATE_INDEPENDENT', + payload: { value: amountFormatter(valueToSet, inputDecimals, inputDecimals, false), field: INPUT } + }) + } + } + }} + onCurrencySelected={inputCurrency => { + dispatchSwapState({ type: 'SELECT_CURRENCY', payload: { currency: inputCurrency, field: INPUT } }) + }} + onValueChange={inputValue => { + dispatchSwapState({ type: 'UPDATE_INDEPENDENT', payload: { value: inputValue, field: INPUT } }) + }} + showUnlock={showUnlock} + selectedTokens={[inputCurrency, outputCurrency]} + selectedTokenAddress={inputCurrency} + value={inputValueFormatted} + errorMessage={inputError ? inputError : independentField === INPUT ? independentError : ''} + /> + + + { + dispatchSwapState({ type: 'FLIP_INDEPENDENT' }) + }} + clickable + alt="swap" + src={isValid ? ArrowDownBlue : ArrowDownGrey} + /> + + + { + dispatchSwapState({ type: 'SELECT_CURRENCY', payload: { currency: outputCurrency, field: OUTPUT } }) + }} + onValueChange={outputValue => { + dispatchSwapState({ type: 'UPDATE_INDEPENDENT', payload: { value: outputValue, field: OUTPUT } }) + }} + selectedTokens={[inputCurrency, outputCurrency]} + selectedTokenAddress={outputCurrency} + value={outputValueFormatted} + errorMessage={independentField === OUTPUT ? independentError : ''} + disableUnlock + /> + {sending ? ( + <> + + + + + + + + ) : ( + '' + )} + + { + setInverted(inverted => !inverted) + }} + > + {t('exchangeRate')} + {inverted ? ( + + {exchangeRate + ? `1 ${outputSymbol} = ${amountFormatter(exchangeRateInverted, 18, 4, false)} ${inputSymbol}` + : ' - '} + + ) : ( + + {exchangeRate + ? `1 ${inputSymbol} = ${amountFormatter(exchangeRate, 18, 4, false)} ${outputSymbol}` + : ' - '} + + )} + + + + + + + + ) +} diff --git a/src/components/TransactionDetails/index.js b/src/components/TransactionDetails/index.js index 367fb64238..2fa9dcfa11 100644 --- a/src/components/TransactionDetails/index.js +++ b/src/components/TransactionDetails/index.js @@ -1,7 +1,8 @@ import React, { useState } from 'react' +import ReactGA from 'react-ga' import { useTranslation } from 'react-i18next' import styled from 'styled-components' -import { amountFormatter } from '../../utils' +import { isAddress, amountFormatter } from '../../utils' import questionMark from '../../assets/images/question-mark.svg' import NewContextualInfo from '../../components/ContextualInfoNew' @@ -194,6 +195,10 @@ export default function TransactionDetails(props) { 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 @@ -211,7 +216,13 @@ export default function TransactionDetails(props) { closeDetailsText={t('hideDetails')} contextualInfo={contextualInfo ? contextualInfo : slippageWarningText} allowExpand={ - !!(props.inputCurrency && props.outputCurrency && props.inputValueParsed && props.outputValueParsed) + !!( + props.inputCurrency && + props.outputCurrency && + props.inputValueParsed && + props.outputValueParsed && + (props.sending ? props.recipientAddress : true) + ) } isError={isError} slippageWarning={props.slippageWarning && !contextualInfo} @@ -405,8 +416,40 @@ export default function TransactionDetails(props) { const b = text => {text} const renderTransactionDetails = () => { + ReactGA.event({ + category: 'TransactionDetail', + action: 'Open' + }) + if (props.independentField === props.INPUT) { - return ( + 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')}{' '} @@ -433,7 +476,34 @@ export default function TransactionDetails(props) {
) } else { - return ( + return props.sending ? ( +
+
+ {t('youAreSending')}{' '} + {b( + `${amountFormatter( + props.independentValueParsed, + props.independentDecimals, + Math.min(4, props.independentDecimals) + )} ${props.outputSymbol}` + )}{' '} + {t('to')} {b(props.recipientAddress)}. +
+ + {t('itWillCost')}{' '} + {b( + `${amountFormatter( + props.dependentValueMaximum, + props.dependentDecimals, + Math.min(4, props.dependentDecimals) + )} ${props.inputSymbol}` + )}{' '} + + + {t('priceChange')} {b(`${props.percentSlippageFormatted}%`)}. + +
+ ) : (
{t('youAreBuying')}{' '} @@ -455,7 +525,6 @@ export default function TransactionDetails(props) { Math.min(4, props.dependentDecimals) )} ${props.inputSymbol}` )}{' '} - {t('orTransFail')} {t('priceChange')} {b(`${props.percentSlippageFormatted}%`)}. diff --git a/src/pages/App.js b/src/pages/App.js index 96f3d0bbfe..e51230cad8 100644 --- a/src/pages/App.js +++ b/src/pages/App.js @@ -46,7 +46,7 @@ export default function App() { {/* this Suspense is for route code-splitting */} - } /> + } /> - + } /> theme.royalBlue}; -` - -const DownArrowBackground = styled.div` - ${({ theme }) => theme.flexRowNoWrap} - justify-content: center; - align-items: center; -` - -const DownArrow = styled.img` - width: 0.625rem; - height: 0.625rem; - position: relative; - padding: 0.875rem; - cursor: ${({ clickable }) => clickable && 'pointer'}; -` - -const LastSummaryText = styled.div` - margin-top: 1rem; -` - -const ExchangeRateWrapper = styled.div` - ${({ theme }) => theme.flexRowNoWrap}; - align-items: center; - color: ${({ theme }) => theme.doveGray}; - font-size: 0.75rem; - padding: 0.5rem 1rem; -` - -const ExchangeRate = styled.span` - flex: 1 1 auto; - width: 0; - color: ${({ theme }) => theme.chaliceGray}; -` - -const Flex = styled.div` - display: flex; - justify-content: center; - padding: 2rem; - - button { - max-width: 20rem; - } -` - -function calculateSlippageBounds(value, token = false) { - if (value) { - const offset = value.mul(token ? TOKEN_ALLOWED_SLIPPAGE : ALLOWED_SLIPPAGE).div(ethers.utils.bigNumberify(10000)) - const minimum = value.sub(offset) - const maximum = value.add(offset) - return { - minimum: minimum.lt(ethers.constants.Zero) ? ethers.constants.Zero : minimum, - maximum: maximum.gt(ethers.constants.MaxUint256) ? ethers.constants.MaxUint256 : maximum - } - } else { - return {} - } -} - -function getSwapType(inputCurrency, outputCurrency) { - if (!inputCurrency || !outputCurrency) { - return null - } else if (inputCurrency === 'ETH') { - return ETH_TO_TOKEN - } else if (outputCurrency === 'ETH') { - return TOKEN_TO_ETH - } else { - return TOKEN_TO_TOKEN - } -} - -// this mocks the getInputPrice function, and calculates the required output -function calculateEtherTokenOutputFromInput(inputAmount, inputReserve, outputReserve) { - const inputAmountWithFee = inputAmount.mul(ethers.utils.bigNumberify(997)) - const numerator = inputAmountWithFee.mul(outputReserve) - const denominator = inputReserve.mul(ethers.utils.bigNumberify(1000)).add(inputAmountWithFee) - return numerator.div(denominator) -} - -// this mocks the getOutputPrice function, and calculates the required input -function calculateEtherTokenInputFromOutput(outputAmount, inputReserve, outputReserve) { - const numerator = inputReserve.mul(outputAmount).mul(ethers.utils.bigNumberify(1000)) - const denominator = outputReserve.sub(outputAmount).mul(ethers.utils.bigNumberify(997)) - return numerator.div(denominator).add(ethers.constants.One) -} - -function getInitialSwapState(outputCurrency) { - return { - independentValue: '', // this is a user input - dependentValue: '', // this is a calculated number - independentField: INPUT, - inputCurrency: 'ETH', - outputCurrency: outputCurrency ? outputCurrency : '' - } -} - -function swapStateReducer(state, action) { - switch (action.type) { - case 'FLIP_INDEPENDENT': { - const { independentField, inputCurrency, outputCurrency } = state - return { - ...state, - dependentValue: '', - independentField: independentField === INPUT ? OUTPUT : INPUT, - inputCurrency: outputCurrency, - outputCurrency: inputCurrency - } - } - case 'SELECT_CURRENCY': { - const { inputCurrency, outputCurrency } = state - const { field, currency } = action.payload - - const newInputCurrency = field === INPUT ? currency : inputCurrency - const newOutputCurrency = field === OUTPUT ? currency : outputCurrency - - if (newInputCurrency === newOutputCurrency) { - return { - ...state, - inputCurrency: field === INPUT ? currency : '', - outputCurrency: field === OUTPUT ? currency : '' - } - } else { - return { - ...state, - inputCurrency: newInputCurrency, - outputCurrency: newOutputCurrency - } - } - } - case 'UPDATE_INDEPENDENT': { - const { field, value } = action.payload - return { - ...state, - independentValue: value, - dependentValue: '', - independentField: field - } - } - case 'UPDATE_DEPENDENT': { - return { - ...state, - dependentValue: action.payload - } - } - default: { - return getInitialSwapState() - } - } -} - -function getExchangeRate(inputValue, inputDecimals, outputValue, outputDecimals, invert = false) { - try { - if ( - inputValue && - (inputDecimals || inputDecimals === 0) && - outputValue && - (outputDecimals || outputDecimals === 0) - ) { - const factor = ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(18)) - - if (invert) { - return inputValue - .mul(factor) - .div(outputValue) - .mul(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(outputDecimals))) - .div(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(inputDecimals))) - } else { - return outputValue - .mul(factor) - .div(inputValue) - .mul(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(inputDecimals))) - .div(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(outputDecimals))) - } - } - } catch {} -} - -function getMarketRate( - swapType, - inputReserveETH, - inputReserveToken, - inputDecimals, - outputReserveETH, - outputReserveToken, - outputDecimals, - invert = false -) { - if (swapType === ETH_TO_TOKEN) { - return getExchangeRate(outputReserveETH, 18, outputReserveToken, outputDecimals, invert) - } else if (swapType === TOKEN_TO_ETH) { - return getExchangeRate(inputReserveToken, inputDecimals, inputReserveETH, 18, invert) - } else if (swapType === TOKEN_TO_TOKEN) { - const factor = ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(18)) - const firstRate = getExchangeRate(inputReserveToken, inputDecimals, inputReserveETH, 18) - const secondRate = getExchangeRate(outputReserveETH, 18, outputReserveToken, outputDecimals) - try { - return !!(firstRate && secondRate) ? firstRate.mul(secondRate).div(factor) : undefined - } catch {} - } -} - -export default function Swap({ initialCurrency }) { - const { t } = useTranslation() - const { account } = useWeb3Context() - - const addTransaction = useTransactionAdder() - - // analytics - useEffect(() => { - ReactGA.pageview(window.location.pathname + window.location.search) - }, []) - - // core swap state - const [swapState, dispatchSwapState] = useReducer(swapStateReducer, initialCurrency, getInitialSwapState) - const { independentValue, dependentValue, independentField, inputCurrency, outputCurrency } = swapState - - const [recipient, setRecipient] = useState({ address: '', name: '' }) - const [recipientError, setRecipientError] = useState() - - // get swap type from the currency types - const swapType = getSwapType(inputCurrency, outputCurrency) - - // get decimals and exchange addressfor each of the currency types - const { symbol: inputSymbol, decimals: inputDecimals, exchangeAddress: inputExchangeAddress } = useTokenDetails( - inputCurrency - ) - const { symbol: outputSymbol, decimals: outputDecimals, exchangeAddress: outputExchangeAddress } = useTokenDetails( - outputCurrency - ) - - const inputExchangeContract = useExchangeContract(inputExchangeAddress) - const outputExchangeContract = useExchangeContract(outputExchangeAddress) - const contract = swapType === ETH_TO_TOKEN ? outputExchangeContract : inputExchangeContract - - // get input allowance - const inputAllowance = useAddressAllowance(account, inputCurrency, inputExchangeAddress) - - // fetch reserves for each of the currency types - const { reserveETH: inputReserveETH, reserveToken: inputReserveToken } = useExchangeReserves(inputCurrency) - const { reserveETH: outputReserveETH, reserveToken: outputReserveToken } = useExchangeReserves(outputCurrency) - - // get balances for each of the currency types - const inputBalance = useAddressBalance(account, inputCurrency) - const outputBalance = useAddressBalance(account, outputCurrency) - const inputBalanceFormatted = !!(inputBalance && Number.isInteger(inputDecimals)) - ? amountFormatter(inputBalance, inputDecimals, Math.min(4, inputDecimals)) - : '' - const outputBalanceFormatted = !!(outputBalance && Number.isInteger(outputDecimals)) - ? amountFormatter(outputBalance, outputDecimals, Math.min(4, outputDecimals)) - : '' - - // compute useful transforms of the data above - const independentDecimals = independentField === INPUT ? inputDecimals : outputDecimals - const dependentDecimals = independentField === OUTPUT ? inputDecimals : outputDecimals - - // declare/get parsed and formatted versions of input/output values - const [independentValueParsed, setIndependentValueParsed] = useState() - const dependentValueFormatted = !!(dependentValue && (dependentDecimals || dependentDecimals === 0)) - ? amountFormatter(dependentValue, dependentDecimals, Math.min(4, dependentDecimals), false) - : '' - const inputValueParsed = independentField === INPUT ? independentValueParsed : dependentValue - const inputValueFormatted = independentField === INPUT ? independentValue : dependentValueFormatted - const outputValueParsed = independentField === OUTPUT ? independentValueParsed : dependentValue - const outputValueFormatted = independentField === OUTPUT ? independentValue : dependentValueFormatted - - // validate + parse independent value - const [independentError, setIndependentError] = useState() - useEffect(() => { - if (independentValue && (independentDecimals || independentDecimals === 0)) { - try { - const parsedValue = ethers.utils.parseUnits(independentValue, independentDecimals) - - if (parsedValue.lte(ethers.constants.Zero) || parsedValue.gte(ethers.constants.MaxUint256)) { - throw Error() - } else { - setIndependentValueParsed(parsedValue) - setIndependentError(null) - } - } catch { - setIndependentError(t('inputNotValid')) - } - - return () => { - setIndependentValueParsed() - setIndependentError() - } - } - }, [independentValue, independentDecimals, t]) - - // calculate slippage from target rate - const { minimum: dependentValueMinumum, maximum: dependentValueMaximum } = calculateSlippageBounds( - dependentValue, - swapType === TOKEN_TO_TOKEN - ) - - // validate input allowance + balance - const [inputError, setInputError] = useState() - const [showUnlock, setShowUnlock] = useState(false) - useEffect(() => { - const inputValueCalculation = independentField === INPUT ? independentValueParsed : dependentValueMaximum - - if (inputBalance && (inputAllowance || inputCurrency === 'ETH') && inputValueCalculation) { - if (inputBalance.lt(inputValueCalculation)) { - setInputError(t('insufficientBalance')) - } else if (inputCurrency !== 'ETH' && inputAllowance.lt(inputValueCalculation)) { - setInputError(t('unlockTokenCont')) - setShowUnlock(true) - } else { - setInputError(null) - setShowUnlock(false) - } - - return () => { - setInputError() - setShowUnlock(false) - } - } - }, [independentField, independentValueParsed, dependentValueMaximum, inputBalance, inputCurrency, inputAllowance, t]) - - // calculate dependent value - useEffect(() => { - const amount = independentValueParsed - - if (swapType === ETH_TO_TOKEN) { - const reserveETH = outputReserveETH - const reserveToken = outputReserveToken - - if (amount && reserveETH && reserveToken) { - try { - const calculatedDependentValue = - independentField === INPUT - ? calculateEtherTokenOutputFromInput(amount, reserveETH, reserveToken) - : calculateEtherTokenInputFromOutput(amount, reserveETH, reserveToken) - - if (calculatedDependentValue.lte(ethers.constants.Zero)) { - throw Error() - } - - dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: calculatedDependentValue }) - } catch { - setIndependentError(t('insufficientLiquidity')) - } - return () => { - dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: '' }) - } - } - } else if (swapType === TOKEN_TO_ETH) { - const reserveETH = inputReserveETH - const reserveToken = inputReserveToken - - if (amount && reserveETH && reserveToken) { - try { - const calculatedDependentValue = - independentField === INPUT - ? calculateEtherTokenOutputFromInput(amount, reserveToken, reserveETH) - : calculateEtherTokenInputFromOutput(amount, reserveToken, reserveETH) - - if (calculatedDependentValue.lte(ethers.constants.Zero)) { - throw Error() - } - - dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: calculatedDependentValue }) - } catch { - setIndependentError(t('insufficientLiquidity')) - } - return () => { - dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: '' }) - } - } - } else if (swapType === TOKEN_TO_TOKEN) { - const reserveETHFirst = inputReserveETH - const reserveTokenFirst = inputReserveToken - - const reserveETHSecond = outputReserveETH - const reserveTokenSecond = outputReserveToken - - if (amount && reserveETHFirst && reserveTokenFirst && reserveETHSecond && reserveTokenSecond) { - try { - if (independentField === INPUT) { - const intermediateValue = calculateEtherTokenOutputFromInput(amount, reserveTokenFirst, reserveETHFirst) - if (intermediateValue.lte(ethers.constants.Zero)) { - throw Error() - } - const calculatedDependentValue = calculateEtherTokenOutputFromInput( - intermediateValue, - reserveETHSecond, - reserveTokenSecond - ) - if (calculatedDependentValue.lte(ethers.constants.Zero)) { - throw Error() - } - dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: calculatedDependentValue }) - } else { - const intermediateValue = calculateEtherTokenInputFromOutput(amount, reserveETHSecond, reserveTokenSecond) - if (intermediateValue.lte(ethers.constants.Zero)) { - throw Error() - } - // console.log('hi!', amountFormatter(intermediateValue, )) - const calculatedDependentValue = calculateEtherTokenInputFromOutput( - intermediateValue, - reserveTokenFirst, - reserveETHFirst - ) - if (calculatedDependentValue.lte(ethers.constants.Zero)) { - throw Error() - } - dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: calculatedDependentValue }) - } - } catch { - setIndependentError(t('insufficientLiquidity')) - } - return () => { - dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: '' }) - } - } - } - }, [ - independentValueParsed, - swapType, - outputReserveETH, - outputReserveToken, - inputReserveETH, - inputReserveToken, - independentField, - t - ]) - - const [inverted, setInverted] = useState(false) - const exchangeRate = getExchangeRate(inputValueParsed, inputDecimals, outputValueParsed, outputDecimals) - const exchangeRateInverted = getExchangeRate(inputValueParsed, inputDecimals, outputValueParsed, outputDecimals, true) - - const marketRate = getMarketRate( - swapType, - inputReserveETH, - inputReserveToken, - inputDecimals, - outputReserveETH, - outputReserveToken, - outputDecimals - ) - - const percentSlippage = - exchangeRate && marketRate - ? exchangeRate - .sub(marketRate) - .abs() - .mul(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(18))) - .div(marketRate) - .sub(ethers.utils.bigNumberify(3).mul(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(15)))) - : undefined - const percentSlippageFormatted = percentSlippage && amountFormatter(percentSlippage, 16, 2) - const slippageWarning = - percentSlippage && - percentSlippage.gte(ethers.utils.parseEther('.05')) && - percentSlippage.lt(ethers.utils.parseEther('.2')) // [5% - 20%) - const highSlippageWarning = percentSlippage && percentSlippage.gte(ethers.utils.parseEther('.2')) // [20+% - - const isValid = exchangeRate && inputError === null && independentError === null && recipientError === null - - const estimatedText = `(${t('estimated')})` - function formatBalance(value) { - return `Balance: ${value}` - } - - function renderTransactionDetails() { - ReactGA.event({ - category: 'TransactionDetail', - action: 'Open' - }) - - const b = text => {text} - - if (independentField === INPUT) { - return ( -
-
- {t('youAreSelling')}{' '} - {b( - `${amountFormatter( - independentValueParsed, - independentDecimals, - Math.min(4, independentDecimals) - )} ${inputSymbol}` - )} - . -
- - {b(recipient.address)} {t('willReceive')}{' '} - {b( - `${amountFormatter( - dependentValueMinumum, - dependentDecimals, - Math.min(4, dependentDecimals) - )} ${outputSymbol}` - )}{' '} - {t('orTransFail')} - - - {(slippageWarning || highSlippageWarning) && ( - - ⚠️ - - )} - {t('priceChange')} {b(`${percentSlippageFormatted}%`)}. - -
- ) - } else { - return ( -
-
- {t('youAreSending')}{' '} - {b( - `${amountFormatter( - independentValueParsed, - independentDecimals, - Math.min(4, independentDecimals) - )} ${outputSymbol}` - )}{' '} - {t('to')} {b(recipient.address)}. -
- - {t('itWillCost')}{' '} - {b( - `${amountFormatter( - dependentValueMaximum, - dependentDecimals, - Math.min(4, dependentDecimals) - )} ${inputSymbol}` - )}{' '} - {t('orTransFail')} - - - {t('priceChange')} {b(`${percentSlippageFormatted}%`)}. - -
- ) - } - } - - function renderSummary() { - let contextualInfo = '' - let isError = false - - if (inputError || independentError) { - contextualInfo = inputError || independentError - isError = true - } else if (!inputCurrency || !outputCurrency) { - contextualInfo = t('selectTokenCont') - } else if (!independentValue) { - contextualInfo = t('enterValueCont') - } else if (!recipient.address) { - contextualInfo = t('noRecipient') - } else if (!isAddress(recipient.address)) { - contextualInfo = t('invalidRecipient') - } else if (!account) { - contextualInfo = t('noWallet') - isError = true - } - - const slippageWarningText = highSlippageWarning - ? t('highSlippageWarning') - : slippageWarning - ? t('slippageWarning') - : '' - - return ( - - ) - } - - async function onSwap() { - const deadline = Math.ceil(Date.now() / 1000) + DEADLINE_FROM_NOW - - let estimate, method, args, value - if (independentField === INPUT) { - ReactGA.event({ - category: `${swapType}`, - action: 'TransferInput' - }) - - if (swapType === ETH_TO_TOKEN) { - estimate = contract.estimate.ethToTokenTransferInput - method = contract.ethToTokenTransferInput - args = [dependentValueMinumum, deadline, recipient.address] - value = independentValueParsed - } else if (swapType === TOKEN_TO_ETH) { - estimate = contract.estimate.tokenToEthTransferInput - method = contract.tokenToEthTransferInput - args = [independentValueParsed, dependentValueMinumum, deadline, recipient.address] - value = ethers.constants.Zero - } else if (swapType === TOKEN_TO_TOKEN) { - estimate = contract.estimate.tokenToTokenTransferInput - method = contract.tokenToTokenTransferInput - args = [ - independentValueParsed, - dependentValueMinumum, - ethers.constants.One, - deadline, - recipient.address, - outputCurrency - ] - value = ethers.constants.Zero - } - } else if (independentField === OUTPUT) { - ReactGA.event({ - category: `${swapType}`, - action: 'TransferOutput' - }) - - if (swapType === ETH_TO_TOKEN) { - estimate = contract.estimate.ethToTokenTransferOutput - method = contract.ethToTokenTransferOutput - args = [independentValueParsed, deadline, recipient.address] - value = dependentValueMaximum - } else if (swapType === TOKEN_TO_ETH) { - estimate = contract.estimate.tokenToEthTransferOutput - method = contract.tokenToEthTransferOutput - args = [independentValueParsed, dependentValueMaximum, deadline, recipient.address] - value = ethers.constants.Zero - } else if (swapType === TOKEN_TO_TOKEN) { - estimate = contract.estimate.tokenToTokenTransferOutput - method = contract.tokenToTokenTransferOutput - args = [ - independentValueParsed, - dependentValueMaximum, - ethers.constants.MaxUint256, - deadline, - recipient.address, - outputCurrency - ] - value = ethers.constants.Zero - } - } - - const estimatedGasLimit = await estimate(...args, { value }) - method(...args, { value, gasLimit: calculateGasMargin(estimatedGasLimit, GAS_MARGIN) }).then(response => { - addTransaction(response) - }) - } - - return ( - <> - { - if (inputBalance && inputDecimals) { - const valueToSet = inputCurrency === 'ETH' ? inputBalance.sub(ethers.utils.parseEther('.1')) : inputBalance - if (valueToSet.gt(ethers.constants.Zero)) { - dispatchSwapState({ - type: 'UPDATE_INDEPENDENT', - payload: { value: amountFormatter(valueToSet, inputDecimals, inputDecimals, false), field: INPUT } - }) - } - } - }} - onCurrencySelected={inputCurrency => { - dispatchSwapState({ type: 'SELECT_CURRENCY', payload: { currency: inputCurrency, field: INPUT } }) - }} - onValueChange={inputValue => { - dispatchSwapState({ type: 'UPDATE_INDEPENDENT', payload: { value: inputValue, field: INPUT } }) - }} - showUnlock={showUnlock} - selectedTokens={[inputCurrency, outputCurrency]} - selectedTokenAddress={inputCurrency} - value={inputValueFormatted} - errorMessage={inputError ? inputError : independentField === INPUT ? independentError : ''} - /> - - - { - dispatchSwapState({ type: 'FLIP_INDEPENDENT' }) - }} - clickable - alt="swap" - src={isValid ? ArrowDownBlue : ArrowDownGrey} - /> - - - { - dispatchSwapState({ type: 'SELECT_CURRENCY', payload: { currency: outputCurrency, field: OUTPUT } }) - }} - onValueChange={outputValue => { - dispatchSwapState({ type: 'UPDATE_INDEPENDENT', payload: { value: outputValue, field: OUTPUT } }) - }} - selectedTokens={[inputCurrency, outputCurrency]} - selectedTokenAddress={outputCurrency} - value={outputValueFormatted} - errorMessage={independentField === OUTPUT ? independentError : ''} - disableUnlock - /> - - - - - - - - { - setInverted(inverted => !inverted) - }} - > - {t('exchangeRate')} - {inverted ? ( - - {exchangeRate - ? `1 ${outputSymbol} = ${amountFormatter(exchangeRateInverted, 18, 4, false)} ${inputSymbol}` - : ' - '} - - ) : ( - - {exchangeRate - ? `1 ${inputSymbol} = ${amountFormatter(exchangeRate, 18, 4, false)} ${outputSymbol}` - : ' - '} - - )} - - - {renderSummary()} - - - - - ) +export default function Send({ initialCurrency }) { + return } diff --git a/src/pages/Swap/index.js b/src/pages/Swap/index.js index 70b48eb673..bef79d33dc 100644 --- a/src/pages/Swap/index.js +++ b/src/pages/Swap/index.js @@ -1,700 +1,6 @@ -import React, { useState, useReducer, useEffect } from 'react' -import ReactGA from 'react-ga' -import { useTranslation } from 'react-i18next' -import { useWeb3Context } from 'web3-react' -import { ethers } from 'ethers' -import styled from 'styled-components' +import React from 'react' +import ExchangePage from '../../components/ExchangePage' -import { Button } from '../../theme' -import CurrencyInputPanel from '../../components/CurrencyInputPanel' -import NewContextualInfo from '../../components/ContextualInfoNew' -import AddressInputPanel from '../../components/AddressInputPanel' -import OversizedPanel from '../../components/OversizedPanel' -import TransactionDetails from '../../components/TransactionDetails' -import ArrowDownBlue from '../../assets/images/arrow-down-blue.svg' -import ArrowDownGrey from '../../assets/images/arrow-down-grey.svg' -import { amountFormatter, calculateGasMargin } from '../../utils' -import { useExchangeContract } from '../../hooks' -import { useTokenDetails } from '../../contexts/Tokens' -import { useTransactionAdder } from '../../contexts/Transactions' -import { useAddressBalance, useExchangeReserves } from '../../contexts/Balances' -import { useAddressAllowance } from '../../contexts/Allowances' - -const INPUT = 0 -const OUTPUT = 1 - -const ETH_TO_TOKEN = 0 -const TOKEN_TO_ETH = 1 -const TOKEN_TO_TOKEN = 2 - -// denominated in bips -const ALLOWED_SLIPPAGE_DEFAULT = 150 -const TOKEN_ALLOWED_SLIPPAGE_DEFAULT = 200 - -// denominated in seconds -const DEADLINE_FROM_NOW = 60 * 15 - -// denominated in bips -const GAS_MARGIN = ethers.utils.bigNumberify(1000) - -const BlueSpan = styled.span` - color: ${({ theme }) => theme.royalBlue}; -` - -const LastSummaryText = styled.div` - margin-top: 1rem; -` - -const DownArrowBackground = styled.div` - ${({ theme }) => theme.flexRowNoWrap} - justify-content: center; - align-items: center; -` - -const DownArrow = styled.img` - width: 0.625rem; - height: 0.625rem; - position: relative; - padding: 0.875rem; - cursor: ${({ clickable }) => clickable && 'pointer'}; -` - -const ExchangeRateWrapper = styled.div` - ${({ theme }) => theme.flexRowNoWrap}; - align-items: center; - color: ${({ theme }) => theme.doveGray}; - font-size: 0.75rem; - padding: 0.5rem 1rem; -` - -const ExchangeRate = styled.span` - flex: 1 1 auto; - width: 0; - color: ${({ theme }) => theme.chaliceGray}; -` - -const Flex = styled.div` - display: flex; - justify-content: center; - padding: 2rem; - - button { - max-width: 20rem; - } -` - -function calculateSlippageBounds(value, token = false, tokenAllowedSlippage, allowedSlippage) { - if (value) { - const offset = value.mul(token ? tokenAllowedSlippage : allowedSlippage).div(ethers.utils.bigNumberify(10000)) - const minimum = value.sub(offset) - const maximum = value.add(offset) - return { - minimum: minimum.lt(ethers.constants.Zero) ? ethers.constants.Zero : minimum, - maximum: maximum.gt(ethers.constants.MaxUint256) ? ethers.constants.MaxUint256 : maximum - } - } else { - return {} - } -} - -function getSwapType(inputCurrency, outputCurrency) { - if (!inputCurrency || !outputCurrency) { - return null - } else if (inputCurrency === 'ETH') { - return ETH_TO_TOKEN - } else if (outputCurrency === 'ETH') { - return TOKEN_TO_ETH - } else { - return TOKEN_TO_TOKEN - } -} - -// this mocks the getInputPrice function, and calculates the required output -function calculateEtherTokenOutputFromInput(inputAmount, inputReserve, outputReserve) { - const inputAmountWithFee = inputAmount.mul(ethers.utils.bigNumberify(997)) - const numerator = inputAmountWithFee.mul(outputReserve) - const denominator = inputReserve.mul(ethers.utils.bigNumberify(1000)).add(inputAmountWithFee) - return numerator.div(denominator) -} - -// this mocks the getOutputPrice function, and calculates the required input -function calculateEtherTokenInputFromOutput(outputAmount, inputReserve, outputReserve) { - const numerator = inputReserve.mul(outputAmount).mul(ethers.utils.bigNumberify(1000)) - const denominator = outputReserve.sub(outputAmount).mul(ethers.utils.bigNumberify(997)) - return numerator.div(denominator).add(ethers.constants.One) -} - -function getInitialSwapState(outputCurrency) { - return { - independentValue: '', // this is a user input - dependentValue: '', // this is a calculated number - independentField: INPUT, - inputCurrency: 'ETH', - outputCurrency: outputCurrency ? outputCurrency : '' - } -} - -function swapStateReducer(state, action) { - switch (action.type) { - case 'FLIP_INDEPENDENT': { - const { independentField, inputCurrency, outputCurrency } = state - return { - ...state, - dependentValue: '', - independentField: independentField === INPUT ? OUTPUT : INPUT, - inputCurrency: outputCurrency, - outputCurrency: inputCurrency - } - } - case 'SELECT_CURRENCY': { - const { inputCurrency, outputCurrency } = state - const { field, currency } = action.payload - - const newInputCurrency = field === INPUT ? currency : inputCurrency - const newOutputCurrency = field === OUTPUT ? currency : outputCurrency - - if (newInputCurrency === newOutputCurrency) { - return { - ...state, - inputCurrency: field === INPUT ? currency : '', - outputCurrency: field === OUTPUT ? currency : '' - } - } else { - return { - ...state, - inputCurrency: newInputCurrency, - outputCurrency: newOutputCurrency - } - } - } - case 'UPDATE_INDEPENDENT': { - const { field, value } = action.payload - const { dependentValue, independentValue } = state - return { - ...state, - independentValue: value, - dependentValue: value === independentValue ? dependentValue : '', - independentField: field - } - } - case 'UPDATE_DEPENDENT': { - return { - ...state, - dependentValue: action.payload - } - } - default: { - return getInitialSwapState() - } - } -} - -function getExchangeRate(inputValue, inputDecimals, outputValue, outputDecimals, invert = false) { - try { - if ( - inputValue && - (inputDecimals || inputDecimals === 0) && - outputValue && - (outputDecimals || outputDecimals === 0) - ) { - const factor = ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(18)) - - if (invert) { - return inputValue - .mul(factor) - .div(outputValue) - .mul(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(outputDecimals))) - .div(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(inputDecimals))) - } else { - return outputValue - .mul(factor) - .div(inputValue) - .mul(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(inputDecimals))) - .div(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(outputDecimals))) - } - } - } catch {} -} - -function getMarketRate( - swapType, - inputReserveETH, - inputReserveToken, - inputDecimals, - outputReserveETH, - outputReserveToken, - outputDecimals, - invert = false -) { - if (swapType === ETH_TO_TOKEN) { - return getExchangeRate(outputReserveETH, 18, outputReserveToken, outputDecimals, invert) - } else if (swapType === TOKEN_TO_ETH) { - return getExchangeRate(inputReserveToken, inputDecimals, inputReserveETH, 18, invert) - } else if (swapType === TOKEN_TO_TOKEN) { - const factor = ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(18)) - const firstRate = getExchangeRate(inputReserveToken, inputDecimals, inputReserveETH, 18) - const secondRate = getExchangeRate(outputReserveETH, 18, outputReserveToken, outputDecimals) - try { - return !!(firstRate && secondRate) ? firstRate.mul(secondRate).div(factor) : undefined - } catch {} - } -} - -export default function Swap({ initialCurrency, sending }) { - const { t } = useTranslation() - const { account } = useWeb3Context() - - const addTransaction = useTransactionAdder() - - const [rawSlippage, setRawSlippage] = useState(ALLOWED_SLIPPAGE_DEFAULT) - const [rawTokenSlippage, setRawTokenSlippage] = useState(TOKEN_ALLOWED_SLIPPAGE_DEFAULT) - - let allowedSlippageBig = ethers.utils.bigNumberify(rawSlippage) - let tokenAllowedSlippageBig = ethers.utils.bigNumberify(rawTokenSlippage) - - // analytics - useEffect(() => { - ReactGA.pageview(window.location.pathname + window.location.search) - }, []) - - // core swap state- - const [swapState, dispatchSwapState] = useReducer(swapStateReducer, initialCurrency, getInitialSwapState) - const { independentValue, dependentValue, independentField, inputCurrency, outputCurrency } = swapState - - const [recipient, setRecipient] = useState({ address: '', name: '' }) - const [recipientError, setRecipientError] = useState() - - // get swap type from the currency types - const swapType = getSwapType(inputCurrency, outputCurrency) - - // get decimals and exchange addressfor each of the currency types - const { symbol: inputSymbol, decimals: inputDecimals, exchangeAddress: inputExchangeAddress } = useTokenDetails( - inputCurrency - ) - const { symbol: outputSymbol, decimals: outputDecimals, exchangeAddress: outputExchangeAddress } = useTokenDetails( - outputCurrency - ) - - const inputExchangeContract = useExchangeContract(inputExchangeAddress) - const outputExchangeContract = useExchangeContract(outputExchangeAddress) - const contract = swapType === ETH_TO_TOKEN ? outputExchangeContract : inputExchangeContract - - // get input allowance - const inputAllowance = useAddressAllowance(account, inputCurrency, inputExchangeAddress) - - // fetch reserves for each of the currency types - const { reserveETH: inputReserveETH, reserveToken: inputReserveToken } = useExchangeReserves(inputCurrency) - const { reserveETH: outputReserveETH, reserveToken: outputReserveToken } = useExchangeReserves(outputCurrency) - - // get balances for each of the currency types - const inputBalance = useAddressBalance(account, inputCurrency) - const outputBalance = useAddressBalance(account, outputCurrency) - const inputBalanceFormatted = !!(inputBalance && Number.isInteger(inputDecimals)) - ? amountFormatter(inputBalance, inputDecimals, Math.min(4, inputDecimals)) - : '' - const outputBalanceFormatted = !!(outputBalance && Number.isInteger(outputDecimals)) - ? amountFormatter(outputBalance, outputDecimals, Math.min(4, outputDecimals)) - : '' - - // compute useful transforms of the data above - const independentDecimals = independentField === INPUT ? inputDecimals : outputDecimals - const dependentDecimals = independentField === OUTPUT ? inputDecimals : outputDecimals - - // declare/get parsed and formatted versions of input/output values - const [independentValueParsed, setIndependentValueParsed] = useState() - const dependentValueFormatted = !!(dependentValue && (dependentDecimals || dependentDecimals === 0)) - ? amountFormatter(dependentValue, dependentDecimals, Math.min(4, dependentDecimals), false) - : '' - const inputValueParsed = independentField === INPUT ? independentValueParsed : dependentValue - const inputValueFormatted = independentField === INPUT ? independentValue : dependentValueFormatted - const outputValueParsed = independentField === OUTPUT ? independentValueParsed : dependentValue - const outputValueFormatted = independentField === OUTPUT ? independentValue : dependentValueFormatted - - // validate + parse independent value - const [independentError, setIndependentError] = useState() - useEffect(() => { - if (independentValue && (independentDecimals || independentDecimals === 0)) { - try { - const parsedValue = ethers.utils.parseUnits(independentValue, independentDecimals) - - if (parsedValue.lte(ethers.constants.Zero) || parsedValue.gte(ethers.constants.MaxUint256)) { - throw Error() - } else { - setIndependentValueParsed(parsedValue) - setIndependentError(null) - } - } catch { - setIndependentError(t('inputNotValid')) - } - - return () => { - setIndependentValueParsed() - setIndependentError() - } - } - }, [independentValue, independentDecimals, t]) - - // calculate slippage from target rate - const { minimum: dependentValueMinumum, maximum: dependentValueMaximum } = calculateSlippageBounds( - dependentValue, - swapType === TOKEN_TO_TOKEN, - tokenAllowedSlippageBig, - allowedSlippageBig - ) - - // validate input allowance + balance - const [inputError, setInputError] = useState() - const [showUnlock, setShowUnlock] = useState(false) - useEffect(() => { - const inputValueCalculation = independentField === INPUT ? independentValueParsed : dependentValueMaximum - if (inputBalance && (inputAllowance || inputCurrency === 'ETH') && inputValueCalculation) { - if (inputBalance.lt(inputValueCalculation)) { - setInputError(t('insufficientBalance')) - } else if (inputCurrency !== 'ETH' && inputAllowance.lt(inputValueCalculation)) { - setInputError(t('unlockTokenCont')) - setShowUnlock(true) - } else { - setInputError(null) - setShowUnlock(false) - } - - return () => { - setInputError() - setShowUnlock(false) - } - } - }, [independentField, independentValueParsed, dependentValueMaximum, inputBalance, inputCurrency, inputAllowance, t]) - - // calculate dependent value - useEffect(() => { - const amount = independentValueParsed - - if (swapType === ETH_TO_TOKEN) { - const reserveETH = outputReserveETH - const reserveToken = outputReserveToken - - if (amount && reserveETH && reserveToken) { - try { - const calculatedDependentValue = - independentField === INPUT - ? calculateEtherTokenOutputFromInput(amount, reserveETH, reserveToken) - : calculateEtherTokenInputFromOutput(amount, reserveETH, reserveToken) - - if (calculatedDependentValue.lte(ethers.constants.Zero)) { - throw Error() - } - - dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: calculatedDependentValue }) - } catch { - setIndependentError(t('insufficientLiquidity')) - } - return () => { - dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: '' }) - } - } - } else if (swapType === TOKEN_TO_ETH) { - const reserveETH = inputReserveETH - const reserveToken = inputReserveToken - - if (amount && reserveETH && reserveToken) { - try { - const calculatedDependentValue = - independentField === INPUT - ? calculateEtherTokenOutputFromInput(amount, reserveToken, reserveETH) - : calculateEtherTokenInputFromOutput(amount, reserveToken, reserveETH) - - if (calculatedDependentValue.lte(ethers.constants.Zero)) { - throw Error() - } - - dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: calculatedDependentValue }) - } catch { - setIndependentError(t('insufficientLiquidity')) - } - return () => { - dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: '' }) - } - } - } else if (swapType === TOKEN_TO_TOKEN) { - const reserveETHFirst = inputReserveETH - const reserveTokenFirst = inputReserveToken - - const reserveETHSecond = outputReserveETH - const reserveTokenSecond = outputReserveToken - - if (amount && reserveETHFirst && reserveTokenFirst && reserveETHSecond && reserveTokenSecond) { - try { - if (independentField === INPUT) { - const intermediateValue = calculateEtherTokenOutputFromInput(amount, reserveTokenFirst, reserveETHFirst) - if (intermediateValue.lte(ethers.constants.Zero)) { - throw Error() - } - const calculatedDependentValue = calculateEtherTokenOutputFromInput( - intermediateValue, - reserveETHSecond, - reserveTokenSecond - ) - if (calculatedDependentValue.lte(ethers.constants.Zero)) { - throw Error() - } - dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: calculatedDependentValue }) - } else { - const intermediateValue = calculateEtherTokenInputFromOutput(amount, reserveETHSecond, reserveTokenSecond) - if (intermediateValue.lte(ethers.constants.Zero)) { - throw Error() - } - const calculatedDependentValue = calculateEtherTokenInputFromOutput( - intermediateValue, - reserveTokenFirst, - reserveETHFirst - ) - if (calculatedDependentValue.lte(ethers.constants.Zero)) { - throw Error() - } - dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: calculatedDependentValue }) - } - } catch { - setIndependentError(t('insufficientLiquidity')) - } - return () => { - dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: '' }) - } - } - } - }, [ - independentValueParsed, - swapType, - outputReserveETH, - outputReserveToken, - inputReserveETH, - inputReserveToken, - independentField, - t - ]) - - const [inverted, setInverted] = useState(false) - const exchangeRate = getExchangeRate(inputValueParsed, inputDecimals, outputValueParsed, outputDecimals) - const exchangeRateInverted = getExchangeRate(inputValueParsed, inputDecimals, outputValueParsed, outputDecimals, true) - - const marketRate = getMarketRate( - swapType, - inputReserveETH, - inputReserveToken, - inputDecimals, - outputReserveETH, - outputReserveToken, - outputDecimals - ) - - const percentSlippage = - exchangeRate && marketRate - ? exchangeRate - .sub(marketRate) - .abs() - .mul(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(18))) - .div(marketRate) - .sub(ethers.utils.bigNumberify(3).mul(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(15)))) - : undefined - const percentSlippageFormatted = percentSlippage && amountFormatter(percentSlippage, 16, 2) - const slippageWarning = - percentSlippage && - percentSlippage.gte(ethers.utils.parseEther('.05')) && - percentSlippage.lt(ethers.utils.parseEther('.2')) // [5% - 20%) - const highSlippageWarning = percentSlippage && percentSlippage.gte(ethers.utils.parseEther('.2')) // [20+% - - const isValid = sending - ? exchangeRate && inputError === null && independentError === null && recipientError === null - : exchangeRate && inputError === null && independentError === null - - const estimatedText = `(${t('estimated')})` - function formatBalance(value) { - return `Balance: ${value}` - } - - async function onSwap() { - const deadline = Math.ceil(Date.now() / 1000) + DEADLINE_FROM_NOW - - let estimate, method, args, value - if (independentField === INPUT) { - ReactGA.event({ - category: `${swapType}`, - action: 'SwapInput' - }) - - if (swapType === ETH_TO_TOKEN) { - estimate = contract.estimate.ethToTokenSwapInput - method = contract.ethToTokenSwapInput - args = [dependentValueMinumum, deadline] - value = independentValueParsed - } else if (swapType === TOKEN_TO_ETH) { - estimate = contract.estimate.tokenToEthSwapInput - method = contract.tokenToEthSwapInput - args = [independentValueParsed, dependentValueMinumum, deadline] - value = ethers.constants.Zero - } else if (swapType === TOKEN_TO_TOKEN) { - estimate = contract.estimate.tokenToTokenSwapInput - method = contract.tokenToTokenSwapInput - args = [independentValueParsed, dependentValueMinumum, ethers.constants.One, deadline, outputCurrency] - value = ethers.constants.Zero - } - } else if (independentField === OUTPUT) { - ReactGA.event({ - category: `${swapType}`, - action: 'SwapOutput' - }) - - if (swapType === ETH_TO_TOKEN) { - estimate = contract.estimate.ethToTokenSwapOutput - method = contract.ethToTokenSwapOutput - args = [independentValueParsed, deadline] - value = dependentValueMaximum - } else if (swapType === TOKEN_TO_ETH) { - estimate = contract.estimate.tokenToEthSwapOutput - method = contract.tokenToEthSwapOutput - args = [independentValueParsed, dependentValueMaximum, deadline] - value = ethers.constants.Zero - } else if (swapType === TOKEN_TO_TOKEN) { - estimate = contract.estimate.tokenToTokenSwapOutput - method = contract.tokenToTokenSwapOutput - args = [independentValueParsed, dependentValueMaximum, ethers.constants.MaxUint256, deadline, outputCurrency] - value = ethers.constants.Zero - } - } - - const estimatedGasLimit = await estimate(...args, { value }) - method(...args, { value, gasLimit: calculateGasMargin(estimatedGasLimit, GAS_MARGIN) }).then(response => { - addTransaction(response) - }) - } - - const [customSlippageError, setcustomSlippageError] = useState('') - - return ( - <> - { - if (inputBalance && inputDecimals) { - const valueToSet = inputCurrency === 'ETH' ? inputBalance.sub(ethers.utils.parseEther('.1')) : inputBalance - if (valueToSet.gt(ethers.constants.Zero)) { - dispatchSwapState({ - type: 'UPDATE_INDEPENDENT', - payload: { value: amountFormatter(valueToSet, inputDecimals, inputDecimals, false), field: INPUT } - }) - } - } - }} - onCurrencySelected={inputCurrency => { - dispatchSwapState({ type: 'SELECT_CURRENCY', payload: { currency: inputCurrency, field: INPUT } }) - }} - onValueChange={inputValue => { - dispatchSwapState({ type: 'UPDATE_INDEPENDENT', payload: { value: inputValue, field: INPUT } }) - }} - showUnlock={showUnlock} - selectedTokens={[inputCurrency, outputCurrency]} - selectedTokenAddress={inputCurrency} - value={inputValueFormatted} - errorMessage={inputError ? inputError : independentField === INPUT ? independentError : ''} - /> - - - { - dispatchSwapState({ type: 'FLIP_INDEPENDENT' }) - }} - clickable - alt="swap" - src={isValid ? ArrowDownBlue : ArrowDownGrey} - /> - - - { - dispatchSwapState({ type: 'SELECT_CURRENCY', payload: { currency: outputCurrency, field: OUTPUT } }) - }} - onValueChange={outputValue => { - dispatchSwapState({ type: 'UPDATE_INDEPENDENT', payload: { value: outputValue, field: OUTPUT } }) - }} - selectedTokens={[inputCurrency, outputCurrency]} - selectedTokenAddress={outputCurrency} - value={outputValueFormatted} - errorMessage={independentField === OUTPUT ? independentError : ''} - disableUnlock - /> - {sending ? ( - <> - - - - - - - - ) : ( - '' - )} - - { - setInverted(inverted => !inverted) - }} - > - {t('exchangeRate')} - {inverted ? ( - - {exchangeRate - ? `1 ${outputSymbol} = ${amountFormatter(exchangeRateInverted, 18, 4, false)} ${inputSymbol}` - : ' - '} - - ) : ( - - {exchangeRate - ? `1 ${inputSymbol} = ${amountFormatter(exchangeRate, 18, 4, false)} ${outputSymbol}` - : ' - '} - - )} - - - - - - - - ) +export default function Swap({ initialCurrency }) { + return } From 73042777351cf8424695e6030e7f1f627e414bd2 Mon Sep 17 00:00:00 2001 From: ian-jh Date: Mon, 29 Jul 2019 16:03:35 -0400 Subject: [PATCH 10/12] update appjs component structure --- src/pages/App.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/App.js b/src/pages/App.js index e51230cad8..bc4ba97e1a 100644 --- a/src/pages/App.js +++ b/src/pages/App.js @@ -46,7 +46,7 @@ export default function App() { {/* this Suspense is for route code-splitting */} - } /> + - } /> + Date: Tue, 30 Jul 2019 10:40:00 -0400 Subject: [PATCH 11/12] fix build errors; --- src/components/TransactionDetails/index.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/TransactionDetails/index.js b/src/components/TransactionDetails/index.js index f86a36d931..a4f15b9844 100644 --- a/src/components/TransactionDetails/index.js +++ b/src/components/TransactionDetails/index.js @@ -18,8 +18,6 @@ const WARNING_TYPE = Object.freeze({ riskyEntryLow: 'riskyEntryLow' }) -const b = text => {text} - const Flex = styled.div` display: flex; justify-content: center; From d90604daccdbe1c34fb86a627c1af8e363859eca Mon Sep 17 00:00:00 2001 From: ian-jh Date: Tue, 30 Jul 2019 10:53:38 -0400 Subject: [PATCH 12/12] update code style for build --- src/components/ExchangePage/index.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/ExchangePage/index.jsx b/src/components/ExchangePage/index.jsx index 19291e1af1..51cfb2605b 100644 --- a/src/components/ExchangePage/index.jsx +++ b/src/components/ExchangePage/index.jsx @@ -36,7 +36,6 @@ const DEADLINE_FROM_NOW = 60 * 15 // denominated in bips const GAS_MARGIN = ethers.utils.bigNumberify(1000) - const DownArrowBackground = styled.div` ${({ theme }) => theme.flexRowNoWrap} justify-content: center;