Merge branch 'combine-swap-send' of https://github.com/ianlapham/uniswap-frontend into ianlapham-combine-swap-send
This commit is contained in:
commit
0235bedae5
26650
package-lock.json
generated
Normal file
26650
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
721
src/components/ExchangePage/index.jsx
Normal file
721
src/components/ExchangePage/index.jsx
Normal file
@ -0,0 +1,721 @@
|
||||
import React, { useState, useReducer, useEffect } from 'react'
|
||||
import ReactGA from 'react-ga'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useWeb3Context } from 'web3-react'
|
||||
import { ethers } from 'ethers'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { Button } from '../../theme'
|
||||
import CurrencyInputPanel from '../CurrencyInputPanel'
|
||||
import AddressInputPanel from '../AddressInputPanel'
|
||||
import OversizedPanel from '../OversizedPanel'
|
||||
import TransactionDetails from '../TransactionDetails'
|
||||
import ArrowDownBlue from '../../assets/images/arrow-down-blue.svg'
|
||||
import ArrowDownGrey from '../../assets/images/arrow-down-grey.svg'
|
||||
import { amountFormatter, calculateGasMargin } from '../../utils'
|
||||
import { useExchangeContract } from '../../hooks'
|
||||
import { useTokenDetails } from '../../contexts/Tokens'
|
||||
import { useTransactionAdder } from '../../contexts/Transactions'
|
||||
import { useAddressBalance, useExchangeReserves } from '../../contexts/Balances'
|
||||
import { useAddressAllowance } from '../../contexts/Allowances'
|
||||
|
||||
const INPUT = 0
|
||||
const OUTPUT = 1
|
||||
|
||||
const ETH_TO_TOKEN = 0
|
||||
const TOKEN_TO_ETH = 1
|
||||
const TOKEN_TO_TOKEN = 2
|
||||
|
||||
// denominated in bips
|
||||
const ALLOWED_SLIPPAGE_DEFAULT = 150
|
||||
const TOKEN_ALLOWED_SLIPPAGE_DEFAULT = 200
|
||||
|
||||
// denominated in seconds
|
||||
const DEADLINE_FROM_NOW = 60 * 15
|
||||
|
||||
// denominated in bips
|
||||
const GAS_MARGIN = ethers.utils.bigNumberify(1000)
|
||||
|
||||
const DownArrowBackground = styled.div`
|
||||
${({ theme }) => theme.flexRowNoWrap}
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const DownArrow = styled.img`
|
||||
width: 0.625rem;
|
||||
height: 0.625rem;
|
||||
position: relative;
|
||||
padding: 0.875rem;
|
||||
cursor: ${({ clickable }) => clickable && 'pointer'};
|
||||
`
|
||||
|
||||
const ExchangeRateWrapper = styled.div`
|
||||
${({ theme }) => theme.flexRowNoWrap};
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.doveGray};
|
||||
font-size: 0.75rem;
|
||||
padding: 0.5rem 1rem;
|
||||
`
|
||||
|
||||
const ExchangeRate = styled.span`
|
||||
flex: 1 1 auto;
|
||||
width: 0;
|
||||
color: ${({ theme }) => theme.chaliceGray};
|
||||
`
|
||||
|
||||
const Flex = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 2rem;
|
||||
|
||||
button {
|
||||
max-width: 20rem;
|
||||
}
|
||||
`
|
||||
|
||||
function calculateSlippageBounds(value, token = false, tokenAllowedSlippage, allowedSlippage) {
|
||||
if (value) {
|
||||
const offset = value.mul(token ? tokenAllowedSlippage : allowedSlippage).div(ethers.utils.bigNumberify(10000))
|
||||
const minimum = value.sub(offset)
|
||||
const maximum = value.add(offset)
|
||||
return {
|
||||
minimum: minimum.lt(ethers.constants.Zero) ? ethers.constants.Zero : minimum,
|
||||
maximum: maximum.gt(ethers.constants.MaxUint256) ? ethers.constants.MaxUint256 : maximum
|
||||
}
|
||||
} else {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
function getSwapType(inputCurrency, outputCurrency) {
|
||||
if (!inputCurrency || !outputCurrency) {
|
||||
return null
|
||||
} else if (inputCurrency === 'ETH') {
|
||||
return ETH_TO_TOKEN
|
||||
} else if (outputCurrency === 'ETH') {
|
||||
return TOKEN_TO_ETH
|
||||
} else {
|
||||
return TOKEN_TO_TOKEN
|
||||
}
|
||||
}
|
||||
|
||||
// this mocks the getInputPrice function, and calculates the required output
|
||||
function calculateEtherTokenOutputFromInput(inputAmount, inputReserve, outputReserve) {
|
||||
const inputAmountWithFee = inputAmount.mul(ethers.utils.bigNumberify(997))
|
||||
const numerator = inputAmountWithFee.mul(outputReserve)
|
||||
const denominator = inputReserve.mul(ethers.utils.bigNumberify(1000)).add(inputAmountWithFee)
|
||||
return numerator.div(denominator)
|
||||
}
|
||||
|
||||
// this mocks the getOutputPrice function, and calculates the required input
|
||||
function calculateEtherTokenInputFromOutput(outputAmount, inputReserve, outputReserve) {
|
||||
const numerator = inputReserve.mul(outputAmount).mul(ethers.utils.bigNumberify(1000))
|
||||
const denominator = outputReserve.sub(outputAmount).mul(ethers.utils.bigNumberify(997))
|
||||
return numerator.div(denominator).add(ethers.constants.One)
|
||||
}
|
||||
|
||||
function getInitialSwapState(outputCurrency) {
|
||||
return {
|
||||
independentValue: '', // this is a user input
|
||||
dependentValue: '', // this is a calculated number
|
||||
independentField: INPUT,
|
||||
inputCurrency: 'ETH',
|
||||
outputCurrency: outputCurrency ? outputCurrency : ''
|
||||
}
|
||||
}
|
||||
|
||||
function swapStateReducer(state, action) {
|
||||
switch (action.type) {
|
||||
case 'FLIP_INDEPENDENT': {
|
||||
const { independentField, inputCurrency, outputCurrency } = state
|
||||
return {
|
||||
...state,
|
||||
dependentValue: '',
|
||||
independentField: independentField === INPUT ? OUTPUT : INPUT,
|
||||
inputCurrency: outputCurrency,
|
||||
outputCurrency: inputCurrency
|
||||
}
|
||||
}
|
||||
case 'SELECT_CURRENCY': {
|
||||
const { inputCurrency, outputCurrency } = state
|
||||
const { field, currency } = action.payload
|
||||
|
||||
const newInputCurrency = field === INPUT ? currency : inputCurrency
|
||||
const newOutputCurrency = field === OUTPUT ? currency : outputCurrency
|
||||
|
||||
if (newInputCurrency === newOutputCurrency) {
|
||||
return {
|
||||
...state,
|
||||
inputCurrency: field === INPUT ? currency : '',
|
||||
outputCurrency: field === OUTPUT ? currency : ''
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
...state,
|
||||
inputCurrency: newInputCurrency,
|
||||
outputCurrency: newOutputCurrency
|
||||
}
|
||||
}
|
||||
}
|
||||
case 'UPDATE_INDEPENDENT': {
|
||||
const { field, value } = action.payload
|
||||
const { dependentValue, independentValue } = state
|
||||
return {
|
||||
...state,
|
||||
independentValue: value,
|
||||
dependentValue: value === independentValue ? dependentValue : '',
|
||||
independentField: field
|
||||
}
|
||||
}
|
||||
case 'UPDATE_DEPENDENT': {
|
||||
return {
|
||||
...state,
|
||||
dependentValue: action.payload
|
||||
}
|
||||
}
|
||||
default: {
|
||||
return getInitialSwapState()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getExchangeRate(inputValue, inputDecimals, outputValue, outputDecimals, invert = false) {
|
||||
try {
|
||||
if (
|
||||
inputValue &&
|
||||
(inputDecimals || inputDecimals === 0) &&
|
||||
outputValue &&
|
||||
(outputDecimals || outputDecimals === 0)
|
||||
) {
|
||||
const factor = ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(18))
|
||||
|
||||
if (invert) {
|
||||
return inputValue
|
||||
.mul(factor)
|
||||
.div(outputValue)
|
||||
.mul(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(outputDecimals)))
|
||||
.div(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(inputDecimals)))
|
||||
} else {
|
||||
return outputValue
|
||||
.mul(factor)
|
||||
.div(inputValue)
|
||||
.mul(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(inputDecimals)))
|
||||
.div(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(outputDecimals)))
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function getMarketRate(
|
||||
swapType,
|
||||
inputReserveETH,
|
||||
inputReserveToken,
|
||||
inputDecimals,
|
||||
outputReserveETH,
|
||||
outputReserveToken,
|
||||
outputDecimals,
|
||||
invert = false
|
||||
) {
|
||||
if (swapType === ETH_TO_TOKEN) {
|
||||
return getExchangeRate(outputReserveETH, 18, outputReserveToken, outputDecimals, invert)
|
||||
} else if (swapType === TOKEN_TO_ETH) {
|
||||
return getExchangeRate(inputReserveToken, inputDecimals, inputReserveETH, 18, invert)
|
||||
} else if (swapType === TOKEN_TO_TOKEN) {
|
||||
const factor = ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(18))
|
||||
const firstRate = getExchangeRate(inputReserveToken, inputDecimals, inputReserveETH, 18)
|
||||
const secondRate = getExchangeRate(outputReserveETH, 18, outputReserveToken, outputDecimals)
|
||||
try {
|
||||
return !!(firstRate && secondRate) ? firstRate.mul(secondRate).div(factor) : undefined
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
export default function ExchangePage({ initialCurrency, sending }) {
|
||||
const { t } = useTranslation()
|
||||
const { account } = useWeb3Context()
|
||||
|
||||
const addTransaction = useTransactionAdder()
|
||||
|
||||
const [rawSlippage, setRawSlippage] = useState(ALLOWED_SLIPPAGE_DEFAULT)
|
||||
const [rawTokenSlippage, setRawTokenSlippage] = useState(TOKEN_ALLOWED_SLIPPAGE_DEFAULT)
|
||||
|
||||
let allowedSlippageBig = ethers.utils.bigNumberify(rawSlippage)
|
||||
let tokenAllowedSlippageBig = ethers.utils.bigNumberify(rawTokenSlippage)
|
||||
|
||||
// analytics
|
||||
useEffect(() => {
|
||||
ReactGA.pageview(window.location.pathname + window.location.search)
|
||||
}, [])
|
||||
|
||||
// core swap state-
|
||||
const [swapState, dispatchSwapState] = useReducer(swapStateReducer, initialCurrency, getInitialSwapState)
|
||||
const { independentValue, dependentValue, independentField, inputCurrency, outputCurrency } = swapState
|
||||
|
||||
const [recipient, setRecipient] = useState({ address: '', name: '' })
|
||||
const [recipientError, setRecipientError] = useState()
|
||||
|
||||
// get swap type from the currency types
|
||||
const swapType = getSwapType(inputCurrency, outputCurrency)
|
||||
|
||||
// get decimals and exchange addressfor each of the currency types
|
||||
const { symbol: inputSymbol, decimals: inputDecimals, exchangeAddress: inputExchangeAddress } = useTokenDetails(
|
||||
inputCurrency
|
||||
)
|
||||
const { symbol: outputSymbol, decimals: outputDecimals, exchangeAddress: outputExchangeAddress } = useTokenDetails(
|
||||
outputCurrency
|
||||
)
|
||||
|
||||
const inputExchangeContract = useExchangeContract(inputExchangeAddress)
|
||||
const outputExchangeContract = useExchangeContract(outputExchangeAddress)
|
||||
const contract = swapType === ETH_TO_TOKEN ? outputExchangeContract : inputExchangeContract
|
||||
|
||||
// get input allowance
|
||||
const inputAllowance = useAddressAllowance(account, inputCurrency, inputExchangeAddress)
|
||||
|
||||
// fetch reserves for each of the currency types
|
||||
const { reserveETH: inputReserveETH, reserveToken: inputReserveToken } = useExchangeReserves(inputCurrency)
|
||||
const { reserveETH: outputReserveETH, reserveToken: outputReserveToken } = useExchangeReserves(outputCurrency)
|
||||
|
||||
// get balances for each of the currency types
|
||||
const inputBalance = useAddressBalance(account, inputCurrency)
|
||||
const outputBalance = useAddressBalance(account, outputCurrency)
|
||||
const inputBalanceFormatted = !!(inputBalance && Number.isInteger(inputDecimals))
|
||||
? amountFormatter(inputBalance, inputDecimals, Math.min(4, inputDecimals))
|
||||
: ''
|
||||
const outputBalanceFormatted = !!(outputBalance && Number.isInteger(outputDecimals))
|
||||
? amountFormatter(outputBalance, outputDecimals, Math.min(4, outputDecimals))
|
||||
: ''
|
||||
|
||||
// compute useful transforms of the data above
|
||||
const independentDecimals = independentField === INPUT ? inputDecimals : outputDecimals
|
||||
const dependentDecimals = independentField === OUTPUT ? inputDecimals : outputDecimals
|
||||
|
||||
// declare/get parsed and formatted versions of input/output values
|
||||
const [independentValueParsed, setIndependentValueParsed] = useState()
|
||||
const dependentValueFormatted = !!(dependentValue && (dependentDecimals || dependentDecimals === 0))
|
||||
? amountFormatter(dependentValue, dependentDecimals, Math.min(4, dependentDecimals), false)
|
||||
: ''
|
||||
const inputValueParsed = independentField === INPUT ? independentValueParsed : dependentValue
|
||||
const inputValueFormatted = independentField === INPUT ? independentValue : dependentValueFormatted
|
||||
const outputValueParsed = independentField === OUTPUT ? independentValueParsed : dependentValue
|
||||
const outputValueFormatted = independentField === OUTPUT ? independentValue : dependentValueFormatted
|
||||
|
||||
// validate + parse independent value
|
||||
const [independentError, setIndependentError] = useState()
|
||||
useEffect(() => {
|
||||
if (independentValue && (independentDecimals || independentDecimals === 0)) {
|
||||
try {
|
||||
const parsedValue = ethers.utils.parseUnits(independentValue, independentDecimals)
|
||||
|
||||
if (parsedValue.lte(ethers.constants.Zero) || parsedValue.gte(ethers.constants.MaxUint256)) {
|
||||
throw Error()
|
||||
} else {
|
||||
setIndependentValueParsed(parsedValue)
|
||||
setIndependentError(null)
|
||||
}
|
||||
} catch {
|
||||
setIndependentError(t('inputNotValid'))
|
||||
}
|
||||
|
||||
return () => {
|
||||
setIndependentValueParsed()
|
||||
setIndependentError()
|
||||
}
|
||||
}
|
||||
}, [independentValue, independentDecimals, t])
|
||||
|
||||
// calculate slippage from target rate
|
||||
const { minimum: dependentValueMinumum, maximum: dependentValueMaximum } = calculateSlippageBounds(
|
||||
dependentValue,
|
||||
swapType === TOKEN_TO_TOKEN,
|
||||
tokenAllowedSlippageBig,
|
||||
allowedSlippageBig
|
||||
)
|
||||
|
||||
// validate input allowance + balance
|
||||
const [inputError, setInputError] = useState()
|
||||
const [showUnlock, setShowUnlock] = useState(false)
|
||||
useEffect(() => {
|
||||
const inputValueCalculation = independentField === INPUT ? independentValueParsed : dependentValueMaximum
|
||||
if (inputBalance && (inputAllowance || inputCurrency === 'ETH') && inputValueCalculation) {
|
||||
if (inputBalance.lt(inputValueCalculation)) {
|
||||
setInputError(t('insufficientBalance'))
|
||||
} else if (inputCurrency !== 'ETH' && inputAllowance.lt(inputValueCalculation)) {
|
||||
setInputError(t('unlockTokenCont'))
|
||||
setShowUnlock(true)
|
||||
} else {
|
||||
setInputError(null)
|
||||
setShowUnlock(false)
|
||||
}
|
||||
|
||||
return () => {
|
||||
setInputError()
|
||||
setShowUnlock(false)
|
||||
}
|
||||
}
|
||||
}, [independentField, independentValueParsed, dependentValueMaximum, inputBalance, inputCurrency, inputAllowance, t])
|
||||
|
||||
// calculate dependent value
|
||||
useEffect(() => {
|
||||
const amount = independentValueParsed
|
||||
|
||||
if (swapType === ETH_TO_TOKEN) {
|
||||
const reserveETH = outputReserveETH
|
||||
const reserveToken = outputReserveToken
|
||||
|
||||
if (amount && reserveETH && reserveToken) {
|
||||
try {
|
||||
const calculatedDependentValue =
|
||||
independentField === INPUT
|
||||
? calculateEtherTokenOutputFromInput(amount, reserveETH, reserveToken)
|
||||
: calculateEtherTokenInputFromOutput(amount, reserveETH, reserveToken)
|
||||
|
||||
if (calculatedDependentValue.lte(ethers.constants.Zero)) {
|
||||
throw Error()
|
||||
}
|
||||
|
||||
dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: calculatedDependentValue })
|
||||
} catch {
|
||||
setIndependentError(t('insufficientLiquidity'))
|
||||
}
|
||||
return () => {
|
||||
dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: '' })
|
||||
}
|
||||
}
|
||||
} else if (swapType === TOKEN_TO_ETH) {
|
||||
const reserveETH = inputReserveETH
|
||||
const reserveToken = inputReserveToken
|
||||
|
||||
if (amount && reserveETH && reserveToken) {
|
||||
try {
|
||||
const calculatedDependentValue =
|
||||
independentField === INPUT
|
||||
? calculateEtherTokenOutputFromInput(amount, reserveToken, reserveETH)
|
||||
: calculateEtherTokenInputFromOutput(amount, reserveToken, reserveETH)
|
||||
|
||||
if (calculatedDependentValue.lte(ethers.constants.Zero)) {
|
||||
throw Error()
|
||||
}
|
||||
|
||||
dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: calculatedDependentValue })
|
||||
} catch {
|
||||
setIndependentError(t('insufficientLiquidity'))
|
||||
}
|
||||
return () => {
|
||||
dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: '' })
|
||||
}
|
||||
}
|
||||
} else if (swapType === TOKEN_TO_TOKEN) {
|
||||
const reserveETHFirst = inputReserveETH
|
||||
const reserveTokenFirst = inputReserveToken
|
||||
|
||||
const reserveETHSecond = outputReserveETH
|
||||
const reserveTokenSecond = outputReserveToken
|
||||
|
||||
if (amount && reserveETHFirst && reserveTokenFirst && reserveETHSecond && reserveTokenSecond) {
|
||||
try {
|
||||
if (independentField === INPUT) {
|
||||
const intermediateValue = calculateEtherTokenOutputFromInput(amount, reserveTokenFirst, reserveETHFirst)
|
||||
if (intermediateValue.lte(ethers.constants.Zero)) {
|
||||
throw Error()
|
||||
}
|
||||
const calculatedDependentValue = calculateEtherTokenOutputFromInput(
|
||||
intermediateValue,
|
||||
reserveETHSecond,
|
||||
reserveTokenSecond
|
||||
)
|
||||
if (calculatedDependentValue.lte(ethers.constants.Zero)) {
|
||||
throw Error()
|
||||
}
|
||||
dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: calculatedDependentValue })
|
||||
} else {
|
||||
const intermediateValue = calculateEtherTokenInputFromOutput(amount, reserveETHSecond, reserveTokenSecond)
|
||||
if (intermediateValue.lte(ethers.constants.Zero)) {
|
||||
throw Error()
|
||||
}
|
||||
const calculatedDependentValue = calculateEtherTokenInputFromOutput(
|
||||
intermediateValue,
|
||||
reserveTokenFirst,
|
||||
reserveETHFirst
|
||||
)
|
||||
if (calculatedDependentValue.lte(ethers.constants.Zero)) {
|
||||
throw Error()
|
||||
}
|
||||
dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: calculatedDependentValue })
|
||||
}
|
||||
} catch {
|
||||
setIndependentError(t('insufficientLiquidity'))
|
||||
}
|
||||
return () => {
|
||||
dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: '' })
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [
|
||||
independentValueParsed,
|
||||
swapType,
|
||||
outputReserveETH,
|
||||
outputReserveToken,
|
||||
inputReserveETH,
|
||||
inputReserveToken,
|
||||
independentField,
|
||||
t
|
||||
])
|
||||
|
||||
const [inverted, setInverted] = useState(false)
|
||||
const exchangeRate = getExchangeRate(inputValueParsed, inputDecimals, outputValueParsed, outputDecimals)
|
||||
const exchangeRateInverted = getExchangeRate(inputValueParsed, inputDecimals, outputValueParsed, outputDecimals, true)
|
||||
|
||||
const marketRate = getMarketRate(
|
||||
swapType,
|
||||
inputReserveETH,
|
||||
inputReserveToken,
|
||||
inputDecimals,
|
||||
outputReserveETH,
|
||||
outputReserveToken,
|
||||
outputDecimals
|
||||
)
|
||||
|
||||
const percentSlippage =
|
||||
exchangeRate && marketRate
|
||||
? exchangeRate
|
||||
.sub(marketRate)
|
||||
.abs()
|
||||
.mul(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(18)))
|
||||
.div(marketRate)
|
||||
.sub(ethers.utils.bigNumberify(3).mul(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(15))))
|
||||
: undefined
|
||||
const percentSlippageFormatted = percentSlippage && amountFormatter(percentSlippage, 16, 2)
|
||||
const slippageWarning =
|
||||
percentSlippage &&
|
||||
percentSlippage.gte(ethers.utils.parseEther('.05')) &&
|
||||
percentSlippage.lt(ethers.utils.parseEther('.2')) // [5% - 20%)
|
||||
const highSlippageWarning = percentSlippage && percentSlippage.gte(ethers.utils.parseEther('.2')) // [20+%
|
||||
|
||||
const isValid = sending
|
||||
? exchangeRate && inputError === null && independentError === null && recipientError === null
|
||||
: exchangeRate && inputError === null && independentError === null
|
||||
|
||||
const estimatedText = `(${t('estimated')})`
|
||||
function formatBalance(value) {
|
||||
return `Balance: ${value}`
|
||||
}
|
||||
|
||||
async function onSwap() {
|
||||
const deadline = Math.ceil(Date.now() / 1000) + DEADLINE_FROM_NOW
|
||||
|
||||
let estimate, method, args, value
|
||||
if (independentField === INPUT) {
|
||||
ReactGA.event({
|
||||
category: `${swapType}`,
|
||||
action: sending ? 'TransferInput' : 'SwapInput'
|
||||
})
|
||||
|
||||
if (swapType === ETH_TO_TOKEN) {
|
||||
estimate = sending ? contract.estimate.ethToTokenTransferInput : contract.estimate.ethToTokenSwapInput
|
||||
method = sending ? contract.ethToTokenTransferInput : contract.ethToTokenSwapInput
|
||||
args = sending ? [dependentValueMinumum, deadline, recipient.address] : [dependentValueMinumum, deadline]
|
||||
value = independentValueParsed
|
||||
} else if (swapType === TOKEN_TO_ETH) {
|
||||
estimate = sending ? contract.estimate.tokenToEthTransferInput : contract.estimate.tokenToEthSwapInput
|
||||
method = sending ? contract.tokenToEthTransferInput : contract.tokenToEthSwapInput
|
||||
args = sending
|
||||
? [independentValueParsed, dependentValueMinumum, deadline, recipient.address]
|
||||
: [independentValueParsed, dependentValueMinumum, deadline]
|
||||
value = ethers.constants.Zero
|
||||
} else if (swapType === TOKEN_TO_TOKEN) {
|
||||
estimate = sending ? contract.estimate.tokenToTokenTransferInput : contract.estimate.tokenToTokenSwapInput
|
||||
method = sending ? contract.tokenToTokenTransferInput : contract.tokenToTokenSwapInput
|
||||
args = sending
|
||||
? [
|
||||
independentValueParsed,
|
||||
dependentValueMinumum,
|
||||
ethers.constants.One,
|
||||
deadline,
|
||||
recipient.address,
|
||||
outputCurrency
|
||||
]
|
||||
: [independentValueParsed, dependentValueMinumum, ethers.constants.One, deadline, outputCurrency]
|
||||
value = ethers.constants.Zero
|
||||
}
|
||||
} else if (independentField === OUTPUT) {
|
||||
ReactGA.event({
|
||||
category: `${swapType}`,
|
||||
action: sending ? 'TransferOutput' : 'SwapOutput'
|
||||
})
|
||||
|
||||
if (swapType === ETH_TO_TOKEN) {
|
||||
estimate = sending ? contract.estimate.ethToTokenTransferOutput : contract.estimate.ethToTokenSwapOutput
|
||||
method = sending ? contract.ethToTokenTransferOutput : contract.ethToTokenSwapOutput
|
||||
args = sending ? [independentValueParsed, deadline, recipient.address] : [independentValueParsed, deadline]
|
||||
value = dependentValueMaximum
|
||||
} else if (swapType === TOKEN_TO_ETH) {
|
||||
estimate = sending ? contract.estimate.tokenToEthTransferOutput : contract.estimate.tokenToEthSwapOutput
|
||||
method = sending ? contract.tokenToEthTransferOutput : contract.tokenToEthSwapOutput
|
||||
args = sending
|
||||
? [independentValueParsed, dependentValueMaximum, deadline, recipient.address]
|
||||
: [independentValueParsed, dependentValueMaximum, deadline]
|
||||
value = ethers.constants.Zero
|
||||
} else if (swapType === TOKEN_TO_TOKEN) {
|
||||
estimate = sending ? contract.estimate.tokenToTokenTransferOutput : contract.estimate.tokenToTokenSwapOutput
|
||||
method = sending ? contract.tokenToTokenTransferOutput : contract.tokenToTokenSwapOutput
|
||||
args = sending
|
||||
? [
|
||||
independentValueParsed,
|
||||
dependentValueMaximum,
|
||||
ethers.constants.MaxUint256,
|
||||
deadline,
|
||||
recipient.address,
|
||||
outputCurrency
|
||||
]
|
||||
: [independentValueParsed, dependentValueMaximum, ethers.constants.MaxUint256, deadline, outputCurrency]
|
||||
value = ethers.constants.Zero
|
||||
}
|
||||
}
|
||||
|
||||
const estimatedGasLimit = await estimate(...args, { value })
|
||||
method(...args, { value, gasLimit: calculateGasMargin(estimatedGasLimit, GAS_MARGIN) }).then(response => {
|
||||
addTransaction(response)
|
||||
})
|
||||
}
|
||||
|
||||
const [customSlippageError, setcustomSlippageError] = useState('')
|
||||
|
||||
return (
|
||||
<>
|
||||
<CurrencyInputPanel
|
||||
title={t('input')}
|
||||
description={inputValueFormatted && independentField === OUTPUT ? estimatedText : ''}
|
||||
extraText={inputBalanceFormatted && formatBalance(inputBalanceFormatted)}
|
||||
extraTextClickHander={() => {
|
||||
if (inputBalance && inputDecimals) {
|
||||
const valueToSet = inputCurrency === 'ETH' ? inputBalance.sub(ethers.utils.parseEther('.1')) : inputBalance
|
||||
if (valueToSet.gt(ethers.constants.Zero)) {
|
||||
dispatchSwapState({
|
||||
type: 'UPDATE_INDEPENDENT',
|
||||
payload: { value: amountFormatter(valueToSet, inputDecimals, inputDecimals, false), field: INPUT }
|
||||
})
|
||||
}
|
||||
}
|
||||
}}
|
||||
onCurrencySelected={inputCurrency => {
|
||||
dispatchSwapState({ type: 'SELECT_CURRENCY', payload: { currency: inputCurrency, field: INPUT } })
|
||||
}}
|
||||
onValueChange={inputValue => {
|
||||
dispatchSwapState({ type: 'UPDATE_INDEPENDENT', payload: { value: inputValue, field: INPUT } })
|
||||
}}
|
||||
showUnlock={showUnlock}
|
||||
selectedTokens={[inputCurrency, outputCurrency]}
|
||||
selectedTokenAddress={inputCurrency}
|
||||
value={inputValueFormatted}
|
||||
errorMessage={inputError ? inputError : independentField === INPUT ? independentError : ''}
|
||||
/>
|
||||
<OversizedPanel>
|
||||
<DownArrowBackground>
|
||||
<DownArrow
|
||||
onClick={() => {
|
||||
dispatchSwapState({ type: 'FLIP_INDEPENDENT' })
|
||||
}}
|
||||
clickable
|
||||
alt="swap"
|
||||
src={isValid ? ArrowDownBlue : ArrowDownGrey}
|
||||
/>
|
||||
</DownArrowBackground>
|
||||
</OversizedPanel>
|
||||
<CurrencyInputPanel
|
||||
title={t('output')}
|
||||
description={outputValueFormatted && independentField === INPUT ? estimatedText : ''}
|
||||
extraText={outputBalanceFormatted && formatBalance(outputBalanceFormatted)}
|
||||
onCurrencySelected={outputCurrency => {
|
||||
dispatchSwapState({ type: 'SELECT_CURRENCY', payload: { currency: outputCurrency, field: OUTPUT } })
|
||||
}}
|
||||
onValueChange={outputValue => {
|
||||
dispatchSwapState({ type: 'UPDATE_INDEPENDENT', payload: { value: outputValue, field: OUTPUT } })
|
||||
}}
|
||||
selectedTokens={[inputCurrency, outputCurrency]}
|
||||
selectedTokenAddress={outputCurrency}
|
||||
value={outputValueFormatted}
|
||||
errorMessage={independentField === OUTPUT ? independentError : ''}
|
||||
disableUnlock
|
||||
/>
|
||||
{sending ? (
|
||||
<>
|
||||
<OversizedPanel>
|
||||
<DownArrowBackground>
|
||||
<DownArrow src={isValid ? ArrowDownBlue : ArrowDownGrey} alt="arrow" />
|
||||
</DownArrowBackground>
|
||||
</OversizedPanel>
|
||||
<AddressInputPanel onChange={setRecipient} onError={setRecipientError} />
|
||||
</>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
<OversizedPanel hideBottom>
|
||||
<ExchangeRateWrapper
|
||||
onClick={() => {
|
||||
setInverted(inverted => !inverted)
|
||||
}}
|
||||
>
|
||||
<ExchangeRate>{t('exchangeRate')}</ExchangeRate>
|
||||
{inverted ? (
|
||||
<span>
|
||||
{exchangeRate
|
||||
? `1 ${outputSymbol} = ${amountFormatter(exchangeRateInverted, 18, 4, false)} ${inputSymbol}`
|
||||
: ' - '}
|
||||
</span>
|
||||
) : (
|
||||
<span>
|
||||
{exchangeRate
|
||||
? `1 ${inputSymbol} = ${amountFormatter(exchangeRate, 18, 4, false)} ${outputSymbol}`
|
||||
: ' - '}
|
||||
</span>
|
||||
)}
|
||||
</ExchangeRateWrapper>
|
||||
</OversizedPanel>
|
||||
<TransactionDetails
|
||||
account={account}
|
||||
setRawSlippage={setRawSlippage}
|
||||
setRawTokenSlippage={setRawTokenSlippage}
|
||||
rawSlippage={rawSlippage}
|
||||
slippageWarning={slippageWarning}
|
||||
highSlippageWarning={highSlippageWarning}
|
||||
inputError={inputError}
|
||||
independentError={independentError}
|
||||
inputCurrency={inputCurrency}
|
||||
outputCurrency={outputCurrency}
|
||||
independentValue={independentValue}
|
||||
independentValueParsed={independentValueParsed}
|
||||
independentField={independentField}
|
||||
INPUT={INPUT}
|
||||
inputValueParsed={inputValueParsed}
|
||||
outputValueParsed={outputValueParsed}
|
||||
inputSymbol={inputSymbol}
|
||||
outputSymbol={outputSymbol}
|
||||
dependentValueMinumum={dependentValueMinumum}
|
||||
dependentValueMaximum={dependentValueMaximum}
|
||||
dependentDecimals={dependentDecimals}
|
||||
independentDecimals={independentDecimals}
|
||||
percentSlippageFormatted={percentSlippageFormatted}
|
||||
setcustomSlippageError={setcustomSlippageError}
|
||||
recipientAddress={recipient.address}
|
||||
sending={sending}
|
||||
/>
|
||||
<Flex>
|
||||
<Button
|
||||
disabled={!isValid || customSlippageError === 'invalid'}
|
||||
onClick={onSwap}
|
||||
warning={highSlippageWarning || customSlippageError === 'warning'}
|
||||
>
|
||||
{sending
|
||||
? highSlippageWarning || customSlippageError === 'warning'
|
||||
? t('sendAnyway')
|
||||
: t('send')
|
||||
: highSlippageWarning || customSlippageError === 'warning'
|
||||
? t('swapAnyway')
|
||||
: t('swap')}
|
||||
</Button>
|
||||
</Flex>
|
||||
</>
|
||||
)
|
||||
}
|
@ -1,8 +1,9 @@
|
||||
import React, { useState, useEffect, useRef } from 'react'
|
||||
import ReactGA from 'react-ga'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled, { css, keyframes } from 'styled-components'
|
||||
import { darken, lighten } from 'polished'
|
||||
import { amountFormatter } from '../../utils'
|
||||
import { isAddress, amountFormatter } from '../../utils'
|
||||
import { useDebounce } from '../../hooks'
|
||||
|
||||
import question from '../../assets/images/question.svg'
|
||||
@ -17,8 +18,6 @@ const WARNING_TYPE = Object.freeze({
|
||||
riskyEntryLow: 'riskyEntryLow'
|
||||
})
|
||||
|
||||
const b = text => <Bold>{text}</Bold>
|
||||
|
||||
const Flex = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@ -321,6 +320,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
|
||||
@ -338,7 +341,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}
|
||||
@ -545,9 +554,47 @@ export default function TransactionDetails(props) {
|
||||
props.setRawTokenSlippage(numParsed)
|
||||
}
|
||||
|
||||
const b = text => <Bold>{text}</Bold>
|
||||
|
||||
const renderTransactionDetails = () => {
|
||||
ReactGA.event({
|
||||
category: 'TransactionDetail',
|
||||
action: 'Open'
|
||||
})
|
||||
|
||||
if (props.independentField === props.INPUT) {
|
||||
return (
|
||||
return props.sending ? (
|
||||
<TransactionInfo>
|
||||
<div>
|
||||
{t('youAreSelling')}{' '}
|
||||
<ValueWrapper>
|
||||
{b(
|
||||
`${amountFormatter(
|
||||
props.independentValueParsed,
|
||||
props.independentDecimals,
|
||||
Math.min(4, props.independentDecimals)
|
||||
)} ${props.inputSymbol}`
|
||||
)}
|
||||
</ValueWrapper>
|
||||
.
|
||||
</div>
|
||||
<LastSummaryText>
|
||||
{b(props.recipientAddress)} {t('willReceive')}{' '}
|
||||
<ValueWrapper>
|
||||
{b(
|
||||
`${amountFormatter(
|
||||
props.dependentValueMinumum,
|
||||
props.dependentDecimals,
|
||||
Math.min(4, props.dependentDecimals)
|
||||
)} ${props.outputSymbol}`
|
||||
)}
|
||||
</ValueWrapper>{' '}
|
||||
</LastSummaryText>
|
||||
<LastSummaryText>
|
||||
{t('priceChange')} <ValueWrapper>{b(`${props.percentSlippageFormatted}%`)}</ValueWrapper>.
|
||||
</LastSummaryText>
|
||||
</TransactionInfo>
|
||||
) : (
|
||||
<TransactionInfo>
|
||||
<div>
|
||||
{t('youAreSelling')}{' '}
|
||||
@ -560,7 +607,7 @@ export default function TransactionDetails(props) {
|
||||
)} ${props.inputSymbol}`
|
||||
)}
|
||||
</ValueWrapper>{' '}
|
||||
{t('forAtLeast')}{' '}
|
||||
{t('forAtLeast')}
|
||||
<ValueWrapper>
|
||||
{b(
|
||||
`${amountFormatter(
|
||||
@ -573,12 +620,43 @@ export default function TransactionDetails(props) {
|
||||
.
|
||||
</div>
|
||||
<LastSummaryText>
|
||||
{t('priceChange')} <ValueWrapper>{b(`${props.percentSlippageFormatted}%`)}</ValueWrapper>
|
||||
{t('priceChange')} <ValueWrapper>{b(`${props.percentSlippageFormatted}%`)}</ValueWrapper>.
|
||||
</LastSummaryText>
|
||||
</TransactionInfo>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
return props.sending ? (
|
||||
<TransactionInfo>
|
||||
<div>
|
||||
{t('youAreSending')}{' '}
|
||||
<ValueWrapper>
|
||||
{b(
|
||||
`${amountFormatter(
|
||||
props.independentValueParsed,
|
||||
props.independentDecimals,
|
||||
Math.min(4, props.independentDecimals)
|
||||
)} ${props.outputSymbol}`
|
||||
)}
|
||||
</ValueWrapper>{' '}
|
||||
{t('to')} {b(props.recipientAddress)}.
|
||||
</div>
|
||||
<LastSummaryText>
|
||||
{t('itWillCost')}{' '}
|
||||
<ValueWrapper>
|
||||
{b(
|
||||
`${amountFormatter(
|
||||
props.dependentValueMaximum,
|
||||
props.dependentDecimals,
|
||||
Math.min(4, props.dependentDecimals)
|
||||
)} ${props.inputSymbol}`
|
||||
)}
|
||||
</ValueWrapper>{' '}
|
||||
</LastSummaryText>
|
||||
<LastSummaryText>
|
||||
{t('priceChange')} <ValueWrapper>{b(`${props.percentSlippageFormatted}%`)}</ValueWrapper>.
|
||||
</LastSummaryText>
|
||||
</TransactionInfo>
|
||||
) : (
|
||||
<TransactionInfo>
|
||||
<div>
|
||||
{t('youAreBuying')}{' '}
|
||||
@ -604,10 +682,9 @@ export default function TransactionDetails(props) {
|
||||
)} ${props.inputSymbol}`
|
||||
)}
|
||||
</ValueWrapper>{' '}
|
||||
{t('orTransFail')}
|
||||
</LastSummaryText>
|
||||
<LastSummaryText>
|
||||
{t('priceChange')} <ValueWrapper>{b(`${props.percentSlippageFormatted}%`)}</ValueWrapper>
|
||||
{t('priceChange')} <ValueWrapper>{b(`${props.percentSlippageFormatted}%`)}</ValueWrapper>.
|
||||
</LastSummaryText>
|
||||
</TransactionInfo>
|
||||
)
|
||||
|
@ -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