resolve merge conflicts

This commit is contained in:
ian-jh 2019-07-29 15:45:48 -04:00
commit 3d452b70e4
7 changed files with 1022 additions and 1778 deletions

@ -32,7 +32,8 @@ const SummaryWrapperContainer = styled.div`
const Details = styled.div`
background-color: ${({ theme }) => theme.concreteGray};
/* padding: 1.25rem 1.25rem 1rem 1.25rem; */
padding: 1.5rem;
padding-bottom: 1rem;
border-radius: 1rem;
font-size: 0.75rem;
margin: 1rem 0.5rem 0 0.5rem;

@ -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 (
<>
<CurrencyInputPanel
title={t('input')}
description={inputValueFormatted && independentField === OUTPUT ? estimatedText : ''}
extraText={inputBalanceFormatted && formatBalance(inputBalanceFormatted)}
extraTextClickHander={() => {
if (inputBalance && inputDecimals) {
const valueToSet = inputCurrency === 'ETH' ? inputBalance.sub(ethers.utils.parseEther('.1')) : inputBalance
if (valueToSet.gt(ethers.constants.Zero)) {
dispatchSwapState({
type: 'UPDATE_INDEPENDENT',
payload: { value: amountFormatter(valueToSet, inputDecimals, inputDecimals, false), field: INPUT }
})
}
}
}}
onCurrencySelected={inputCurrency => {
dispatchSwapState({ type: 'SELECT_CURRENCY', payload: { currency: inputCurrency, field: INPUT } })
}}
onValueChange={inputValue => {
dispatchSwapState({ type: 'UPDATE_INDEPENDENT', payload: { value: inputValue, field: INPUT } })
}}
showUnlock={showUnlock}
selectedTokens={[inputCurrency, outputCurrency]}
selectedTokenAddress={inputCurrency}
value={inputValueFormatted}
errorMessage={inputError ? inputError : independentField === INPUT ? independentError : ''}
/>
<OversizedPanel>
<DownArrowBackground>
<DownArrow
onClick={() => {
dispatchSwapState({ type: 'FLIP_INDEPENDENT' })
}}
clickable
alt="swap"
src={isValid ? ArrowDownBlue : ArrowDownGrey}
/>
</DownArrowBackground>
</OversizedPanel>
<CurrencyInputPanel
title={t('output')}
description={outputValueFormatted && independentField === INPUT ? estimatedText : ''}
extraText={outputBalanceFormatted && formatBalance(outputBalanceFormatted)}
onCurrencySelected={outputCurrency => {
dispatchSwapState({ type: 'SELECT_CURRENCY', payload: { currency: outputCurrency, field: OUTPUT } })
}}
onValueChange={outputValue => {
dispatchSwapState({ type: 'UPDATE_INDEPENDENT', payload: { value: outputValue, field: OUTPUT } })
}}
selectedTokens={[inputCurrency, outputCurrency]}
selectedTokenAddress={outputCurrency}
value={outputValueFormatted}
errorMessage={independentField === OUTPUT ? independentError : ''}
disableUnlock
/>
{sending ? (
<>
<OversizedPanel>
<DownArrowBackground>
<DownArrow src={isValid ? ArrowDownBlue : ArrowDownGrey} alt="arrow" />
</DownArrowBackground>
</OversizedPanel>
<AddressInputPanel onChange={setRecipient} onError={setRecipientError} />
</>
) : (
''
)}
<OversizedPanel hideBottom>
<ExchangeRateWrapper
onClick={() => {
setInverted(inverted => !inverted)
}}
>
<ExchangeRate>{t('exchangeRate')}</ExchangeRate>
{inverted ? (
<span>
{exchangeRate
? `1 ${outputSymbol} = ${amountFormatter(exchangeRateInverted, 18, 4, false)} ${inputSymbol}`
: ' - '}
</span>
) : (
<span>
{exchangeRate
? `1 ${inputSymbol} = ${amountFormatter(exchangeRate, 18, 4, false)} ${outputSymbol}`
: ' - '}
</span>
)}
</ExchangeRateWrapper>
</OversizedPanel>
<TransactionDetails
account={account}
setRawSlippage={setRawSlippage}
setRawTokenSlippage={setRawTokenSlippage}
rawSlippage={rawSlippage}
slippageWarning={slippageWarning}
highSlippageWarning={highSlippageWarning}
inputError={inputError}
independentError={independentError}
inputCurrency={inputCurrency}
outputCurrency={outputCurrency}
independentValue={independentValue}
independentValueParsed={independentValueParsed}
independentField={independentField}
INPUT={INPUT}
inputValueParsed={inputValueParsed}
outputValueParsed={outputValueParsed}
inputSymbol={inputSymbol}
outputSymbol={outputSymbol}
dependentValueMinumum={dependentValueMinumum}
dependentValueMaximum={dependentValueMaximum}
dependentDecimals={dependentDecimals}
independentDecimals={independentDecimals}
percentSlippageFormatted={percentSlippageFormatted}
setcustomSlippageError={setcustomSlippageError}
recipientAddress={recipient.address}
sending={sending}
/>
<Flex>
<Button
disabled={!isValid || customSlippageError === 'invalid'}
onClick={onSwap}
warning={highSlippageWarning || customSlippageError === 'warning'}
>
{sending
? highSlippageWarning || customSlippageError === 'warning'
? t('sendAnyway')
: t('send')
: highSlippageWarning || customSlippageError === 'warning'
? t('swapAnyway')
: t('swap')}
</Button>
</Flex>
</>
)
}

@ -5,9 +5,8 @@ import { DialogOverlay, DialogContent } from '@reach/dialog'
import '@reach/dialog/styles.css'
const AnimatedDialogOverlay = animated(DialogOverlay)
const WrappedDialogOverlay = ({ suppressClassNameWarning, ...rest }) => <AnimatedDialogOverlay {...rest} />
const StyledDialogOverlay = styled(WrappedDialogOverlay).attrs({
suppressClassNameWarning: true
const StyledDialogOverlay = styled(AnimatedDialogOverlay).attrs({
suppressclassnamewarning: 'true'
})`
&[data-reach-dialog-overlay] {
z-index: 2;

@ -1,80 +1,38 @@
import React, { useState, useEffect, useRef } from 'react'
import React, { useState } from 'react'
import ReactGA from 'react-ga'
import { useTranslation } from 'react-i18next'
import styled, { css, keyframes } from 'styled-components'
import { transparentize, darken } from 'polished'
import { amountFormatter } from '../../utils'
import { useDebounce } from '../../hooks'
import question from '../../assets/images/question.svg'
import styled from 'styled-components'
import { isAddress, amountFormatter } from '../../utils'
import questionMark from '../../assets/images/question-mark.svg'
import NewContextualInfo from '../../components/ContextualInfoNew'
const WARNING_TYPE = Object.freeze({
none: 'none',
emptyInput: 'emptyInput',
invalidEntryBound: 'invalidEntryBound',
riskyEntryHigh: 'riskyEntryHigh',
riskyEntryLow: 'riskyEntryLow'
})
const b = text => <Bold>{text}</Bold>
const Flex = styled.div`
display: flex;
justify-content: center;
button {
max-width: 20rem;
}
`
const FlexBetween = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
height: 100%;
`
const WrappedSlippageRow = ({ wrap, ...rest }) => <Flex {...rest} />
const SlippageRow = styled(WrappedSlippageRow)`
const SlippageRow = styled(Flex)`
position: relative;
flex-wrap: ${({ wrap }) => wrap && 'wrap'};
width: 100%;
flex-direction: row;
justify-content: flex-start;
align-items: center;
width: 100%;
font-size: 0.8rem;
padding: 0;
padding-top: ${({ wrap }) => wrap && '0.25rem'};
height: 24px;
margin-bottom: 14px;
`
const QuestionWrapper = styled.button`
display: flex;
align-items: center;
justify-content: center;
margin: 0;
padding: 0;
const QuestionWrapper = styled.div`
margin-left: 0.4rem;
padding: 0.2rem;
border: none;
background: none;
outline: none;
cursor: default;
border-radius: 36px;
margin-top: 0.2rem;
:hover,
:focus {
opacity: 0.7;
}
`
const HelpCircleStyled = styled.img`
height: 18px;
width: 18px;
`
const fadeIn = keyframes`
from {
opacity : 0;
}
to {
opacity : 1;
&:hover {
cursor: pointer;
}
`
@ -83,135 +41,100 @@ const Popup = styled(Flex)`
width: 228px;
left: -78px;
top: -124px;
flex-direction: column;
align-items: center;
padding: 0.6rem 1rem;
line-height: 150%;
background: ${({ theme }) => theme.charcoalBlack};
border-radius: 8px;
animation: ${fadeIn} 0.15s linear;
flex-direction: column;
aligm-items: center;
padding: 1rem;
line-height: 183.52%;
background: #404040;
border-radius: 8px;
color: white;
font-style: italic;
${({ theme }) => theme.mediaWidth.upToSmall`
left: -20px;
`}
`
const FancyButton = styled.button`
const Option = styled(Flex)`
align-items: center;
min-width: 55px;
height: 2rem;
height: 24px;
margin-right: 4px;
border-radius: 36px;
border: 1px solid ${({ theme }) => theme.mercuryGray};
outline: none;
background: ${({ theme }) => theme.white};
border: 1px solid #f2f2f2;
:hover {
cursor: inherit;
border: 1px solid ${({ theme }) => theme.royalBlue};
box-shadow: ${({ theme }) => transparentize(0.6, theme.royalBlue)} 0px 0px 0px 2px;
}
:focus {
box-shadow: ${({ theme }) => transparentize(0.6, theme.royalBlue)} 0px 0px 0px 2px;
}
`
const Option = styled(FancyButton)`
margin-right: 8px;
margin-top: 6px;
${({ active, theme }) =>
${({ active }) =>
active &&
css`
background-color: ${({ theme }) => theme.royalBlue};
color: ${({ theme }) => theme.white};
border: none;
`}
`
`
background-color: #2f80ed;
color: white;
border: 1px solid #2f80ed;
`}
const OptionLarge = styled(Option)`
width: 120px;
&:hover {
cursor: pointer;
}
`
const Input = styled.input`
background: ${({ theme }) => theme.white};
flex-grow: 1;
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;
}
cursor: inherit;
color: ${({ theme }) => theme.doveGray};
text-align: left;
${({ active }) =>
active &&
css`
color: initial;
cursor: initial;
text-align: right;
`}
`
border: 1px solid #2f80ed;
text-align: right;
padding-right 1.5rem;
padding-left 0rem;
color : inherit;
`}
${({ placeholder }) =>
placeholder !== 'Custom' &&
css`
text-align: right;
`}
${({ color }) =>
color === 'red' &&
css`
color: ${({ theme }) => theme.salmonRed};
`}
${({ warning }) =>
warning === 'red' &&
`
color : #FF6871;
border: 1px solid #FF6871;
`}
`
const BottomError = styled.div`
${({ show }) =>
show &&
css`
padding-top: 12px;
`}
color: ${({ theme }) => theme.doveGray};
margin-top: 1rem;
color: #aeaeae;
${({ color }) =>
color === 'red' &&
css`
color: ${({ theme }) => theme.salmonRed};
`}
`
color : #FF6871;
`}
`
const OptionCustom = styled(FancyButton)`
height: 2rem;
position: relative;
const Break = styled.div`
border: 1px solid #f2f2f2;
width: 100%;
margin-top: 1rem;
`
const OptionLarge = styled(Option)`
width: 120px;
margin-top: 6px;
padding: 0 0.75rem;
${({ active }) =>
active &&
css`
border: 1px solid ${({ theme }) => theme.royalBlue};
`}
${({ color }) =>
color === 'red' &&
css`
border: 1px solid ${({ theme }) => theme.salmonRed};
`}
input {
width: 100%;
height: 100%;
border: 0px;
border-radius: 2rem;
}
`
const Bold = styled.span`
@ -223,55 +146,44 @@ const LastSummaryText = styled.div`
`
const SlippageSelector = styled.div`
background-color: ${({ theme }) => darken(0.04, theme.concreteGray)};
padding: 1rem 1.25rem 1rem 1.25rem;
border-radius: 12px;
margin-top: 1rem;
`
const InputGroup = styled.div`
position: relative;
`
const Percent = styled.div`
right: 14px;
top: 9px;
position: absolute;
color: inherit;
font-size: 0, 8rem;
flex-grow: 0;
${({ color, theme }) =>
${({ color }) =>
(color === 'faded' &&
css`
color: ${theme.doveGray};
`) ||
`
color : #AEAEAE
`) ||
(color === 'red' &&
css`
color: ${theme.salmonRed};
`)};
`
color : #FF6871
`)}
`
const Faded = styled.span`
opacity: 0.7;
`
const TransactionInfo = styled.div`
padding: 1.25rem 1.25rem 1rem 1.25rem;
const ErrorEmoji = styled.span`
left: 30px;
top: 4px;
position: absolute;
`
export default function TransactionDetails(props) {
const { t } = useTranslation()
const [activeIndex, setActiveIndex] = useState(3)
const [warningType, setWarningType] = useState(WARNING_TYPE.none)
const inputRef = useRef()
const [showPopup, setPopup] = useState(false)
const [userInput, setUserInput] = useState('')
const debouncedInput = useDebounce(userInput, 150)
useEffect(() => {
if (activeIndex === 4) {
checkBounds(debouncedInput)
}
})
function renderSummary() {
let contextualInfo = ''
let isError = false
@ -283,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
@ -300,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}
@ -311,17 +233,23 @@ export default function TransactionDetails(props) {
)
}
const [activeIndex, setActiveIndex] = useState(3)
const [placeHolder, setplaceHolder] = useState('Custom')
const [warningType, setWarningType] = useState('none')
const [showPopup, setPopup] = useState(false)
const dropDownContent = () => {
return (
<>
{renderTransactionDetails()}
<Break />
<SlippageSelector>
<SlippageRow>
Limit additional price slippage
Limit addtional price slippage
<QuestionWrapper
onClick={() => {
setPopup(!showPopup)
}}
onMouseEnter={() => {
setPopup(true)
}}
@ -329,114 +257,105 @@ export default function TransactionDetails(props) {
setPopup(false)
}}
>
<HelpCircleStyled src={question} alt="popup" />
<img src={questionMark} alt="question mark" />
</QuestionWrapper>
{showPopup ? (
<Popup>
Lowering this limit decreases your risk of frontrunning. However, this makes it more likely that your
transaction will fail due to normal price movements.
Lowering this limit decreases your risk of frontrunning. This makes it more likely that your transaction
will fail due to normal price movements.
</Popup>
) : (
''
)}
</SlippageRow>
<SlippageRow wrap>
<SlippageRow>
<Option
onClick={() => {
setFromFixed(1, 0.1)
updateSlippage(0.1)
setWarningType('none')
setActiveIndex(1)
props.setcustomSlippageError('valid')
setplaceHolder('Custom')
}}
active={activeIndex === 1}
active={activeIndex === 1 ? true : false}
>
0.1%
</Option>
<Option
onClick={() => {
setFromFixed(2, 0.5)
updateSlippage(1)
setWarningType('none')
setActiveIndex(2)
props.setcustomSlippageError('valid')
setplaceHolder('Custom')
}}
active={activeIndex === 2}
active={activeIndex === 2 ? true : false}
>
0.5%
1%
</Option>
<OptionLarge
onClick={() => {
setFromFixed(3, 1)
updateSlippage(2)
setWarningType('none')
setActiveIndex(3)
props.setcustomSlippageError('valid')
setplaceHolder('Custom')
}}
active={activeIndex === 3}
active={activeIndex === 3 ? true : false}
>
1% <Faded>(suggested)</Faded>
2%
<Faded>(suggested)</Faded>
</OptionLarge>
<OptionCustom
active={activeIndex === 4}
color={
warningType === WARNING_TYPE.emptyInput
? ''
: warningType !== WARNING_TYPE.none && warningType !== WARNING_TYPE.riskyEntryLow
? 'red'
: ''
}
onClick={() => {
setFromCustom()
}}
>
<FlexBetween>
{!(warningType === WARNING_TYPE.none || warningType === WARNING_TYPE.emptyInput) && (
<span role="img" aria-label="warning">
</span>
)}
<Input
tabIndex={-1}
ref={inputRef}
active={activeIndex === 4}
placeholder={
activeIndex === 4
? !!userInput
? ''
: '0'
: activeIndex !== 4 && userInput !== ''
? userInput
: 'Custom'
}
value={activeIndex === 4 ? userInput : ''}
onChange={parseInput}
color={
warningType === WARNING_TYPE.emptyInput
? ''
: warningType !== WARNING_TYPE.none && warningType !== WARNING_TYPE.riskyEntryLow
? 'red'
: ''
}
/>
<Percent
color={
activeIndex !== 4
? 'faded'
: warningType === WARNING_TYPE.riskyEntryHigh || warningType === WARNING_TYPE.invalidEntryBound
? 'red'
: ''
}
>
%
</Percent>
</FlexBetween>
</OptionCustom>
<InputGroup>
{warningType !== 'none' ? <ErrorEmoji></ErrorEmoji> : ''}
<Input
placeholder={placeHolder}
value={userInput || ''}
onChange={parseInput}
onClick={e => {
setActiveIndex(4)
setplaceHolder('')
parseInput(e)
}}
active={activeIndex === 4 ? true : false}
warning={
warningType === 'emptyInput'
? ''
: warningType !== 'none' && warningType !== 'riskyEntryLow'
? 'red'
: ''
}
/>
<Percent
color={
warningType === 'emptyInput'
? 'faded'
: warningType !== 'none' && warningType !== 'riskyEntryLow'
? 'red'
: activeIndex !== 4
? 'faded'
: ''
}
>
%
</Percent>
</InputGroup>
</SlippageRow>
<SlippageRow>
<BottomError
show={activeIndex === 4}
color={
warningType === WARNING_TYPE.emptyInput
warningType === 'emptyInput'
? ''
: warningType !== WARNING_TYPE.none && warningType !== WARNING_TYPE.riskyEntryLow
: warningType !== 'none' && warningType !== 'riskyEntryLow'
? 'red'
: ''
}
>
{activeIndex === 4 && warningType.toString() === 'none' && 'Custom slippage value entered'}
{warningType === WARNING_TYPE.emptyInput && 'Enter a slippage percentage.'}
{warningType === WARNING_TYPE.invalidEntryBound && 'Please select value less than 50%'}
{warningType === WARNING_TYPE.riskyEntryHigh && 'Your transaction may be frontrun.'}
{warningType === WARNING_TYPE.riskyEntryLow && 'Your transaction may fail.'}
{warningType === 'emptyInput' ? 'Enter a slippage percentage.' : ''}
{warningType === 'invalidEntry' ? 'Please input a valid percentage.' : ''}
{warningType === 'invalidEntryBound' ? 'Pleae select value less than 50%' : ''}
{warningType === 'riskyEntryHigh' ? 'Your transaction may be frontrun.' : ''}
{warningType === 'riskyEntryLow' ? 'Your transaction may fail.' : ''}
</BottomError>
</SlippageRow>
</SlippageSelector>
@ -444,73 +363,94 @@ export default function TransactionDetails(props) {
)
}
const setFromCustom = () => {
setActiveIndex(4)
inputRef.current.focus()
// if there's a value, evaluate the bounds
checkBounds(userInput)
}
const [userInput, setUserInput] = useState()
// used for slippage presets
const setFromFixed = (index, slippage) => {
// update slippage in parent, reset errors and input state
updateSlippage(slippage)
setWarningType(WARNING_TYPE.none)
setActiveIndex(index)
props.setcustomSlippageError('valid`')
}
const checkBounds = slippageValue => {
setWarningType(WARNING_TYPE.none)
props.setcustomSlippageError('valid')
if (slippageValue === '') {
props.setcustomSlippageError('invalid')
return setWarningType(WARNING_TYPE.emptyInput)
}
// check bounds and set errors
if (slippageValue < 0 || slippageValue > 50) {
props.setcustomSlippageError('invalid')
return setWarningType(WARNING_TYPE.invalidEntryBound)
}
if (slippageValue >= 0 && slippageValue < 0.1) {
props.setcustomSlippageError('valid')
setWarningType(WARNING_TYPE.riskyEntryLow)
}
if (slippageValue > 5) {
props.setcustomSlippageError('warning')
setWarningType(WARNING_TYPE.riskyEntryHigh)
}
//update the actual slippage value in parent
updateSlippage(slippageValue)
}
// check that the theyve entered number and correct decimal
const parseInput = e => {
let input = e.target.value
// restrict to 2 decimal places
let acceptableValues = [/^$/, /^\d{1,2}$/, /^\d{0,2}\.\d{0,2}$/]
// if its within accepted decimal limit, update the input state
if (acceptableValues.some(a => a.test(input))) {
if (input === '') {
setUserInput(input)
props.setcustomSlippageError('invalid')
return setWarningType('emptyInput')
}
//check for decimal
let isValid = /^[+]?\d*\.?\d{1,2}$/.test(input) || /^[+]?\d*\.$/.test(input)
let decimalLimit = /^\d+\.?\d{0,2}$/.test(input) || input === ''
if (decimalLimit) {
setUserInput(input)
} else {
return
}
if (isValid) {
checkAcceptablePercentValue(input)
} else {
setWarningType('invalidEntry')
}
}
const checkAcceptablePercentValue = input => {
setTimeout(function() {
setWarningType('none')
props.setcustomSlippageError('valid')
if (input < 0 || input > 50) {
props.setcustomSlippageError('invalid')
return setWarningType('invalidEntryBound')
}
if (input >= 0 && input < 0.1) {
props.setcustomSlippageError('valid')
setWarningType('riskyEntryLow')
}
if (input >= 5) {
props.setcustomSlippageError('warning')
setWarningType('riskyEntryHigh')
}
updateSlippage(input)
}, 300)
}
const updateSlippage = newSlippage => {
// round to 2 decimals to prevent ethers error
let numParsed = parseFloat((newSlippage * 100).toFixed(2))
// set both slippage values in parents
props.setRawSlippage(numParsed)
props.setRawTokenSlippage(numParsed)
}
const b = text => <Bold>{text}</Bold>
const renderTransactionDetails = () => {
ReactGA.event({
category: 'TransactionDetail',
action: 'Open'
})
if (props.independentField === props.INPUT) {
return (
<TransactionInfo>
return props.sending ? (
<div>
<div>
{t('youAreSelling')}{' '}
{b(
`${amountFormatter(
props.independentValueParsed,
props.independentDecimals,
Math.min(4, props.independentDecimals)
)} ${props.inputSymbol}`
)}
.
</div>
<LastSummaryText>
{b(props.recipientAddress)} {t('willReceive')}{' '}
{b(
`${amountFormatter(
props.dependentValueMinumum,
props.dependentDecimals,
Math.min(4, props.dependentDecimals)
)} ${props.outputSymbol}`
)}{' '}
</LastSummaryText>
<LastSummaryText>
{t('priceChange')} {b(`${props.percentSlippageFormatted}%`)}.
</LastSummaryText>
</div>
) : (
<div>
<div>
{t('youAreSelling')}{' '}
{b(
@ -533,11 +473,38 @@ export default function TransactionDetails(props) {
<LastSummaryText>
{t('priceChange')} {b(`${props.percentSlippageFormatted}%`)}.
</LastSummaryText>
</TransactionInfo>
</div>
)
} else {
return (
<TransactionInfo>
return props.sending ? (
<div>
<div>
{t('youAreSending')}{' '}
{b(
`${amountFormatter(
props.independentValueParsed,
props.independentDecimals,
Math.min(4, props.independentDecimals)
)} ${props.outputSymbol}`
)}{' '}
{t('to')} {b(props.recipientAddress)}.
</div>
<LastSummaryText>
{t('itWillCost')}{' '}
{b(
`${amountFormatter(
props.dependentValueMaximum,
props.dependentDecimals,
Math.min(4, props.dependentDecimals)
)} ${props.inputSymbol}`
)}{' '}
</LastSummaryText>
<LastSummaryText>
{t('priceChange')} {b(`${props.percentSlippageFormatted}%`)}.
</LastSummaryText>
</div>
) : (
<div>
<div>
{t('youAreBuying')}{' '}
{b(
@ -558,12 +525,11 @@ export default function TransactionDetails(props) {
Math.min(4, props.dependentDecimals)
)} ${props.inputSymbol}`
)}{' '}
{t('orTransFail')}
</LastSummaryText>
<LastSummaryText>
{t('priceChange')} {b(`${props.percentSlippageFormatted}%`)}.
</LastSummaryText>
</TransactionInfo>
</div>
)
}
}

@ -46,7 +46,7 @@ export default function App() {
{/* this Suspense is for route code-splitting */}
<Suspense fallback={null}>
<Switch>
<Route exact strict path="/swap" component={Swap} />
<Route exact strict path="/swap" component={() => <Swap />} />
<Route
exact
strict
@ -59,7 +59,7 @@ export default function App() {
}
}}
/>
<Route exact strict path="/send" component={Send} />
<Route exact strict path="/send" component={() => <Send />} />
<Route
exact
strict

@ -1,783 +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 OversizedPanel from '../../components/OversizedPanel'
import AddressInputPanel from '../../components/AddressInputPanel'
import ArrowDownBlue from '../../assets/images/arrow-down-blue.svg'
import ArrowDownGrey from '../../assets/images/arrow-down-grey.svg'
import { isAddress, 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 = ethers.utils.bigNumberify(200)
const TOKEN_ALLOWED_SLIPPAGE = ethers.utils.bigNumberify(400)
// denominated in seconds
const DEADLINE_FROM_NOW = 60 * 15
// denominated in bips
const GAS_MARGIN = ethers.utils.bigNumberify(1000)
const BlueSpan = styled.span`
color: ${({ theme }) => theme.royalBlue};
`
const DownArrowBackground = styled.div`
${({ theme }) => theme.flexRowNoWrap}
justify-content: center;
align-items: center;
`
const DownArrow = styled.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 => <BlueSpan>{text}</BlueSpan>
if (independentField === INPUT) {
return (
<div>
<div>
{t('youAreSelling')}{' '}
{b(
`${amountFormatter(
independentValueParsed,
independentDecimals,
Math.min(4, independentDecimals)
)} ${inputSymbol}`
)}
.
</div>
<LastSummaryText>
{b(recipient.address)} {t('willReceive')}{' '}
{b(
`${amountFormatter(
dependentValueMinumum,
dependentDecimals,
Math.min(4, dependentDecimals)
)} ${outputSymbol}`
)}{' '}
{t('orTransFail')}
</LastSummaryText>
<LastSummaryText>
{(slippageWarning || highSlippageWarning) && (
<span role="img" aria-label="warning">
</span>
)}
{t('priceChange')} {b(`${percentSlippageFormatted}%`)}.
</LastSummaryText>
</div>
)
} else {
return (
<div>
<div>
{t('youAreSending')}{' '}
{b(
`${amountFormatter(
independentValueParsed,
independentDecimals,
Math.min(4, independentDecimals)
)} ${outputSymbol}`
)}{' '}
{t('to')} {b(recipient.address)}.
</div>
<LastSummaryText>
{t('itWillCost')}{' '}
{b(
`${amountFormatter(
dependentValueMaximum,
dependentDecimals,
Math.min(4, dependentDecimals)
)} ${inputSymbol}`
)}{' '}
{t('orTransFail')}
</LastSummaryText>
<LastSummaryText>
{t('priceChange')} {b(`${percentSlippageFormatted}%`)}.
</LastSummaryText>
</div>
)
}
}
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 (
<NewContextualInfo
openDetailsText={t('transactionDetails')}
closeDetailsText={t('hideDetails')}
contextualInfo={contextualInfo ? contextualInfo : slippageWarningText}
allowExpand={!!(inputCurrency && outputCurrency && inputValueParsed && outputValueParsed && recipient.address)}
isError={isError}
slippageWarning={slippageWarning && slippageWarningText}
highSlippageWarning={highSlippageWarning && slippageWarningText}
dropDownContent={renderTransactionDetails}
/>
)
}
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 (
<>
<CurrencyInputPanel
title={t('input')}
description={inputValueFormatted && independentField === OUTPUT ? estimatedText : ''}
extraText={inputBalanceFormatted && formatBalance(inputBalanceFormatted)}
extraTextClickHander={() => {
if (inputBalance && inputDecimals) {
const valueToSet = inputCurrency === 'ETH' ? inputBalance.sub(ethers.utils.parseEther('.1')) : inputBalance
if (valueToSet.gt(ethers.constants.Zero)) {
dispatchSwapState({
type: 'UPDATE_INDEPENDENT',
payload: { value: amountFormatter(valueToSet, inputDecimals, inputDecimals, false), field: INPUT }
})
}
}
}}
onCurrencySelected={inputCurrency => {
dispatchSwapState({ type: 'SELECT_CURRENCY', payload: { currency: inputCurrency, field: INPUT } })
}}
onValueChange={inputValue => {
dispatchSwapState({ type: 'UPDATE_INDEPENDENT', payload: { value: inputValue, field: INPUT } })
}}
showUnlock={showUnlock}
selectedTokens={[inputCurrency, outputCurrency]}
selectedTokenAddress={inputCurrency}
value={inputValueFormatted}
errorMessage={inputError ? inputError : independentField === INPUT ? independentError : ''}
/>
<OversizedPanel>
<DownArrowBackground>
<DownArrow
onClick={() => {
dispatchSwapState({ type: 'FLIP_INDEPENDENT' })
}}
clickable
alt="swap"
src={isValid ? ArrowDownBlue : ArrowDownGrey}
/>
</DownArrowBackground>
</OversizedPanel>
<CurrencyInputPanel
title={t('output')}
description={outputValueFormatted && independentField === INPUT ? estimatedText : ''}
extraText={outputBalanceFormatted && formatBalance(outputBalanceFormatted)}
onCurrencySelected={outputCurrency => {
dispatchSwapState({ type: 'SELECT_CURRENCY', payload: { currency: outputCurrency, field: OUTPUT } })
}}
onValueChange={outputValue => {
dispatchSwapState({ type: 'UPDATE_INDEPENDENT', payload: { value: outputValue, field: OUTPUT } })
}}
selectedTokens={[inputCurrency, outputCurrency]}
selectedTokenAddress={outputCurrency}
value={outputValueFormatted}
errorMessage={independentField === OUTPUT ? independentError : ''}
disableUnlock
/>
<OversizedPanel>
<DownArrowBackground>
<DownArrow src={isValid ? ArrowDownBlue : ArrowDownGrey} alt="arrow" />
</DownArrowBackground>
</OversizedPanel>
<AddressInputPanel onChange={setRecipient} onError={setRecipientError} />
<OversizedPanel hideBottom>
<ExchangeRateWrapper
onClick={() => {
setInverted(inverted => !inverted)
}}
>
<ExchangeRate>{t('exchangeRate')}</ExchangeRate>
{inverted ? (
<span>
{exchangeRate
? `1 ${inputSymbol} = ${amountFormatter(exchangeRate, 18, 4, false)} ${outputSymbol}`
: ' - '}
</span>
) : (
<span>
{exchangeRate
? `1 ${outputSymbol} = ${amountFormatter(exchangeRateInverted, 18, 4, false)} ${inputSymbol}`
: ' - '}
</span>
)}
</ExchangeRateWrapper>
</OversizedPanel>
{renderSummary()}
<Flex>
<Button disabled={!isValid} onClick={onSwap} warning={highSlippageWarning}>
{highSlippageWarning ? t('sendAnyway') : t('send')}
</Button>
</Flex>
</>
)
export default function Send({ initialCurrency }) {
return <ExchangePage initialCurrency={initialCurrency} sending={true} />
}

@ -1,673 +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 { Button } from '../../theme'
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
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 = 100
const TOKEN_ALLOWED_SLIPPAGE_DEFAULT = 100
// 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 {}
}
}
import React from 'react'
import ExchangePage from '../../components/ExchangePage'
export default function Swap({ initialCurrency }) {
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
// 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 = 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 (
<>
<CurrencyInputPanel
title={t('input')}
description={inputValueFormatted && independentField === OUTPUT ? estimatedText : ''}
extraText={inputBalanceFormatted && formatBalance(inputBalanceFormatted)}
extraTextClickHander={() => {
if (inputBalance && inputDecimals) {
const valueToSet = inputCurrency === 'ETH' ? inputBalance.sub(ethers.utils.parseEther('.1')) : inputBalance
if (valueToSet.gt(ethers.constants.Zero)) {
dispatchSwapState({
type: 'UPDATE_INDEPENDENT',
payload: { value: amountFormatter(valueToSet, inputDecimals, inputDecimals, false), field: INPUT }
})
}
}
}}
onCurrencySelected={inputCurrency => {
dispatchSwapState({ type: 'SELECT_CURRENCY', payload: { currency: inputCurrency, field: INPUT } })
}}
onValueChange={inputValue => {
dispatchSwapState({ type: 'UPDATE_INDEPENDENT', payload: { value: inputValue, field: INPUT } })
}}
showUnlock={showUnlock}
selectedTokens={[inputCurrency, outputCurrency]}
selectedTokenAddress={inputCurrency}
value={inputValueFormatted}
errorMessage={inputError ? inputError : independentField === INPUT ? independentError : ''}
/>
<OversizedPanel>
<DownArrowBackground>
<DownArrow
onClick={() => {
dispatchSwapState({ type: 'FLIP_INDEPENDENT' })
}}
clickable
alt="swap"
src={isValid ? ArrowDownBlue : ArrowDownGrey}
/>
</DownArrowBackground>
</OversizedPanel>
<CurrencyInputPanel
title={t('output')}
description={outputValueFormatted && independentField === INPUT ? estimatedText : ''}
extraText={outputBalanceFormatted && formatBalance(outputBalanceFormatted)}
onCurrencySelected={outputCurrency => {
dispatchSwapState({ type: 'SELECT_CURRENCY', payload: { currency: outputCurrency, field: OUTPUT } })
}}
onValueChange={outputValue => {
dispatchSwapState({ type: 'UPDATE_INDEPENDENT', payload: { value: outputValue, field: OUTPUT } })
}}
selectedTokens={[inputCurrency, outputCurrency]}
selectedTokenAddress={outputCurrency}
value={outputValueFormatted}
errorMessage={independentField === OUTPUT ? independentError : ''}
disableUnlock
/>
<OversizedPanel hideBottom>
<ExchangeRateWrapper
onClick={() => {
setInverted(inverted => !inverted)
}}
>
<ExchangeRate>{t('exchangeRate')}</ExchangeRate>
{inverted ? (
<span>
{exchangeRate
? `1 ${inputSymbol} = ${amountFormatter(exchangeRate, 18, 4, false)} ${outputSymbol}`
: ' - '}
</span>
) : (
<span>
{exchangeRate
? `1 ${outputSymbol} = ${amountFormatter(exchangeRateInverted, 18, 4, false)} ${inputSymbol}`
: ' - '}
</span>
)}
</ExchangeRateWrapper>
</OversizedPanel>
<TransactionDetails
account={account}
setRawSlippage={setRawSlippage}
setRawTokenSlippage={setRawTokenSlippage}
rawSlippage={rawSlippage}
slippageWarning={slippageWarning}
highSlippageWarning={highSlippageWarning}
inputError={inputError}
independentError={independentError}
inputCurrency={inputCurrency}
outputCurrency={outputCurrency}
independentValue={independentValue}
independentValueParsed={independentValueParsed}
independentField={independentField}
INPUT={INPUT}
inputValueParsed={inputValueParsed}
outputValueParsed={outputValueParsed}
inputSymbol={inputSymbol}
outputSymbol={outputSymbol}
dependentValueMinumum={dependentValueMinumum}
dependentValueMaximum={dependentValueMaximum}
dependentDecimals={dependentDecimals}
independentDecimals={independentDecimals}
percentSlippageFormatted={percentSlippageFormatted}
setcustomSlippageError={setcustomSlippageError}
/>
<Flex>
<Button
disabled={!isValid || customSlippageError === 'invalid'}
onClick={onSwap}
warning={highSlippageWarning || customSlippageError === 'warning'}
>
{highSlippageWarning || customSlippageError === 'warning' ? t('swapAnyway') : t('swap')}
</Button>
</Flex>
</>
)
return <ExchangePage initialCurrency={initialCurrency} />
}