Merge branch 'combine-swap-send' of https://github.com/ianlapham/uniswap-frontend into ianlapham-combine-swap-send

This commit is contained in:
Noah Zinsmeister 2019-07-30 14:05:28 -04:00
commit 0235bedae5
No known key found for this signature in database
GPG Key ID: 83022DD49188C9F2
5 changed files with 27465 additions and 1461 deletions

26650
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

@ -0,0 +1,721 @@
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>
</>
)
}

@ -1,8 +1,9 @@
import React, { useState, useEffect, useRef } from 'react' import React, { useState, useEffect, useRef } from 'react'
import ReactGA from 'react-ga'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled, { css, keyframes } from 'styled-components' import styled, { css, keyframes } from 'styled-components'
import { darken, lighten } from 'polished' import { darken, lighten } from 'polished'
import { amountFormatter } from '../../utils' import { isAddress, amountFormatter } from '../../utils'
import { useDebounce } from '../../hooks' import { useDebounce } from '../../hooks'
import question from '../../assets/images/question.svg' import question from '../../assets/images/question.svg'
@ -17,8 +18,6 @@ const WARNING_TYPE = Object.freeze({
riskyEntryLow: 'riskyEntryLow' riskyEntryLow: 'riskyEntryLow'
}) })
const b = text => <Bold>{text}</Bold>
const Flex = styled.div` const Flex = styled.div`
display: flex; display: flex;
justify-content: center; justify-content: center;
@ -321,6 +320,10 @@ export default function TransactionDetails(props) {
contextualInfo = t('selectTokenCont') contextualInfo = t('selectTokenCont')
} else if (!props.independentValue) { } else if (!props.independentValue) {
contextualInfo = t('enterValueCont') 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) { } else if (!props.account) {
contextualInfo = t('noWallet') contextualInfo = t('noWallet')
isError = true isError = true
@ -338,7 +341,13 @@ export default function TransactionDetails(props) {
closeDetailsText={t('hideDetails')} closeDetailsText={t('hideDetails')}
contextualInfo={contextualInfo ? contextualInfo : slippageWarningText} contextualInfo={contextualInfo ? contextualInfo : slippageWarningText}
allowExpand={ allowExpand={
!!(props.inputCurrency && props.outputCurrency && props.inputValueParsed && props.outputValueParsed) !!(
props.inputCurrency &&
props.outputCurrency &&
props.inputValueParsed &&
props.outputValueParsed &&
(props.sending ? props.recipientAddress : true)
)
} }
isError={isError} isError={isError}
slippageWarning={props.slippageWarning && !contextualInfo} slippageWarning={props.slippageWarning && !contextualInfo}
@ -545,9 +554,47 @@ export default function TransactionDetails(props) {
props.setRawTokenSlippage(numParsed) props.setRawTokenSlippage(numParsed)
} }
const b = text => <Bold>{text}</Bold>
const renderTransactionDetails = () => { const renderTransactionDetails = () => {
ReactGA.event({
category: 'TransactionDetail',
action: 'Open'
})
if (props.independentField === props.INPUT) { if (props.independentField === props.INPUT) {
return ( return props.sending ? (
<TransactionInfo>
<div>
{t('youAreSelling')}{' '}
<ValueWrapper>
{b(
`${amountFormatter(
props.independentValueParsed,
props.independentDecimals,
Math.min(4, props.independentDecimals)
)} ${props.inputSymbol}`
)}
</ValueWrapper>
.
</div>
<LastSummaryText>
{b(props.recipientAddress)} {t('willReceive')}{' '}
<ValueWrapper>
{b(
`${amountFormatter(
props.dependentValueMinumum,
props.dependentDecimals,
Math.min(4, props.dependentDecimals)
)} ${props.outputSymbol}`
)}
</ValueWrapper>{' '}
</LastSummaryText>
<LastSummaryText>
{t('priceChange')} <ValueWrapper>{b(`${props.percentSlippageFormatted}%`)}</ValueWrapper>.
</LastSummaryText>
</TransactionInfo>
) : (
<TransactionInfo> <TransactionInfo>
<div> <div>
{t('youAreSelling')}{' '} {t('youAreSelling')}{' '}
@ -560,7 +607,7 @@ export default function TransactionDetails(props) {
)} ${props.inputSymbol}` )} ${props.inputSymbol}`
)} )}
</ValueWrapper>{' '} </ValueWrapper>{' '}
{t('forAtLeast')}{' '} {t('forAtLeast')}
<ValueWrapper> <ValueWrapper>
{b( {b(
`${amountFormatter( `${amountFormatter(
@ -573,12 +620,43 @@ export default function TransactionDetails(props) {
. .
</div> </div>
<LastSummaryText> <LastSummaryText>
{t('priceChange')} <ValueWrapper>{b(`${props.percentSlippageFormatted}%`)}</ValueWrapper> {t('priceChange')} <ValueWrapper>{b(`${props.percentSlippageFormatted}%`)}</ValueWrapper>.
</LastSummaryText> </LastSummaryText>
</TransactionInfo> </TransactionInfo>
) )
} else { } else {
return ( return props.sending ? (
<TransactionInfo>
<div>
{t('youAreSending')}{' '}
<ValueWrapper>
{b(
`${amountFormatter(
props.independentValueParsed,
props.independentDecimals,
Math.min(4, props.independentDecimals)
)} ${props.outputSymbol}`
)}
</ValueWrapper>{' '}
{t('to')} {b(props.recipientAddress)}.
</div>
<LastSummaryText>
{t('itWillCost')}{' '}
<ValueWrapper>
{b(
`${amountFormatter(
props.dependentValueMaximum,
props.dependentDecimals,
Math.min(4, props.dependentDecimals)
)} ${props.inputSymbol}`
)}
</ValueWrapper>{' '}
</LastSummaryText>
<LastSummaryText>
{t('priceChange')} <ValueWrapper>{b(`${props.percentSlippageFormatted}%`)}</ValueWrapper>.
</LastSummaryText>
</TransactionInfo>
) : (
<TransactionInfo> <TransactionInfo>
<div> <div>
{t('youAreBuying')}{' '} {t('youAreBuying')}{' '}
@ -604,10 +682,9 @@ export default function TransactionDetails(props) {
)} ${props.inputSymbol}` )} ${props.inputSymbol}`
)} )}
</ValueWrapper>{' '} </ValueWrapper>{' '}
{t('orTransFail')}
</LastSummaryText> </LastSummaryText>
<LastSummaryText> <LastSummaryText>
{t('priceChange')} <ValueWrapper>{b(`${props.percentSlippageFormatted}%`)}</ValueWrapper> {t('priceChange')} <ValueWrapper>{b(`${props.percentSlippageFormatted}%`)}</ValueWrapper>.
</LastSummaryText> </LastSummaryText>
</TransactionInfo> </TransactionInfo>
) )

@ -1,783 +1,6 @@
import React, { useState, useReducer, useEffect } from 'react' import React from 'react'
import ReactGA from 'react-ga' import ExchangePage from '../../components/ExchangePage'
import { useTranslation } from 'react-i18next'
import { useWeb3Context } from 'web3-react'
import { ethers } from 'ethers'
import styled from 'styled-components'
import { Button } from '../../theme' export default function Send({ initialCurrency }) {
import CurrencyInputPanel from '../../components/CurrencyInputPanel' return <ExchangePage initialCurrency={initialCurrency} sending={true} />
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>
</>
)
} }

@ -1,673 +1,6 @@
import React, { useState, useReducer, useEffect } from 'react' import React from 'react'
import ReactGA from 'react-ga' import ExchangePage from '../../components/ExchangePage'
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 {}
}
}
export default function Swap({ initialCurrency }) { export default function Swap({ initialCurrency }) {
const { t } = useTranslation() return <ExchangePage initialCurrency={initialCurrency} />
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>
</>
)
} }