resolve merge conflicts
This commit is contained in:
commit
3d452b70e4
@ -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;
|
||||
|
722
src/components/ExchangePage/index.jsx
Normal file
722
src/components/ExchangePage/index.jsx
Normal file
@ -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} />
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user