resolve merge conflicts
This commit is contained in:
commit
3d452b70e4
@ -32,7 +32,8 @@ const SummaryWrapperContainer = styled.div`
|
|||||||
|
|
||||||
const Details = styled.div`
|
const Details = styled.div`
|
||||||
background-color: ${({ theme }) => theme.concreteGray};
|
background-color: ${({ theme }) => theme.concreteGray};
|
||||||
/* padding: 1.25rem 1.25rem 1rem 1.25rem; */
|
padding: 1.5rem;
|
||||||
|
padding-bottom: 1rem;
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
margin: 1rem 0.5rem 0 0.5rem;
|
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'
|
import '@reach/dialog/styles.css'
|
||||||
|
|
||||||
const AnimatedDialogOverlay = animated(DialogOverlay)
|
const AnimatedDialogOverlay = animated(DialogOverlay)
|
||||||
const WrappedDialogOverlay = ({ suppressClassNameWarning, ...rest }) => <AnimatedDialogOverlay {...rest} />
|
const StyledDialogOverlay = styled(AnimatedDialogOverlay).attrs({
|
||||||
const StyledDialogOverlay = styled(WrappedDialogOverlay).attrs({
|
suppressclassnamewarning: 'true'
|
||||||
suppressClassNameWarning: true
|
|
||||||
})`
|
})`
|
||||||
&[data-reach-dialog-overlay] {
|
&[data-reach-dialog-overlay] {
|
||||||
z-index: 2;
|
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 { useTranslation } from 'react-i18next'
|
||||||
import styled, { css, keyframes } from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import { transparentize, darken } from 'polished'
|
import { isAddress, amountFormatter } from '../../utils'
|
||||||
import { amountFormatter } from '../../utils'
|
import questionMark from '../../assets/images/question-mark.svg'
|
||||||
import { useDebounce } from '../../hooks'
|
|
||||||
|
|
||||||
import question from '../../assets/images/question.svg'
|
|
||||||
|
|
||||||
import NewContextualInfo from '../../components/ContextualInfoNew'
|
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`
|
const Flex = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
button {
|
||||||
|
max-width: 20rem;
|
||||||
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const FlexBetween = styled.div`
|
const SlippageRow = styled(Flex)`
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
height: 100%;
|
|
||||||
`
|
|
||||||
|
|
||||||
const WrappedSlippageRow = ({ wrap, ...rest }) => <Flex {...rest} />
|
|
||||||
const SlippageRow = styled(WrappedSlippageRow)`
|
|
||||||
position: relative;
|
position: relative;
|
||||||
flex-wrap: ${({ wrap }) => wrap && 'wrap'};
|
width: 100%;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
font-size: 0.8rem;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
padding-top: ${({ wrap }) => wrap && '0.25rem'};
|
height: 24px;
|
||||||
|
margin-bottom: 14px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const QuestionWrapper = styled.button`
|
const QuestionWrapper = styled.div`
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
margin-left: 0.4rem;
|
margin-left: 0.4rem;
|
||||||
padding: 0.2rem;
|
margin-top: 0.2rem;
|
||||||
border: none;
|
|
||||||
background: none;
|
|
||||||
outline: none;
|
|
||||||
cursor: default;
|
|
||||||
border-radius: 36px;
|
|
||||||
|
|
||||||
:hover,
|
&:hover {
|
||||||
:focus {
|
cursor: pointer;
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const HelpCircleStyled = styled.img`
|
|
||||||
height: 18px;
|
|
||||||
width: 18px;
|
|
||||||
`
|
|
||||||
|
|
||||||
const fadeIn = keyframes`
|
|
||||||
from {
|
|
||||||
opacity : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
opacity : 1;
|
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
@ -83,135 +41,100 @@ const Popup = styled(Flex)`
|
|||||||
width: 228px;
|
width: 228px;
|
||||||
left: -78px;
|
left: -78px;
|
||||||
top: -124px;
|
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;
|
color: white;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
|
|
||||||
${({ theme }) => theme.mediaWidth.upToSmall`
|
|
||||||
left: -20px;
|
|
||||||
`}
|
|
||||||
`
|
`
|
||||||
|
|
||||||
const FancyButton = styled.button`
|
const Option = styled(Flex)`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
min-width: 55px;
|
min-width: 55px;
|
||||||
height: 2rem;
|
height: 24px;
|
||||||
|
margin-right: 4px;
|
||||||
border-radius: 36px;
|
border-radius: 36px;
|
||||||
border: 1px solid ${({ theme }) => theme.mercuryGray};
|
border: 1px solid #f2f2f2;
|
||||||
outline: none;
|
|
||||||
background: ${({ theme }) => theme.white};
|
|
||||||
|
|
||||||
:hover {
|
${({ active }) =>
|
||||||
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)`
|
&:hover {
|
||||||
width: 120px;
|
cursor: pointer;
|
||||||
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const Input = styled.input`
|
const Input = styled.input`
|
||||||
background: ${({ theme }) => theme.white};
|
width: 123.27px;
|
||||||
flex-grow: 1;
|
background: #ffffff;
|
||||||
|
height: 2rem;
|
||||||
outline: none;
|
outline: none;
|
||||||
|
margin-left: 20px;
|
||||||
|
border: 1px solid #f2f2f2;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
border-radius: 36px;
|
||||||
|
color: #aeaeae;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
}
|
||||||
|
|
||||||
|
text-align: left;
|
||||||
|
padding-left: 0.9rem;
|
||||||
|
|
||||||
&::-webkit-outer-spin-button,
|
&::-webkit-outer-spin-button,
|
||||||
&::-webkit-inner-spin-button {
|
&::-webkit-inner-spin-button {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
cursor: inherit;
|
|
||||||
|
|
||||||
color: ${({ theme }) => theme.doveGray};
|
|
||||||
text-align: left;
|
|
||||||
${({ active }) =>
|
${({ active }) =>
|
||||||
active &&
|
active &&
|
||||||
css`
|
`
|
||||||
color: initial;
|
border: 1px solid #2f80ed;
|
||||||
cursor: initial;
|
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
padding-right 1.5rem;
|
||||||
|
padding-left 0rem;
|
||||||
|
color : inherit;
|
||||||
`}
|
`}
|
||||||
|
|
||||||
${({ placeholder }) =>
|
${({ warning }) =>
|
||||||
placeholder !== 'Custom' &&
|
warning === 'red' &&
|
||||||
css`
|
`
|
||||||
text-align: right;
|
color : #FF6871;
|
||||||
`}
|
border: 1px solid #FF6871;
|
||||||
|
|
||||||
${({ color }) =>
|
|
||||||
color === 'red' &&
|
|
||||||
css`
|
|
||||||
color: ${({ theme }) => theme.salmonRed};
|
|
||||||
`}
|
`}
|
||||||
`
|
`
|
||||||
|
|
||||||
const BottomError = styled.div`
|
const BottomError = styled.div`
|
||||||
${({ show }) =>
|
margin-top: 1rem;
|
||||||
show &&
|
color: #aeaeae;
|
||||||
css`
|
|
||||||
padding-top: 12px;
|
|
||||||
`}
|
|
||||||
color: ${({ theme }) => theme.doveGray};
|
|
||||||
${({ color }) =>
|
${({ color }) =>
|
||||||
color === 'red' &&
|
color === 'red' &&
|
||||||
css`
|
`
|
||||||
color: ${({ theme }) => theme.salmonRed};
|
color : #FF6871;
|
||||||
`}
|
`}
|
||||||
`
|
`
|
||||||
|
|
||||||
const OptionCustom = styled(FancyButton)`
|
const Break = styled.div`
|
||||||
height: 2rem;
|
border: 1px solid #f2f2f2;
|
||||||
position: relative;
|
|
||||||
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%;
|
width: 100%;
|
||||||
height: 100%;
|
margin-top: 1rem;
|
||||||
border: 0px;
|
`
|
||||||
border-radius: 2rem;
|
|
||||||
}
|
const OptionLarge = styled(Option)`
|
||||||
|
width: 120px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const Bold = styled.span`
|
const Bold = styled.span`
|
||||||
@ -223,55 +146,44 @@ const LastSummaryText = styled.div`
|
|||||||
`
|
`
|
||||||
|
|
||||||
const SlippageSelector = styled.div`
|
const SlippageSelector = styled.div`
|
||||||
background-color: ${({ theme }) => darken(0.04, theme.concreteGray)};
|
margin-top: 1rem;
|
||||||
padding: 1rem 1.25rem 1rem 1.25rem;
|
`
|
||||||
border-radius: 12px;
|
|
||||||
|
const InputGroup = styled.div`
|
||||||
|
position: relative;
|
||||||
`
|
`
|
||||||
|
|
||||||
const Percent = styled.div`
|
const Percent = styled.div`
|
||||||
|
right: 14px;
|
||||||
|
top: 9px;
|
||||||
|
position: absolute;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
font-size: 0, 8rem;
|
font-size: 0, 8rem;
|
||||||
flex-grow: 0;
|
|
||||||
|
|
||||||
${({ color, theme }) =>
|
${({ color }) =>
|
||||||
(color === 'faded' &&
|
(color === 'faded' &&
|
||||||
css`
|
`
|
||||||
color: ${theme.doveGray};
|
color : #AEAEAE
|
||||||
`) ||
|
`) ||
|
||||||
(color === 'red' &&
|
(color === 'red' &&
|
||||||
css`
|
`
|
||||||
color: ${theme.salmonRed};
|
color : #FF6871
|
||||||
`)};
|
`)}
|
||||||
`
|
`
|
||||||
|
|
||||||
const Faded = styled.span`
|
const Faded = styled.span`
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
`
|
`
|
||||||
|
|
||||||
const TransactionInfo = styled.div`
|
const ErrorEmoji = styled.span`
|
||||||
padding: 1.25rem 1.25rem 1rem 1.25rem;
|
left: 30px;
|
||||||
|
top: 4px;
|
||||||
|
position: absolute;
|
||||||
`
|
`
|
||||||
|
|
||||||
export default function TransactionDetails(props) {
|
export default function TransactionDetails(props) {
|
||||||
const { t } = useTranslation()
|
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() {
|
function renderSummary() {
|
||||||
let contextualInfo = ''
|
let contextualInfo = ''
|
||||||
let isError = false
|
let isError = false
|
||||||
@ -283,6 +195,10 @@ export default function TransactionDetails(props) {
|
|||||||
contextualInfo = t('selectTokenCont')
|
contextualInfo = t('selectTokenCont')
|
||||||
} else if (!props.independentValue) {
|
} else if (!props.independentValue) {
|
||||||
contextualInfo = t('enterValueCont')
|
contextualInfo = t('enterValueCont')
|
||||||
|
} else if (props.sending && !props.recipientAddress) {
|
||||||
|
contextualInfo = t('noRecipient')
|
||||||
|
} else if (props.sending && !isAddress(props.recipientAddress)) {
|
||||||
|
contextualInfo = t('invalidRecipient')
|
||||||
} else if (!props.account) {
|
} else if (!props.account) {
|
||||||
contextualInfo = t('noWallet')
|
contextualInfo = t('noWallet')
|
||||||
isError = true
|
isError = true
|
||||||
@ -300,7 +216,13 @@ export default function TransactionDetails(props) {
|
|||||||
closeDetailsText={t('hideDetails')}
|
closeDetailsText={t('hideDetails')}
|
||||||
contextualInfo={contextualInfo ? contextualInfo : slippageWarningText}
|
contextualInfo={contextualInfo ? contextualInfo : slippageWarningText}
|
||||||
allowExpand={
|
allowExpand={
|
||||||
!!(props.inputCurrency && props.outputCurrency && props.inputValueParsed && props.outputValueParsed)
|
!!(
|
||||||
|
props.inputCurrency &&
|
||||||
|
props.outputCurrency &&
|
||||||
|
props.inputValueParsed &&
|
||||||
|
props.outputValueParsed &&
|
||||||
|
(props.sending ? props.recipientAddress : true)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
isError={isError}
|
isError={isError}
|
||||||
slippageWarning={props.slippageWarning && !contextualInfo}
|
slippageWarning={props.slippageWarning && !contextualInfo}
|
||||||
@ -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 = () => {
|
const dropDownContent = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{renderTransactionDetails()}
|
{renderTransactionDetails()}
|
||||||
|
<Break />
|
||||||
<SlippageSelector>
|
<SlippageSelector>
|
||||||
<SlippageRow>
|
<SlippageRow>
|
||||||
Limit additional price slippage
|
Limit addtional price slippage
|
||||||
<QuestionWrapper
|
<QuestionWrapper
|
||||||
onClick={() => {
|
|
||||||
setPopup(!showPopup)
|
|
||||||
}}
|
|
||||||
onMouseEnter={() => {
|
onMouseEnter={() => {
|
||||||
setPopup(true)
|
setPopup(true)
|
||||||
}}
|
}}
|
||||||
@ -329,114 +257,105 @@ export default function TransactionDetails(props) {
|
|||||||
setPopup(false)
|
setPopup(false)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<HelpCircleStyled src={question} alt="popup" />
|
<img src={questionMark} alt="question mark" />
|
||||||
</QuestionWrapper>
|
</QuestionWrapper>
|
||||||
{showPopup ? (
|
{showPopup ? (
|
||||||
<Popup>
|
<Popup>
|
||||||
Lowering this limit decreases your risk of frontrunning. However, this makes it more likely that your
|
Lowering this limit decreases your risk of frontrunning. This makes it more likely that your transaction
|
||||||
transaction will fail due to normal price movements.
|
will fail due to normal price movements.
|
||||||
</Popup>
|
</Popup>
|
||||||
) : (
|
) : (
|
||||||
''
|
''
|
||||||
)}
|
)}
|
||||||
</SlippageRow>
|
</SlippageRow>
|
||||||
<SlippageRow wrap>
|
<SlippageRow>
|
||||||
<Option
|
<Option
|
||||||
onClick={() => {
|
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%
|
0.1%
|
||||||
</Option>
|
</Option>
|
||||||
<Option
|
<Option
|
||||||
onClick={() => {
|
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>
|
</Option>
|
||||||
<OptionLarge
|
<OptionLarge
|
||||||
onClick={() => {
|
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>
|
</OptionLarge>
|
||||||
<OptionCustom
|
<InputGroup>
|
||||||
active={activeIndex === 4}
|
{warningType !== 'none' ? <ErrorEmoji>⚠️</ErrorEmoji> : ''}
|
||||||
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
|
<Input
|
||||||
tabIndex={-1}
|
placeholder={placeHolder}
|
||||||
ref={inputRef}
|
value={userInput || ''}
|
||||||
active={activeIndex === 4}
|
|
||||||
placeholder={
|
|
||||||
activeIndex === 4
|
|
||||||
? !!userInput
|
|
||||||
? ''
|
|
||||||
: '0'
|
|
||||||
: activeIndex !== 4 && userInput !== ''
|
|
||||||
? userInput
|
|
||||||
: 'Custom'
|
|
||||||
}
|
|
||||||
value={activeIndex === 4 ? userInput : ''}
|
|
||||||
onChange={parseInput}
|
onChange={parseInput}
|
||||||
color={
|
onClick={e => {
|
||||||
warningType === WARNING_TYPE.emptyInput
|
setActiveIndex(4)
|
||||||
|
setplaceHolder('')
|
||||||
|
parseInput(e)
|
||||||
|
}}
|
||||||
|
active={activeIndex === 4 ? true : false}
|
||||||
|
warning={
|
||||||
|
warningType === 'emptyInput'
|
||||||
? ''
|
? ''
|
||||||
: warningType !== WARNING_TYPE.none && warningType !== WARNING_TYPE.riskyEntryLow
|
: warningType !== 'none' && warningType !== 'riskyEntryLow'
|
||||||
? 'red'
|
? 'red'
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Percent
|
<Percent
|
||||||
color={
|
color={
|
||||||
activeIndex !== 4
|
warningType === 'emptyInput'
|
||||||
? 'faded'
|
? 'faded'
|
||||||
: warningType === WARNING_TYPE.riskyEntryHigh || warningType === WARNING_TYPE.invalidEntryBound
|
: warningType !== 'none' && warningType !== 'riskyEntryLow'
|
||||||
? 'red'
|
? 'red'
|
||||||
|
: activeIndex !== 4
|
||||||
|
? 'faded'
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
%
|
%
|
||||||
</Percent>
|
</Percent>
|
||||||
</FlexBetween>
|
</InputGroup>
|
||||||
</OptionCustom>
|
|
||||||
</SlippageRow>
|
</SlippageRow>
|
||||||
<SlippageRow>
|
<SlippageRow>
|
||||||
<BottomError
|
<BottomError
|
||||||
show={activeIndex === 4}
|
|
||||||
color={
|
color={
|
||||||
warningType === WARNING_TYPE.emptyInput
|
warningType === 'emptyInput'
|
||||||
? ''
|
? ''
|
||||||
: warningType !== WARNING_TYPE.none && warningType !== WARNING_TYPE.riskyEntryLow
|
: warningType !== 'none' && warningType !== 'riskyEntryLow'
|
||||||
? 'red'
|
? 'red'
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{activeIndex === 4 && warningType.toString() === 'none' && 'Custom slippage value entered'}
|
{warningType === 'emptyInput' ? 'Enter a slippage percentage.' : ''}
|
||||||
{warningType === WARNING_TYPE.emptyInput && 'Enter a slippage percentage.'}
|
{warningType === 'invalidEntry' ? 'Please input a valid percentage.' : ''}
|
||||||
{warningType === WARNING_TYPE.invalidEntryBound && 'Please select value less than 50%'}
|
{warningType === 'invalidEntryBound' ? 'Pleae select value less than 50%' : ''}
|
||||||
{warningType === WARNING_TYPE.riskyEntryHigh && 'Your transaction may be frontrun.'}
|
{warningType === 'riskyEntryHigh' ? 'Your transaction may be frontrun.' : ''}
|
||||||
{warningType === WARNING_TYPE.riskyEntryLow && 'Your transaction may fail.'}
|
{warningType === 'riskyEntryLow' ? 'Your transaction may fail.' : ''}
|
||||||
</BottomError>
|
</BottomError>
|
||||||
</SlippageRow>
|
</SlippageRow>
|
||||||
</SlippageSelector>
|
</SlippageSelector>
|
||||||
@ -444,73 +363,94 @@ export default function TransactionDetails(props) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const setFromCustom = () => {
|
const [userInput, setUserInput] = useState()
|
||||||
setActiveIndex(4)
|
|
||||||
inputRef.current.focus()
|
|
||||||
// if there's a value, evaluate the bounds
|
|
||||||
checkBounds(userInput)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 => {
|
const parseInput = e => {
|
||||||
let input = e.target.value
|
let input = e.target.value
|
||||||
|
if (input === '') {
|
||||||
// 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))) {
|
|
||||||
setUserInput(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 => {
|
const updateSlippage = newSlippage => {
|
||||||
// round to 2 decimals to prevent ethers error
|
|
||||||
let numParsed = parseFloat((newSlippage * 100).toFixed(2))
|
let numParsed = parseFloat((newSlippage * 100).toFixed(2))
|
||||||
|
|
||||||
// set both slippage values in parents
|
|
||||||
props.setRawSlippage(numParsed)
|
props.setRawSlippage(numParsed)
|
||||||
props.setRawTokenSlippage(numParsed)
|
props.setRawTokenSlippage(numParsed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const b = text => <Bold>{text}</Bold>
|
||||||
|
|
||||||
const renderTransactionDetails = () => {
|
const renderTransactionDetails = () => {
|
||||||
|
ReactGA.event({
|
||||||
|
category: 'TransactionDetail',
|
||||||
|
action: 'Open'
|
||||||
|
})
|
||||||
|
|
||||||
if (props.independentField === props.INPUT) {
|
if (props.independentField === props.INPUT) {
|
||||||
return (
|
return props.sending ? (
|
||||||
<TransactionInfo>
|
<div>
|
||||||
|
<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>
|
<div>
|
||||||
{t('youAreSelling')}{' '}
|
{t('youAreSelling')}{' '}
|
||||||
{b(
|
{b(
|
||||||
@ -533,11 +473,38 @@ export default function TransactionDetails(props) {
|
|||||||
<LastSummaryText>
|
<LastSummaryText>
|
||||||
{t('priceChange')} {b(`${props.percentSlippageFormatted}%`)}.
|
{t('priceChange')} {b(`${props.percentSlippageFormatted}%`)}.
|
||||||
</LastSummaryText>
|
</LastSummaryText>
|
||||||
</TransactionInfo>
|
</div>
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
return (
|
return props.sending ? (
|
||||||
<TransactionInfo>
|
<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>
|
<div>
|
||||||
{t('youAreBuying')}{' '}
|
{t('youAreBuying')}{' '}
|
||||||
{b(
|
{b(
|
||||||
@ -558,12 +525,11 @@ export default function TransactionDetails(props) {
|
|||||||
Math.min(4, props.dependentDecimals)
|
Math.min(4, props.dependentDecimals)
|
||||||
)} ${props.inputSymbol}`
|
)} ${props.inputSymbol}`
|
||||||
)}{' '}
|
)}{' '}
|
||||||
{t('orTransFail')}
|
|
||||||
</LastSummaryText>
|
</LastSummaryText>
|
||||||
<LastSummaryText>
|
<LastSummaryText>
|
||||||
{t('priceChange')} {b(`${props.percentSlippageFormatted}%`)}.
|
{t('priceChange')} {b(`${props.percentSlippageFormatted}%`)}.
|
||||||
</LastSummaryText>
|
</LastSummaryText>
|
||||||
</TransactionInfo>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -46,7 +46,7 @@ export default function App() {
|
|||||||
{/* this Suspense is for route code-splitting */}
|
{/* this Suspense is for route code-splitting */}
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route exact strict path="/swap" component={Swap} />
|
<Route exact strict path="/swap" component={() => <Swap />} />
|
||||||
<Route
|
<Route
|
||||||
exact
|
exact
|
||||||
strict
|
strict
|
||||||
@ -59,7 +59,7 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Route exact strict path="/send" component={Send} />
|
<Route exact strict path="/send" component={() => <Send />} />
|
||||||
<Route
|
<Route
|
||||||
exact
|
exact
|
||||||
strict
|
strict
|
||||||
|
@ -1,783 +1,6 @@
|
|||||||
import React, { useState, useReducer, useEffect } from 'react'
|
import React from 'react'
|
||||||
import ReactGA from 'react-ga'
|
import ExchangePage from '../../components/ExchangePage'
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { useWeb3Context } from 'web3-react'
|
|
||||||
import { ethers } from 'ethers'
|
|
||||||
import styled from 'styled-components'
|
|
||||||
|
|
||||||
import { Button } from '../../theme'
|
export default function Send({ initialCurrency }) {
|
||||||
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
|
return <ExchangePage initialCurrency={initialCurrency} sending={true} />
|
||||||
import NewContextualInfo from '../../components/ContextualInfoNew'
|
|
||||||
import OversizedPanel from '../../components/OversizedPanel'
|
|
||||||
import AddressInputPanel from '../../components/AddressInputPanel'
|
|
||||||
import ArrowDownBlue from '../../assets/images/arrow-down-blue.svg'
|
|
||||||
import ArrowDownGrey from '../../assets/images/arrow-down-grey.svg'
|
|
||||||
import { isAddress, amountFormatter, calculateGasMargin } from '../../utils'
|
|
||||||
import { useExchangeContract } from '../../hooks'
|
|
||||||
import { useTokenDetails } from '../../contexts/Tokens'
|
|
||||||
import { useTransactionAdder } from '../../contexts/Transactions'
|
|
||||||
import { useAddressBalance, useExchangeReserves } from '../../contexts/Balances'
|
|
||||||
import { useAddressAllowance } from '../../contexts/Allowances'
|
|
||||||
|
|
||||||
const INPUT = 0
|
|
||||||
const OUTPUT = 1
|
|
||||||
|
|
||||||
const ETH_TO_TOKEN = 0
|
|
||||||
const TOKEN_TO_ETH = 1
|
|
||||||
const TOKEN_TO_TOKEN = 2
|
|
||||||
|
|
||||||
// denominated in bips
|
|
||||||
const ALLOWED_SLIPPAGE = ethers.utils.bigNumberify(200)
|
|
||||||
const TOKEN_ALLOWED_SLIPPAGE = ethers.utils.bigNumberify(400)
|
|
||||||
|
|
||||||
// denominated in seconds
|
|
||||||
const DEADLINE_FROM_NOW = 60 * 15
|
|
||||||
|
|
||||||
// denominated in bips
|
|
||||||
const GAS_MARGIN = ethers.utils.bigNumberify(1000)
|
|
||||||
|
|
||||||
const BlueSpan = styled.span`
|
|
||||||
color: ${({ theme }) => theme.royalBlue};
|
|
||||||
`
|
|
||||||
|
|
||||||
const DownArrowBackground = styled.div`
|
|
||||||
${({ theme }) => theme.flexRowNoWrap}
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
`
|
|
||||||
|
|
||||||
const DownArrow = styled.img`
|
|
||||||
width: 0.625rem;
|
|
||||||
height: 0.625rem;
|
|
||||||
position: relative;
|
|
||||||
padding: 0.875rem;
|
|
||||||
cursor: ${({ clickable }) => clickable && 'pointer'};
|
|
||||||
`
|
|
||||||
|
|
||||||
const LastSummaryText = styled.div`
|
|
||||||
margin-top: 1rem;
|
|
||||||
`
|
|
||||||
|
|
||||||
const ExchangeRateWrapper = styled.div`
|
|
||||||
${({ theme }) => theme.flexRowNoWrap};
|
|
||||||
align-items: center;
|
|
||||||
color: ${({ theme }) => theme.doveGray};
|
|
||||||
font-size: 0.75rem;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
`
|
|
||||||
|
|
||||||
const ExchangeRate = styled.span`
|
|
||||||
flex: 1 1 auto;
|
|
||||||
width: 0;
|
|
||||||
color: ${({ theme }) => theme.chaliceGray};
|
|
||||||
`
|
|
||||||
|
|
||||||
const Flex = styled.div`
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 2rem;
|
|
||||||
|
|
||||||
button {
|
|
||||||
max-width: 20rem;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
function calculateSlippageBounds(value, token = false) {
|
|
||||||
if (value) {
|
|
||||||
const offset = value.mul(token ? TOKEN_ALLOWED_SLIPPAGE : ALLOWED_SLIPPAGE).div(ethers.utils.bigNumberify(10000))
|
|
||||||
const minimum = value.sub(offset)
|
|
||||||
const maximum = value.add(offset)
|
|
||||||
return {
|
|
||||||
minimum: minimum.lt(ethers.constants.Zero) ? ethers.constants.Zero : minimum,
|
|
||||||
maximum: maximum.gt(ethers.constants.MaxUint256) ? ethers.constants.MaxUint256 : maximum
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSwapType(inputCurrency, outputCurrency) {
|
|
||||||
if (!inputCurrency || !outputCurrency) {
|
|
||||||
return null
|
|
||||||
} else if (inputCurrency === 'ETH') {
|
|
||||||
return ETH_TO_TOKEN
|
|
||||||
} else if (outputCurrency === 'ETH') {
|
|
||||||
return TOKEN_TO_ETH
|
|
||||||
} else {
|
|
||||||
return TOKEN_TO_TOKEN
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// this mocks the getInputPrice function, and calculates the required output
|
|
||||||
function calculateEtherTokenOutputFromInput(inputAmount, inputReserve, outputReserve) {
|
|
||||||
const inputAmountWithFee = inputAmount.mul(ethers.utils.bigNumberify(997))
|
|
||||||
const numerator = inputAmountWithFee.mul(outputReserve)
|
|
||||||
const denominator = inputReserve.mul(ethers.utils.bigNumberify(1000)).add(inputAmountWithFee)
|
|
||||||
return numerator.div(denominator)
|
|
||||||
}
|
|
||||||
|
|
||||||
// this mocks the getOutputPrice function, and calculates the required input
|
|
||||||
function calculateEtherTokenInputFromOutput(outputAmount, inputReserve, outputReserve) {
|
|
||||||
const numerator = inputReserve.mul(outputAmount).mul(ethers.utils.bigNumberify(1000))
|
|
||||||
const denominator = outputReserve.sub(outputAmount).mul(ethers.utils.bigNumberify(997))
|
|
||||||
return numerator.div(denominator).add(ethers.constants.One)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getInitialSwapState(outputCurrency) {
|
|
||||||
return {
|
|
||||||
independentValue: '', // this is a user input
|
|
||||||
dependentValue: '', // this is a calculated number
|
|
||||||
independentField: INPUT,
|
|
||||||
inputCurrency: 'ETH',
|
|
||||||
outputCurrency: outputCurrency ? outputCurrency : ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function swapStateReducer(state, action) {
|
|
||||||
switch (action.type) {
|
|
||||||
case 'FLIP_INDEPENDENT': {
|
|
||||||
const { independentField, inputCurrency, outputCurrency } = state
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
dependentValue: '',
|
|
||||||
independentField: independentField === INPUT ? OUTPUT : INPUT,
|
|
||||||
inputCurrency: outputCurrency,
|
|
||||||
outputCurrency: inputCurrency
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case 'SELECT_CURRENCY': {
|
|
||||||
const { inputCurrency, outputCurrency } = state
|
|
||||||
const { field, currency } = action.payload
|
|
||||||
|
|
||||||
const newInputCurrency = field === INPUT ? currency : inputCurrency
|
|
||||||
const newOutputCurrency = field === OUTPUT ? currency : outputCurrency
|
|
||||||
|
|
||||||
if (newInputCurrency === newOutputCurrency) {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
inputCurrency: field === INPUT ? currency : '',
|
|
||||||
outputCurrency: field === OUTPUT ? currency : ''
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
inputCurrency: newInputCurrency,
|
|
||||||
outputCurrency: newOutputCurrency
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case 'UPDATE_INDEPENDENT': {
|
|
||||||
const { field, value } = action.payload
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
independentValue: value,
|
|
||||||
dependentValue: '',
|
|
||||||
independentField: field
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case 'UPDATE_DEPENDENT': {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
dependentValue: action.payload
|
|
||||||
}
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
return getInitialSwapState()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getExchangeRate(inputValue, inputDecimals, outputValue, outputDecimals, invert = false) {
|
|
||||||
try {
|
|
||||||
if (
|
|
||||||
inputValue &&
|
|
||||||
(inputDecimals || inputDecimals === 0) &&
|
|
||||||
outputValue &&
|
|
||||||
(outputDecimals || outputDecimals === 0)
|
|
||||||
) {
|
|
||||||
const factor = ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(18))
|
|
||||||
|
|
||||||
if (invert) {
|
|
||||||
return inputValue
|
|
||||||
.mul(factor)
|
|
||||||
.div(outputValue)
|
|
||||||
.mul(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(outputDecimals)))
|
|
||||||
.div(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(inputDecimals)))
|
|
||||||
} else {
|
|
||||||
return outputValue
|
|
||||||
.mul(factor)
|
|
||||||
.div(inputValue)
|
|
||||||
.mul(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(inputDecimals)))
|
|
||||||
.div(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(outputDecimals)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMarketRate(
|
|
||||||
swapType,
|
|
||||||
inputReserveETH,
|
|
||||||
inputReserveToken,
|
|
||||||
inputDecimals,
|
|
||||||
outputReserveETH,
|
|
||||||
outputReserveToken,
|
|
||||||
outputDecimals,
|
|
||||||
invert = false
|
|
||||||
) {
|
|
||||||
if (swapType === ETH_TO_TOKEN) {
|
|
||||||
return getExchangeRate(outputReserveETH, 18, outputReserveToken, outputDecimals, invert)
|
|
||||||
} else if (swapType === TOKEN_TO_ETH) {
|
|
||||||
return getExchangeRate(inputReserveToken, inputDecimals, inputReserveETH, 18, invert)
|
|
||||||
} else if (swapType === TOKEN_TO_TOKEN) {
|
|
||||||
const factor = ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(18))
|
|
||||||
const firstRate = getExchangeRate(inputReserveToken, inputDecimals, inputReserveETH, 18)
|
|
||||||
const secondRate = getExchangeRate(outputReserveETH, 18, outputReserveToken, outputDecimals)
|
|
||||||
try {
|
|
||||||
return !!(firstRate && secondRate) ? firstRate.mul(secondRate).div(factor) : undefined
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Swap({ initialCurrency }) {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
const { account } = useWeb3Context()
|
|
||||||
|
|
||||||
const addTransaction = useTransactionAdder()
|
|
||||||
|
|
||||||
// analytics
|
|
||||||
useEffect(() => {
|
|
||||||
ReactGA.pageview(window.location.pathname + window.location.search)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// core swap state
|
|
||||||
const [swapState, dispatchSwapState] = useReducer(swapStateReducer, initialCurrency, getInitialSwapState)
|
|
||||||
const { independentValue, dependentValue, independentField, inputCurrency, outputCurrency } = swapState
|
|
||||||
|
|
||||||
const [recipient, setRecipient] = useState({ address: '', name: '' })
|
|
||||||
const [recipientError, setRecipientError] = useState()
|
|
||||||
|
|
||||||
// get swap type from the currency types
|
|
||||||
const swapType = getSwapType(inputCurrency, outputCurrency)
|
|
||||||
|
|
||||||
// get decimals and exchange addressfor each of the currency types
|
|
||||||
const { symbol: inputSymbol, decimals: inputDecimals, exchangeAddress: inputExchangeAddress } = useTokenDetails(
|
|
||||||
inputCurrency
|
|
||||||
)
|
|
||||||
const { symbol: outputSymbol, decimals: outputDecimals, exchangeAddress: outputExchangeAddress } = useTokenDetails(
|
|
||||||
outputCurrency
|
|
||||||
)
|
|
||||||
|
|
||||||
const inputExchangeContract = useExchangeContract(inputExchangeAddress)
|
|
||||||
const outputExchangeContract = useExchangeContract(outputExchangeAddress)
|
|
||||||
const contract = swapType === ETH_TO_TOKEN ? outputExchangeContract : inputExchangeContract
|
|
||||||
|
|
||||||
// get input allowance
|
|
||||||
const inputAllowance = useAddressAllowance(account, inputCurrency, inputExchangeAddress)
|
|
||||||
|
|
||||||
// fetch reserves for each of the currency types
|
|
||||||
const { reserveETH: inputReserveETH, reserveToken: inputReserveToken } = useExchangeReserves(inputCurrency)
|
|
||||||
const { reserveETH: outputReserveETH, reserveToken: outputReserveToken } = useExchangeReserves(outputCurrency)
|
|
||||||
|
|
||||||
// get balances for each of the currency types
|
|
||||||
const inputBalance = useAddressBalance(account, inputCurrency)
|
|
||||||
const outputBalance = useAddressBalance(account, outputCurrency)
|
|
||||||
const inputBalanceFormatted = !!(inputBalance && Number.isInteger(inputDecimals))
|
|
||||||
? amountFormatter(inputBalance, inputDecimals, Math.min(4, inputDecimals))
|
|
||||||
: ''
|
|
||||||
const outputBalanceFormatted = !!(outputBalance && Number.isInteger(outputDecimals))
|
|
||||||
? amountFormatter(outputBalance, outputDecimals, Math.min(4, outputDecimals))
|
|
||||||
: ''
|
|
||||||
|
|
||||||
// compute useful transforms of the data above
|
|
||||||
const independentDecimals = independentField === INPUT ? inputDecimals : outputDecimals
|
|
||||||
const dependentDecimals = independentField === OUTPUT ? inputDecimals : outputDecimals
|
|
||||||
|
|
||||||
// declare/get parsed and formatted versions of input/output values
|
|
||||||
const [independentValueParsed, setIndependentValueParsed] = useState()
|
|
||||||
const dependentValueFormatted = !!(dependentValue && (dependentDecimals || dependentDecimals === 0))
|
|
||||||
? amountFormatter(dependentValue, dependentDecimals, Math.min(4, dependentDecimals), false)
|
|
||||||
: ''
|
|
||||||
const inputValueParsed = independentField === INPUT ? independentValueParsed : dependentValue
|
|
||||||
const inputValueFormatted = independentField === INPUT ? independentValue : dependentValueFormatted
|
|
||||||
const outputValueParsed = independentField === OUTPUT ? independentValueParsed : dependentValue
|
|
||||||
const outputValueFormatted = independentField === OUTPUT ? independentValue : dependentValueFormatted
|
|
||||||
|
|
||||||
// validate + parse independent value
|
|
||||||
const [independentError, setIndependentError] = useState()
|
|
||||||
useEffect(() => {
|
|
||||||
if (independentValue && (independentDecimals || independentDecimals === 0)) {
|
|
||||||
try {
|
|
||||||
const parsedValue = ethers.utils.parseUnits(independentValue, independentDecimals)
|
|
||||||
|
|
||||||
if (parsedValue.lte(ethers.constants.Zero) || parsedValue.gte(ethers.constants.MaxUint256)) {
|
|
||||||
throw Error()
|
|
||||||
} else {
|
|
||||||
setIndependentValueParsed(parsedValue)
|
|
||||||
setIndependentError(null)
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
setIndependentError(t('inputNotValid'))
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
setIndependentValueParsed()
|
|
||||||
setIndependentError()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [independentValue, independentDecimals, t])
|
|
||||||
|
|
||||||
// calculate slippage from target rate
|
|
||||||
const { minimum: dependentValueMinumum, maximum: dependentValueMaximum } = calculateSlippageBounds(
|
|
||||||
dependentValue,
|
|
||||||
swapType === TOKEN_TO_TOKEN
|
|
||||||
)
|
|
||||||
|
|
||||||
// validate input allowance + balance
|
|
||||||
const [inputError, setInputError] = useState()
|
|
||||||
const [showUnlock, setShowUnlock] = useState(false)
|
|
||||||
useEffect(() => {
|
|
||||||
const inputValueCalculation = independentField === INPUT ? independentValueParsed : dependentValueMaximum
|
|
||||||
|
|
||||||
if (inputBalance && (inputAllowance || inputCurrency === 'ETH') && inputValueCalculation) {
|
|
||||||
if (inputBalance.lt(inputValueCalculation)) {
|
|
||||||
setInputError(t('insufficientBalance'))
|
|
||||||
} else if (inputCurrency !== 'ETH' && inputAllowance.lt(inputValueCalculation)) {
|
|
||||||
setInputError(t('unlockTokenCont'))
|
|
||||||
setShowUnlock(true)
|
|
||||||
} else {
|
|
||||||
setInputError(null)
|
|
||||||
setShowUnlock(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
setInputError()
|
|
||||||
setShowUnlock(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [independentField, independentValueParsed, dependentValueMaximum, inputBalance, inputCurrency, inputAllowance, t])
|
|
||||||
|
|
||||||
// calculate dependent value
|
|
||||||
useEffect(() => {
|
|
||||||
const amount = independentValueParsed
|
|
||||||
|
|
||||||
if (swapType === ETH_TO_TOKEN) {
|
|
||||||
const reserveETH = outputReserveETH
|
|
||||||
const reserveToken = outputReserveToken
|
|
||||||
|
|
||||||
if (amount && reserveETH && reserveToken) {
|
|
||||||
try {
|
|
||||||
const calculatedDependentValue =
|
|
||||||
independentField === INPUT
|
|
||||||
? calculateEtherTokenOutputFromInput(amount, reserveETH, reserveToken)
|
|
||||||
: calculateEtherTokenInputFromOutput(amount, reserveETH, reserveToken)
|
|
||||||
|
|
||||||
if (calculatedDependentValue.lte(ethers.constants.Zero)) {
|
|
||||||
throw Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: calculatedDependentValue })
|
|
||||||
} catch {
|
|
||||||
setIndependentError(t('insufficientLiquidity'))
|
|
||||||
}
|
|
||||||
return () => {
|
|
||||||
dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: '' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (swapType === TOKEN_TO_ETH) {
|
|
||||||
const reserveETH = inputReserveETH
|
|
||||||
const reserveToken = inputReserveToken
|
|
||||||
|
|
||||||
if (amount && reserveETH && reserveToken) {
|
|
||||||
try {
|
|
||||||
const calculatedDependentValue =
|
|
||||||
independentField === INPUT
|
|
||||||
? calculateEtherTokenOutputFromInput(amount, reserveToken, reserveETH)
|
|
||||||
: calculateEtherTokenInputFromOutput(amount, reserveToken, reserveETH)
|
|
||||||
|
|
||||||
if (calculatedDependentValue.lte(ethers.constants.Zero)) {
|
|
||||||
throw Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: calculatedDependentValue })
|
|
||||||
} catch {
|
|
||||||
setIndependentError(t('insufficientLiquidity'))
|
|
||||||
}
|
|
||||||
return () => {
|
|
||||||
dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: '' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (swapType === TOKEN_TO_TOKEN) {
|
|
||||||
const reserveETHFirst = inputReserveETH
|
|
||||||
const reserveTokenFirst = inputReserveToken
|
|
||||||
|
|
||||||
const reserveETHSecond = outputReserveETH
|
|
||||||
const reserveTokenSecond = outputReserveToken
|
|
||||||
|
|
||||||
if (amount && reserveETHFirst && reserveTokenFirst && reserveETHSecond && reserveTokenSecond) {
|
|
||||||
try {
|
|
||||||
if (independentField === INPUT) {
|
|
||||||
const intermediateValue = calculateEtherTokenOutputFromInput(amount, reserveTokenFirst, reserveETHFirst)
|
|
||||||
if (intermediateValue.lte(ethers.constants.Zero)) {
|
|
||||||
throw Error()
|
|
||||||
}
|
|
||||||
const calculatedDependentValue = calculateEtherTokenOutputFromInput(
|
|
||||||
intermediateValue,
|
|
||||||
reserveETHSecond,
|
|
||||||
reserveTokenSecond
|
|
||||||
)
|
|
||||||
if (calculatedDependentValue.lte(ethers.constants.Zero)) {
|
|
||||||
throw Error()
|
|
||||||
}
|
|
||||||
dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: calculatedDependentValue })
|
|
||||||
} else {
|
|
||||||
const intermediateValue = calculateEtherTokenInputFromOutput(amount, reserveETHSecond, reserveTokenSecond)
|
|
||||||
if (intermediateValue.lte(ethers.constants.Zero)) {
|
|
||||||
throw Error()
|
|
||||||
}
|
|
||||||
// console.log('hi!', amountFormatter(intermediateValue, ))
|
|
||||||
const calculatedDependentValue = calculateEtherTokenInputFromOutput(
|
|
||||||
intermediateValue,
|
|
||||||
reserveTokenFirst,
|
|
||||||
reserveETHFirst
|
|
||||||
)
|
|
||||||
if (calculatedDependentValue.lte(ethers.constants.Zero)) {
|
|
||||||
throw Error()
|
|
||||||
}
|
|
||||||
dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: calculatedDependentValue })
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
setIndependentError(t('insufficientLiquidity'))
|
|
||||||
}
|
|
||||||
return () => {
|
|
||||||
dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: '' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
independentValueParsed,
|
|
||||||
swapType,
|
|
||||||
outputReserveETH,
|
|
||||||
outputReserveToken,
|
|
||||||
inputReserveETH,
|
|
||||||
inputReserveToken,
|
|
||||||
independentField,
|
|
||||||
t
|
|
||||||
])
|
|
||||||
|
|
||||||
const [inverted, setInverted] = useState(false)
|
|
||||||
const exchangeRate = getExchangeRate(inputValueParsed, inputDecimals, outputValueParsed, outputDecimals)
|
|
||||||
const exchangeRateInverted = getExchangeRate(inputValueParsed, inputDecimals, outputValueParsed, outputDecimals, true)
|
|
||||||
|
|
||||||
const marketRate = getMarketRate(
|
|
||||||
swapType,
|
|
||||||
inputReserveETH,
|
|
||||||
inputReserveToken,
|
|
||||||
inputDecimals,
|
|
||||||
outputReserveETH,
|
|
||||||
outputReserveToken,
|
|
||||||
outputDecimals
|
|
||||||
)
|
|
||||||
|
|
||||||
const percentSlippage =
|
|
||||||
exchangeRate && marketRate
|
|
||||||
? exchangeRate
|
|
||||||
.sub(marketRate)
|
|
||||||
.abs()
|
|
||||||
.mul(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(18)))
|
|
||||||
.div(marketRate)
|
|
||||||
.sub(ethers.utils.bigNumberify(3).mul(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(15))))
|
|
||||||
: undefined
|
|
||||||
const percentSlippageFormatted = percentSlippage && amountFormatter(percentSlippage, 16, 2)
|
|
||||||
const slippageWarning =
|
|
||||||
percentSlippage &&
|
|
||||||
percentSlippage.gte(ethers.utils.parseEther('.05')) &&
|
|
||||||
percentSlippage.lt(ethers.utils.parseEther('.2')) // [5% - 20%)
|
|
||||||
const highSlippageWarning = percentSlippage && percentSlippage.gte(ethers.utils.parseEther('.2')) // [20+%
|
|
||||||
|
|
||||||
const isValid = exchangeRate && inputError === null && independentError === null && recipientError === null
|
|
||||||
|
|
||||||
const estimatedText = `(${t('estimated')})`
|
|
||||||
function formatBalance(value) {
|
|
||||||
return `Balance: ${value}`
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderTransactionDetails() {
|
|
||||||
ReactGA.event({
|
|
||||||
category: 'TransactionDetail',
|
|
||||||
action: 'Open'
|
|
||||||
})
|
|
||||||
|
|
||||||
const b = text => <BlueSpan>{text}</BlueSpan>
|
|
||||||
|
|
||||||
if (independentField === INPUT) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
{t('youAreSelling')}{' '}
|
|
||||||
{b(
|
|
||||||
`${amountFormatter(
|
|
||||||
independentValueParsed,
|
|
||||||
independentDecimals,
|
|
||||||
Math.min(4, independentDecimals)
|
|
||||||
)} ${inputSymbol}`
|
|
||||||
)}
|
|
||||||
.
|
|
||||||
</div>
|
|
||||||
<LastSummaryText>
|
|
||||||
{b(recipient.address)} {t('willReceive')}{' '}
|
|
||||||
{b(
|
|
||||||
`${amountFormatter(
|
|
||||||
dependentValueMinumum,
|
|
||||||
dependentDecimals,
|
|
||||||
Math.min(4, dependentDecimals)
|
|
||||||
)} ${outputSymbol}`
|
|
||||||
)}{' '}
|
|
||||||
{t('orTransFail')}
|
|
||||||
</LastSummaryText>
|
|
||||||
<LastSummaryText>
|
|
||||||
{(slippageWarning || highSlippageWarning) && (
|
|
||||||
<span role="img" aria-label="warning">
|
|
||||||
⚠️
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{t('priceChange')} {b(`${percentSlippageFormatted}%`)}.
|
|
||||||
</LastSummaryText>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
{t('youAreSending')}{' '}
|
|
||||||
{b(
|
|
||||||
`${amountFormatter(
|
|
||||||
independentValueParsed,
|
|
||||||
independentDecimals,
|
|
||||||
Math.min(4, independentDecimals)
|
|
||||||
)} ${outputSymbol}`
|
|
||||||
)}{' '}
|
|
||||||
{t('to')} {b(recipient.address)}.
|
|
||||||
</div>
|
|
||||||
<LastSummaryText>
|
|
||||||
{t('itWillCost')}{' '}
|
|
||||||
{b(
|
|
||||||
`${amountFormatter(
|
|
||||||
dependentValueMaximum,
|
|
||||||
dependentDecimals,
|
|
||||||
Math.min(4, dependentDecimals)
|
|
||||||
)} ${inputSymbol}`
|
|
||||||
)}{' '}
|
|
||||||
{t('orTransFail')}
|
|
||||||
</LastSummaryText>
|
|
||||||
<LastSummaryText>
|
|
||||||
{t('priceChange')} {b(`${percentSlippageFormatted}%`)}.
|
|
||||||
</LastSummaryText>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderSummary() {
|
|
||||||
let contextualInfo = ''
|
|
||||||
let isError = false
|
|
||||||
|
|
||||||
if (inputError || independentError) {
|
|
||||||
contextualInfo = inputError || independentError
|
|
||||||
isError = true
|
|
||||||
} else if (!inputCurrency || !outputCurrency) {
|
|
||||||
contextualInfo = t('selectTokenCont')
|
|
||||||
} else if (!independentValue) {
|
|
||||||
contextualInfo = t('enterValueCont')
|
|
||||||
} else if (!recipient.address) {
|
|
||||||
contextualInfo = t('noRecipient')
|
|
||||||
} else if (!isAddress(recipient.address)) {
|
|
||||||
contextualInfo = t('invalidRecipient')
|
|
||||||
} else if (!account) {
|
|
||||||
contextualInfo = t('noWallet')
|
|
||||||
isError = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const slippageWarningText = highSlippageWarning
|
|
||||||
? t('highSlippageWarning')
|
|
||||||
: slippageWarning
|
|
||||||
? t('slippageWarning')
|
|
||||||
: ''
|
|
||||||
|
|
||||||
return (
|
|
||||||
<NewContextualInfo
|
|
||||||
openDetailsText={t('transactionDetails')}
|
|
||||||
closeDetailsText={t('hideDetails')}
|
|
||||||
contextualInfo={contextualInfo ? contextualInfo : slippageWarningText}
|
|
||||||
allowExpand={!!(inputCurrency && outputCurrency && inputValueParsed && outputValueParsed && recipient.address)}
|
|
||||||
isError={isError}
|
|
||||||
slippageWarning={slippageWarning && slippageWarningText}
|
|
||||||
highSlippageWarning={highSlippageWarning && slippageWarningText}
|
|
||||||
dropDownContent={renderTransactionDetails}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onSwap() {
|
|
||||||
const deadline = Math.ceil(Date.now() / 1000) + DEADLINE_FROM_NOW
|
|
||||||
|
|
||||||
let estimate, method, args, value
|
|
||||||
if (independentField === INPUT) {
|
|
||||||
ReactGA.event({
|
|
||||||
category: `${swapType}`,
|
|
||||||
action: 'TransferInput'
|
|
||||||
})
|
|
||||||
|
|
||||||
if (swapType === ETH_TO_TOKEN) {
|
|
||||||
estimate = contract.estimate.ethToTokenTransferInput
|
|
||||||
method = contract.ethToTokenTransferInput
|
|
||||||
args = [dependentValueMinumum, deadline, recipient.address]
|
|
||||||
value = independentValueParsed
|
|
||||||
} else if (swapType === TOKEN_TO_ETH) {
|
|
||||||
estimate = contract.estimate.tokenToEthTransferInput
|
|
||||||
method = contract.tokenToEthTransferInput
|
|
||||||
args = [independentValueParsed, dependentValueMinumum, deadline, recipient.address]
|
|
||||||
value = ethers.constants.Zero
|
|
||||||
} else if (swapType === TOKEN_TO_TOKEN) {
|
|
||||||
estimate = contract.estimate.tokenToTokenTransferInput
|
|
||||||
method = contract.tokenToTokenTransferInput
|
|
||||||
args = [
|
|
||||||
independentValueParsed,
|
|
||||||
dependentValueMinumum,
|
|
||||||
ethers.constants.One,
|
|
||||||
deadline,
|
|
||||||
recipient.address,
|
|
||||||
outputCurrency
|
|
||||||
]
|
|
||||||
value = ethers.constants.Zero
|
|
||||||
}
|
|
||||||
} else if (independentField === OUTPUT) {
|
|
||||||
ReactGA.event({
|
|
||||||
category: `${swapType}`,
|
|
||||||
action: 'TransferOutput'
|
|
||||||
})
|
|
||||||
|
|
||||||
if (swapType === ETH_TO_TOKEN) {
|
|
||||||
estimate = contract.estimate.ethToTokenTransferOutput
|
|
||||||
method = contract.ethToTokenTransferOutput
|
|
||||||
args = [independentValueParsed, deadline, recipient.address]
|
|
||||||
value = dependentValueMaximum
|
|
||||||
} else if (swapType === TOKEN_TO_ETH) {
|
|
||||||
estimate = contract.estimate.tokenToEthTransferOutput
|
|
||||||
method = contract.tokenToEthTransferOutput
|
|
||||||
args = [independentValueParsed, dependentValueMaximum, deadline, recipient.address]
|
|
||||||
value = ethers.constants.Zero
|
|
||||||
} else if (swapType === TOKEN_TO_TOKEN) {
|
|
||||||
estimate = contract.estimate.tokenToTokenTransferOutput
|
|
||||||
method = contract.tokenToTokenTransferOutput
|
|
||||||
args = [
|
|
||||||
independentValueParsed,
|
|
||||||
dependentValueMaximum,
|
|
||||||
ethers.constants.MaxUint256,
|
|
||||||
deadline,
|
|
||||||
recipient.address,
|
|
||||||
outputCurrency
|
|
||||||
]
|
|
||||||
value = ethers.constants.Zero
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const estimatedGasLimit = await estimate(...args, { value })
|
|
||||||
method(...args, { value, gasLimit: calculateGasMargin(estimatedGasLimit, GAS_MARGIN) }).then(response => {
|
|
||||||
addTransaction(response)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<CurrencyInputPanel
|
|
||||||
title={t('input')}
|
|
||||||
description={inputValueFormatted && independentField === OUTPUT ? estimatedText : ''}
|
|
||||||
extraText={inputBalanceFormatted && formatBalance(inputBalanceFormatted)}
|
|
||||||
extraTextClickHander={() => {
|
|
||||||
if (inputBalance && inputDecimals) {
|
|
||||||
const valueToSet = inputCurrency === 'ETH' ? inputBalance.sub(ethers.utils.parseEther('.1')) : inputBalance
|
|
||||||
if (valueToSet.gt(ethers.constants.Zero)) {
|
|
||||||
dispatchSwapState({
|
|
||||||
type: 'UPDATE_INDEPENDENT',
|
|
||||||
payload: { value: amountFormatter(valueToSet, inputDecimals, inputDecimals, false), field: INPUT }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onCurrencySelected={inputCurrency => {
|
|
||||||
dispatchSwapState({ type: 'SELECT_CURRENCY', payload: { currency: inputCurrency, field: INPUT } })
|
|
||||||
}}
|
|
||||||
onValueChange={inputValue => {
|
|
||||||
dispatchSwapState({ type: 'UPDATE_INDEPENDENT', payload: { value: inputValue, field: INPUT } })
|
|
||||||
}}
|
|
||||||
showUnlock={showUnlock}
|
|
||||||
selectedTokens={[inputCurrency, outputCurrency]}
|
|
||||||
selectedTokenAddress={inputCurrency}
|
|
||||||
value={inputValueFormatted}
|
|
||||||
errorMessage={inputError ? inputError : independentField === INPUT ? independentError : ''}
|
|
||||||
/>
|
|
||||||
<OversizedPanel>
|
|
||||||
<DownArrowBackground>
|
|
||||||
<DownArrow
|
|
||||||
onClick={() => {
|
|
||||||
dispatchSwapState({ type: 'FLIP_INDEPENDENT' })
|
|
||||||
}}
|
|
||||||
clickable
|
|
||||||
alt="swap"
|
|
||||||
src={isValid ? ArrowDownBlue : ArrowDownGrey}
|
|
||||||
/>
|
|
||||||
</DownArrowBackground>
|
|
||||||
</OversizedPanel>
|
|
||||||
<CurrencyInputPanel
|
|
||||||
title={t('output')}
|
|
||||||
description={outputValueFormatted && independentField === INPUT ? estimatedText : ''}
|
|
||||||
extraText={outputBalanceFormatted && formatBalance(outputBalanceFormatted)}
|
|
||||||
onCurrencySelected={outputCurrency => {
|
|
||||||
dispatchSwapState({ type: 'SELECT_CURRENCY', payload: { currency: outputCurrency, field: OUTPUT } })
|
|
||||||
}}
|
|
||||||
onValueChange={outputValue => {
|
|
||||||
dispatchSwapState({ type: 'UPDATE_INDEPENDENT', payload: { value: outputValue, field: OUTPUT } })
|
|
||||||
}}
|
|
||||||
selectedTokens={[inputCurrency, outputCurrency]}
|
|
||||||
selectedTokenAddress={outputCurrency}
|
|
||||||
value={outputValueFormatted}
|
|
||||||
errorMessage={independentField === OUTPUT ? independentError : ''}
|
|
||||||
disableUnlock
|
|
||||||
/>
|
|
||||||
<OversizedPanel>
|
|
||||||
<DownArrowBackground>
|
|
||||||
<DownArrow src={isValid ? ArrowDownBlue : ArrowDownGrey} alt="arrow" />
|
|
||||||
</DownArrowBackground>
|
|
||||||
</OversizedPanel>
|
|
||||||
<AddressInputPanel onChange={setRecipient} onError={setRecipientError} />
|
|
||||||
<OversizedPanel hideBottom>
|
|
||||||
<ExchangeRateWrapper
|
|
||||||
onClick={() => {
|
|
||||||
setInverted(inverted => !inverted)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ExchangeRate>{t('exchangeRate')}</ExchangeRate>
|
|
||||||
{inverted ? (
|
|
||||||
<span>
|
|
||||||
{exchangeRate
|
|
||||||
? `1 ${inputSymbol} = ${amountFormatter(exchangeRate, 18, 4, false)} ${outputSymbol}`
|
|
||||||
: ' - '}
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span>
|
|
||||||
{exchangeRate
|
|
||||||
? `1 ${outputSymbol} = ${amountFormatter(exchangeRateInverted, 18, 4, false)} ${inputSymbol}`
|
|
||||||
: ' - '}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</ExchangeRateWrapper>
|
|
||||||
</OversizedPanel>
|
|
||||||
{renderSummary()}
|
|
||||||
<Flex>
|
|
||||||
<Button disabled={!isValid} onClick={onSwap} warning={highSlippageWarning}>
|
|
||||||
{highSlippageWarning ? t('sendAnyway') : t('send')}
|
|
||||||
</Button>
|
|
||||||
</Flex>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
@ -1,673 +1,6 @@
|
|||||||
import React, { useState, useReducer, useEffect } from 'react'
|
import React from 'react'
|
||||||
import ReactGA from 'react-ga'
|
import ExchangePage from '../../components/ExchangePage'
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { useWeb3Context } from 'web3-react'
|
|
||||||
import { ethers } from 'ethers'
|
|
||||||
import styled from 'styled-components'
|
|
||||||
|
|
||||||
import { Button } from '../../theme'
|
|
||||||
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
|
|
||||||
import OversizedPanel from '../../components/OversizedPanel'
|
|
||||||
import TransactionDetails from '../../components/TransactionDetails'
|
|
||||||
import ArrowDownBlue from '../../assets/images/arrow-down-blue.svg'
|
|
||||||
import ArrowDownGrey from '../../assets/images/arrow-down-grey.svg'
|
|
||||||
import { amountFormatter, calculateGasMargin } from '../../utils'
|
|
||||||
import { useExchangeContract } from '../../hooks'
|
|
||||||
import { useTokenDetails } from '../../contexts/Tokens'
|
|
||||||
import { useTransactionAdder } from '../../contexts/Transactions'
|
|
||||||
import { useAddressBalance, useExchangeReserves } from '../../contexts/Balances'
|
|
||||||
import { useAddressAllowance } from '../../contexts/Allowances'
|
|
||||||
|
|
||||||
const INPUT = 0
|
|
||||||
const OUTPUT = 1
|
|
||||||
|
|
||||||
const ETH_TO_TOKEN = 0
|
|
||||||
const TOKEN_TO_ETH = 1
|
|
||||||
const TOKEN_TO_TOKEN = 2
|
|
||||||
|
|
||||||
// denominated in bips
|
|
||||||
const ALLOWED_SLIPPAGE_DEFAULT = 100
|
|
||||||
const TOKEN_ALLOWED_SLIPPAGE_DEFAULT = 100
|
|
||||||
|
|
||||||
// denominated in seconds
|
|
||||||
const DEADLINE_FROM_NOW = 60 * 15
|
|
||||||
|
|
||||||
// denominated in bips
|
|
||||||
const GAS_MARGIN = ethers.utils.bigNumberify(1000)
|
|
||||||
|
|
||||||
const DownArrowBackground = styled.div`
|
|
||||||
${({ theme }) => theme.flexRowNoWrap}
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
`
|
|
||||||
|
|
||||||
const DownArrow = styled.img`
|
|
||||||
width: 0.625rem;
|
|
||||||
height: 0.625rem;
|
|
||||||
position: relative;
|
|
||||||
padding: 0.875rem;
|
|
||||||
cursor: ${({ clickable }) => clickable && 'pointer'};
|
|
||||||
`
|
|
||||||
|
|
||||||
const ExchangeRateWrapper = styled.div`
|
|
||||||
${({ theme }) => theme.flexRowNoWrap};
|
|
||||||
align-items: center;
|
|
||||||
color: ${({ theme }) => theme.doveGray};
|
|
||||||
font-size: 0.75rem;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
`
|
|
||||||
|
|
||||||
const ExchangeRate = styled.span`
|
|
||||||
flex: 1 1 auto;
|
|
||||||
width: 0;
|
|
||||||
color: ${({ theme }) => theme.chaliceGray};
|
|
||||||
`
|
|
||||||
|
|
||||||
const Flex = styled.div`
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 2rem;
|
|
||||||
|
|
||||||
button {
|
|
||||||
max-width: 20rem;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
function calculateSlippageBounds(value, token = false, tokenAllowedSlippage, allowedSlippage) {
|
|
||||||
if (value) {
|
|
||||||
const offset = value.mul(token ? tokenAllowedSlippage : allowedSlippage).div(ethers.utils.bigNumberify(10000))
|
|
||||||
const minimum = value.sub(offset)
|
|
||||||
const maximum = value.add(offset)
|
|
||||||
return {
|
|
||||||
minimum: minimum.lt(ethers.constants.Zero) ? ethers.constants.Zero : minimum,
|
|
||||||
maximum: maximum.gt(ethers.constants.MaxUint256) ? ethers.constants.MaxUint256 : maximum
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSwapType(inputCurrency, outputCurrency) {
|
|
||||||
if (!inputCurrency || !outputCurrency) {
|
|
||||||
return null
|
|
||||||
} else if (inputCurrency === 'ETH') {
|
|
||||||
return ETH_TO_TOKEN
|
|
||||||
} else if (outputCurrency === 'ETH') {
|
|
||||||
return TOKEN_TO_ETH
|
|
||||||
} else {
|
|
||||||
return TOKEN_TO_TOKEN
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// this mocks the getInputPrice function, and calculates the required output
|
|
||||||
function calculateEtherTokenOutputFromInput(inputAmount, inputReserve, outputReserve) {
|
|
||||||
const inputAmountWithFee = inputAmount.mul(ethers.utils.bigNumberify(997))
|
|
||||||
const numerator = inputAmountWithFee.mul(outputReserve)
|
|
||||||
const denominator = inputReserve.mul(ethers.utils.bigNumberify(1000)).add(inputAmountWithFee)
|
|
||||||
return numerator.div(denominator)
|
|
||||||
}
|
|
||||||
|
|
||||||
// this mocks the getOutputPrice function, and calculates the required input
|
|
||||||
function calculateEtherTokenInputFromOutput(outputAmount, inputReserve, outputReserve) {
|
|
||||||
const numerator = inputReserve.mul(outputAmount).mul(ethers.utils.bigNumberify(1000))
|
|
||||||
const denominator = outputReserve.sub(outputAmount).mul(ethers.utils.bigNumberify(997))
|
|
||||||
return numerator.div(denominator).add(ethers.constants.One)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getInitialSwapState(outputCurrency) {
|
|
||||||
return {
|
|
||||||
independentValue: '', // this is a user input
|
|
||||||
dependentValue: '', // this is a calculated number
|
|
||||||
independentField: INPUT,
|
|
||||||
inputCurrency: 'ETH',
|
|
||||||
outputCurrency: outputCurrency ? outputCurrency : ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function swapStateReducer(state, action) {
|
|
||||||
switch (action.type) {
|
|
||||||
case 'FLIP_INDEPENDENT': {
|
|
||||||
const { independentField, inputCurrency, outputCurrency } = state
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
dependentValue: '',
|
|
||||||
independentField: independentField === INPUT ? OUTPUT : INPUT,
|
|
||||||
inputCurrency: outputCurrency,
|
|
||||||
outputCurrency: inputCurrency
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case 'SELECT_CURRENCY': {
|
|
||||||
const { inputCurrency, outputCurrency } = state
|
|
||||||
const { field, currency } = action.payload
|
|
||||||
|
|
||||||
const newInputCurrency = field === INPUT ? currency : inputCurrency
|
|
||||||
const newOutputCurrency = field === OUTPUT ? currency : outputCurrency
|
|
||||||
|
|
||||||
if (newInputCurrency === newOutputCurrency) {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
inputCurrency: field === INPUT ? currency : '',
|
|
||||||
outputCurrency: field === OUTPUT ? currency : ''
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
inputCurrency: newInputCurrency,
|
|
||||||
outputCurrency: newOutputCurrency
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case 'UPDATE_INDEPENDENT': {
|
|
||||||
const { field, value } = action.payload
|
|
||||||
const { dependentValue, independentValue } = state
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
independentValue: value,
|
|
||||||
dependentValue: value === independentValue ? dependentValue : '',
|
|
||||||
independentField: field
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case 'UPDATE_DEPENDENT': {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
dependentValue: action.payload
|
|
||||||
}
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
return getInitialSwapState()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getExchangeRate(inputValue, inputDecimals, outputValue, outputDecimals, invert = false) {
|
|
||||||
try {
|
|
||||||
if (
|
|
||||||
inputValue &&
|
|
||||||
(inputDecimals || inputDecimals === 0) &&
|
|
||||||
outputValue &&
|
|
||||||
(outputDecimals || outputDecimals === 0)
|
|
||||||
) {
|
|
||||||
const factor = ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(18))
|
|
||||||
|
|
||||||
if (invert) {
|
|
||||||
return inputValue
|
|
||||||
.mul(factor)
|
|
||||||
.div(outputValue)
|
|
||||||
.mul(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(outputDecimals)))
|
|
||||||
.div(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(inputDecimals)))
|
|
||||||
} else {
|
|
||||||
return outputValue
|
|
||||||
.mul(factor)
|
|
||||||
.div(inputValue)
|
|
||||||
.mul(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(inputDecimals)))
|
|
||||||
.div(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(outputDecimals)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMarketRate(
|
|
||||||
swapType,
|
|
||||||
inputReserveETH,
|
|
||||||
inputReserveToken,
|
|
||||||
inputDecimals,
|
|
||||||
outputReserveETH,
|
|
||||||
outputReserveToken,
|
|
||||||
outputDecimals,
|
|
||||||
invert = false
|
|
||||||
) {
|
|
||||||
if (swapType === ETH_TO_TOKEN) {
|
|
||||||
return getExchangeRate(outputReserveETH, 18, outputReserveToken, outputDecimals, invert)
|
|
||||||
} else if (swapType === TOKEN_TO_ETH) {
|
|
||||||
return getExchangeRate(inputReserveToken, inputDecimals, inputReserveETH, 18, invert)
|
|
||||||
} else if (swapType === TOKEN_TO_TOKEN) {
|
|
||||||
const factor = ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(18))
|
|
||||||
const firstRate = getExchangeRate(inputReserveToken, inputDecimals, inputReserveETH, 18)
|
|
||||||
const secondRate = getExchangeRate(outputReserveETH, 18, outputReserveToken, outputDecimals)
|
|
||||||
try {
|
|
||||||
return !!(firstRate && secondRate) ? firstRate.mul(secondRate).div(factor) : undefined
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Swap({ initialCurrency }) {
|
export default function Swap({ initialCurrency }) {
|
||||||
const { t } = useTranslation()
|
return <ExchangePage initialCurrency={initialCurrency} />
|
||||||
const { account } = useWeb3Context()
|
|
||||||
|
|
||||||
const addTransaction = useTransactionAdder()
|
|
||||||
|
|
||||||
const [rawSlippage, setRawSlippage] = useState(ALLOWED_SLIPPAGE_DEFAULT)
|
|
||||||
const [rawTokenSlippage, setRawTokenSlippage] = useState(TOKEN_ALLOWED_SLIPPAGE_DEFAULT)
|
|
||||||
|
|
||||||
let allowedSlippageBig = ethers.utils.bigNumberify(rawSlippage)
|
|
||||||
let tokenAllowedSlippageBig = ethers.utils.bigNumberify(rawTokenSlippage)
|
|
||||||
|
|
||||||
// analytics
|
|
||||||
useEffect(() => {
|
|
||||||
ReactGA.pageview(window.location.pathname + window.location.search)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// core swap state-
|
|
||||||
const [swapState, dispatchSwapState] = useReducer(swapStateReducer, initialCurrency, getInitialSwapState)
|
|
||||||
const { independentValue, dependentValue, independentField, inputCurrency, outputCurrency } = swapState
|
|
||||||
|
|
||||||
// get swap type from the currency types
|
|
||||||
const swapType = getSwapType(inputCurrency, outputCurrency)
|
|
||||||
|
|
||||||
// get decimals and exchange addressfor each of the currency types
|
|
||||||
const { symbol: inputSymbol, decimals: inputDecimals, exchangeAddress: inputExchangeAddress } = useTokenDetails(
|
|
||||||
inputCurrency
|
|
||||||
)
|
|
||||||
const { symbol: outputSymbol, decimals: outputDecimals, exchangeAddress: outputExchangeAddress } = useTokenDetails(
|
|
||||||
outputCurrency
|
|
||||||
)
|
|
||||||
|
|
||||||
const inputExchangeContract = useExchangeContract(inputExchangeAddress)
|
|
||||||
const outputExchangeContract = useExchangeContract(outputExchangeAddress)
|
|
||||||
const contract = swapType === ETH_TO_TOKEN ? outputExchangeContract : inputExchangeContract
|
|
||||||
|
|
||||||
// get input allowance
|
|
||||||
const inputAllowance = useAddressAllowance(account, inputCurrency, inputExchangeAddress)
|
|
||||||
|
|
||||||
// fetch reserves for each of the currency types
|
|
||||||
const { reserveETH: inputReserveETH, reserveToken: inputReserveToken } = useExchangeReserves(inputCurrency)
|
|
||||||
const { reserveETH: outputReserveETH, reserveToken: outputReserveToken } = useExchangeReserves(outputCurrency)
|
|
||||||
|
|
||||||
// get balances for each of the currency types
|
|
||||||
const inputBalance = useAddressBalance(account, inputCurrency)
|
|
||||||
const outputBalance = useAddressBalance(account, outputCurrency)
|
|
||||||
const inputBalanceFormatted = !!(inputBalance && Number.isInteger(inputDecimals))
|
|
||||||
? amountFormatter(inputBalance, inputDecimals, Math.min(4, inputDecimals))
|
|
||||||
: ''
|
|
||||||
const outputBalanceFormatted = !!(outputBalance && Number.isInteger(outputDecimals))
|
|
||||||
? amountFormatter(outputBalance, outputDecimals, Math.min(4, outputDecimals))
|
|
||||||
: ''
|
|
||||||
|
|
||||||
// compute useful transforms of the data above
|
|
||||||
const independentDecimals = independentField === INPUT ? inputDecimals : outputDecimals
|
|
||||||
const dependentDecimals = independentField === OUTPUT ? inputDecimals : outputDecimals
|
|
||||||
|
|
||||||
// declare/get parsed and formatted versions of input/output values
|
|
||||||
const [independentValueParsed, setIndependentValueParsed] = useState()
|
|
||||||
const dependentValueFormatted = !!(dependentValue && (dependentDecimals || dependentDecimals === 0))
|
|
||||||
? amountFormatter(dependentValue, dependentDecimals, Math.min(4, dependentDecimals), false)
|
|
||||||
: ''
|
|
||||||
const inputValueParsed = independentField === INPUT ? independentValueParsed : dependentValue
|
|
||||||
const inputValueFormatted = independentField === INPUT ? independentValue : dependentValueFormatted
|
|
||||||
const outputValueParsed = independentField === OUTPUT ? independentValueParsed : dependentValue
|
|
||||||
const outputValueFormatted = independentField === OUTPUT ? independentValue : dependentValueFormatted
|
|
||||||
|
|
||||||
// validate + parse independent value
|
|
||||||
const [independentError, setIndependentError] = useState()
|
|
||||||
useEffect(() => {
|
|
||||||
if (independentValue && (independentDecimals || independentDecimals === 0)) {
|
|
||||||
try {
|
|
||||||
const parsedValue = ethers.utils.parseUnits(independentValue, independentDecimals)
|
|
||||||
|
|
||||||
if (parsedValue.lte(ethers.constants.Zero) || parsedValue.gte(ethers.constants.MaxUint256)) {
|
|
||||||
throw Error()
|
|
||||||
} else {
|
|
||||||
setIndependentValueParsed(parsedValue)
|
|
||||||
setIndependentError(null)
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
setIndependentError(t('inputNotValid'))
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
setIndependentValueParsed()
|
|
||||||
setIndependentError()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [independentValue, independentDecimals, t])
|
|
||||||
|
|
||||||
// calculate slippage from target rate
|
|
||||||
const { minimum: dependentValueMinumum, maximum: dependentValueMaximum } = calculateSlippageBounds(
|
|
||||||
dependentValue,
|
|
||||||
swapType === TOKEN_TO_TOKEN,
|
|
||||||
tokenAllowedSlippageBig,
|
|
||||||
allowedSlippageBig
|
|
||||||
)
|
|
||||||
|
|
||||||
// validate input allowance + balance
|
|
||||||
const [inputError, setInputError] = useState()
|
|
||||||
const [showUnlock, setShowUnlock] = useState(false)
|
|
||||||
useEffect(() => {
|
|
||||||
const inputValueCalculation = independentField === INPUT ? independentValueParsed : dependentValueMaximum
|
|
||||||
if (inputBalance && (inputAllowance || inputCurrency === 'ETH') && inputValueCalculation) {
|
|
||||||
if (inputBalance.lt(inputValueCalculation)) {
|
|
||||||
setInputError(t('insufficientBalance'))
|
|
||||||
} else if (inputCurrency !== 'ETH' && inputAllowance.lt(inputValueCalculation)) {
|
|
||||||
setInputError(t('unlockTokenCont'))
|
|
||||||
setShowUnlock(true)
|
|
||||||
} else {
|
|
||||||
setInputError(null)
|
|
||||||
setShowUnlock(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
setInputError()
|
|
||||||
setShowUnlock(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [independentField, independentValueParsed, dependentValueMaximum, inputBalance, inputCurrency, inputAllowance, t])
|
|
||||||
|
|
||||||
// calculate dependent value
|
|
||||||
useEffect(() => {
|
|
||||||
const amount = independentValueParsed
|
|
||||||
|
|
||||||
if (swapType === ETH_TO_TOKEN) {
|
|
||||||
const reserveETH = outputReserveETH
|
|
||||||
const reserveToken = outputReserveToken
|
|
||||||
|
|
||||||
if (amount && reserveETH && reserveToken) {
|
|
||||||
try {
|
|
||||||
const calculatedDependentValue =
|
|
||||||
independentField === INPUT
|
|
||||||
? calculateEtherTokenOutputFromInput(amount, reserveETH, reserveToken)
|
|
||||||
: calculateEtherTokenInputFromOutput(amount, reserveETH, reserveToken)
|
|
||||||
|
|
||||||
if (calculatedDependentValue.lte(ethers.constants.Zero)) {
|
|
||||||
throw Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: calculatedDependentValue })
|
|
||||||
} catch {
|
|
||||||
setIndependentError(t('insufficientLiquidity'))
|
|
||||||
}
|
|
||||||
return () => {
|
|
||||||
dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: '' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (swapType === TOKEN_TO_ETH) {
|
|
||||||
const reserveETH = inputReserveETH
|
|
||||||
const reserveToken = inputReserveToken
|
|
||||||
|
|
||||||
if (amount && reserveETH && reserveToken) {
|
|
||||||
try {
|
|
||||||
const calculatedDependentValue =
|
|
||||||
independentField === INPUT
|
|
||||||
? calculateEtherTokenOutputFromInput(amount, reserveToken, reserveETH)
|
|
||||||
: calculateEtherTokenInputFromOutput(amount, reserveToken, reserveETH)
|
|
||||||
|
|
||||||
if (calculatedDependentValue.lte(ethers.constants.Zero)) {
|
|
||||||
throw Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: calculatedDependentValue })
|
|
||||||
} catch {
|
|
||||||
setIndependentError(t('insufficientLiquidity'))
|
|
||||||
}
|
|
||||||
return () => {
|
|
||||||
dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: '' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (swapType === TOKEN_TO_TOKEN) {
|
|
||||||
const reserveETHFirst = inputReserveETH
|
|
||||||
const reserveTokenFirst = inputReserveToken
|
|
||||||
|
|
||||||
const reserveETHSecond = outputReserveETH
|
|
||||||
const reserveTokenSecond = outputReserveToken
|
|
||||||
|
|
||||||
if (amount && reserveETHFirst && reserveTokenFirst && reserveETHSecond && reserveTokenSecond) {
|
|
||||||
try {
|
|
||||||
if (independentField === INPUT) {
|
|
||||||
const intermediateValue = calculateEtherTokenOutputFromInput(amount, reserveTokenFirst, reserveETHFirst)
|
|
||||||
if (intermediateValue.lte(ethers.constants.Zero)) {
|
|
||||||
throw Error()
|
|
||||||
}
|
|
||||||
const calculatedDependentValue = calculateEtherTokenOutputFromInput(
|
|
||||||
intermediateValue,
|
|
||||||
reserveETHSecond,
|
|
||||||
reserveTokenSecond
|
|
||||||
)
|
|
||||||
if (calculatedDependentValue.lte(ethers.constants.Zero)) {
|
|
||||||
throw Error()
|
|
||||||
}
|
|
||||||
dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: calculatedDependentValue })
|
|
||||||
} else {
|
|
||||||
const intermediateValue = calculateEtherTokenInputFromOutput(amount, reserveETHSecond, reserveTokenSecond)
|
|
||||||
if (intermediateValue.lte(ethers.constants.Zero)) {
|
|
||||||
throw Error()
|
|
||||||
}
|
|
||||||
const calculatedDependentValue = calculateEtherTokenInputFromOutput(
|
|
||||||
intermediateValue,
|
|
||||||
reserveTokenFirst,
|
|
||||||
reserveETHFirst
|
|
||||||
)
|
|
||||||
if (calculatedDependentValue.lte(ethers.constants.Zero)) {
|
|
||||||
throw Error()
|
|
||||||
}
|
|
||||||
dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: calculatedDependentValue })
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
setIndependentError(t('insufficientLiquidity'))
|
|
||||||
}
|
|
||||||
return () => {
|
|
||||||
dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: '' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
independentValueParsed,
|
|
||||||
swapType,
|
|
||||||
outputReserveETH,
|
|
||||||
outputReserveToken,
|
|
||||||
inputReserveETH,
|
|
||||||
inputReserveToken,
|
|
||||||
independentField,
|
|
||||||
t
|
|
||||||
])
|
|
||||||
|
|
||||||
const [inverted, setInverted] = useState(false)
|
|
||||||
const exchangeRate = getExchangeRate(inputValueParsed, inputDecimals, outputValueParsed, outputDecimals)
|
|
||||||
const exchangeRateInverted = getExchangeRate(inputValueParsed, inputDecimals, outputValueParsed, outputDecimals, true)
|
|
||||||
|
|
||||||
const marketRate = getMarketRate(
|
|
||||||
swapType,
|
|
||||||
inputReserveETH,
|
|
||||||
inputReserveToken,
|
|
||||||
inputDecimals,
|
|
||||||
outputReserveETH,
|
|
||||||
outputReserveToken,
|
|
||||||
outputDecimals
|
|
||||||
)
|
|
||||||
|
|
||||||
const percentSlippage =
|
|
||||||
exchangeRate && marketRate
|
|
||||||
? exchangeRate
|
|
||||||
.sub(marketRate)
|
|
||||||
.abs()
|
|
||||||
.mul(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(18)))
|
|
||||||
.div(marketRate)
|
|
||||||
.sub(ethers.utils.bigNumberify(3).mul(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(15))))
|
|
||||||
: undefined
|
|
||||||
const percentSlippageFormatted = percentSlippage && amountFormatter(percentSlippage, 16, 2)
|
|
||||||
const slippageWarning =
|
|
||||||
percentSlippage &&
|
|
||||||
percentSlippage.gte(ethers.utils.parseEther('.05')) &&
|
|
||||||
percentSlippage.lt(ethers.utils.parseEther('.2')) // [5% - 20%)
|
|
||||||
const highSlippageWarning = percentSlippage && percentSlippage.gte(ethers.utils.parseEther('.2')) // [20+%
|
|
||||||
|
|
||||||
const isValid = exchangeRate && inputError === null && independentError === null
|
|
||||||
|
|
||||||
const estimatedText = `(${t('estimated')})`
|
|
||||||
function formatBalance(value) {
|
|
||||||
return `Balance: ${value}`
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onSwap() {
|
|
||||||
const deadline = Math.ceil(Date.now() / 1000) + DEADLINE_FROM_NOW
|
|
||||||
|
|
||||||
let estimate, method, args, value
|
|
||||||
if (independentField === INPUT) {
|
|
||||||
ReactGA.event({
|
|
||||||
category: `${swapType}`,
|
|
||||||
action: 'SwapInput'
|
|
||||||
})
|
|
||||||
|
|
||||||
if (swapType === ETH_TO_TOKEN) {
|
|
||||||
estimate = contract.estimate.ethToTokenSwapInput
|
|
||||||
method = contract.ethToTokenSwapInput
|
|
||||||
args = [dependentValueMinumum, deadline]
|
|
||||||
value = independentValueParsed
|
|
||||||
} else if (swapType === TOKEN_TO_ETH) {
|
|
||||||
estimate = contract.estimate.tokenToEthSwapInput
|
|
||||||
method = contract.tokenToEthSwapInput
|
|
||||||
args = [independentValueParsed, dependentValueMinumum, deadline]
|
|
||||||
value = ethers.constants.Zero
|
|
||||||
} else if (swapType === TOKEN_TO_TOKEN) {
|
|
||||||
estimate = contract.estimate.tokenToTokenSwapInput
|
|
||||||
method = contract.tokenToTokenSwapInput
|
|
||||||
args = [independentValueParsed, dependentValueMinumum, ethers.constants.One, deadline, outputCurrency]
|
|
||||||
value = ethers.constants.Zero
|
|
||||||
}
|
|
||||||
} else if (independentField === OUTPUT) {
|
|
||||||
ReactGA.event({
|
|
||||||
category: `${swapType}`,
|
|
||||||
action: 'SwapOutput'
|
|
||||||
})
|
|
||||||
|
|
||||||
if (swapType === ETH_TO_TOKEN) {
|
|
||||||
estimate = contract.estimate.ethToTokenSwapOutput
|
|
||||||
method = contract.ethToTokenSwapOutput
|
|
||||||
args = [independentValueParsed, deadline]
|
|
||||||
value = dependentValueMaximum
|
|
||||||
} else if (swapType === TOKEN_TO_ETH) {
|
|
||||||
estimate = contract.estimate.tokenToEthSwapOutput
|
|
||||||
method = contract.tokenToEthSwapOutput
|
|
||||||
args = [independentValueParsed, dependentValueMaximum, deadline]
|
|
||||||
value = ethers.constants.Zero
|
|
||||||
} else if (swapType === TOKEN_TO_TOKEN) {
|
|
||||||
estimate = contract.estimate.tokenToTokenSwapOutput
|
|
||||||
method = contract.tokenToTokenSwapOutput
|
|
||||||
args = [independentValueParsed, dependentValueMaximum, ethers.constants.MaxUint256, deadline, outputCurrency]
|
|
||||||
value = ethers.constants.Zero
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const estimatedGasLimit = await estimate(...args, { value })
|
|
||||||
method(...args, { value, gasLimit: calculateGasMargin(estimatedGasLimit, GAS_MARGIN) }).then(response => {
|
|
||||||
addTransaction(response)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const [customSlippageError, setcustomSlippageError] = useState('')
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<CurrencyInputPanel
|
|
||||||
title={t('input')}
|
|
||||||
description={inputValueFormatted && independentField === OUTPUT ? estimatedText : ''}
|
|
||||||
extraText={inputBalanceFormatted && formatBalance(inputBalanceFormatted)}
|
|
||||||
extraTextClickHander={() => {
|
|
||||||
if (inputBalance && inputDecimals) {
|
|
||||||
const valueToSet = inputCurrency === 'ETH' ? inputBalance.sub(ethers.utils.parseEther('.1')) : inputBalance
|
|
||||||
if (valueToSet.gt(ethers.constants.Zero)) {
|
|
||||||
dispatchSwapState({
|
|
||||||
type: 'UPDATE_INDEPENDENT',
|
|
||||||
payload: { value: amountFormatter(valueToSet, inputDecimals, inputDecimals, false), field: INPUT }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onCurrencySelected={inputCurrency => {
|
|
||||||
dispatchSwapState({ type: 'SELECT_CURRENCY', payload: { currency: inputCurrency, field: INPUT } })
|
|
||||||
}}
|
|
||||||
onValueChange={inputValue => {
|
|
||||||
dispatchSwapState({ type: 'UPDATE_INDEPENDENT', payload: { value: inputValue, field: INPUT } })
|
|
||||||
}}
|
|
||||||
showUnlock={showUnlock}
|
|
||||||
selectedTokens={[inputCurrency, outputCurrency]}
|
|
||||||
selectedTokenAddress={inputCurrency}
|
|
||||||
value={inputValueFormatted}
|
|
||||||
errorMessage={inputError ? inputError : independentField === INPUT ? independentError : ''}
|
|
||||||
/>
|
|
||||||
<OversizedPanel>
|
|
||||||
<DownArrowBackground>
|
|
||||||
<DownArrow
|
|
||||||
onClick={() => {
|
|
||||||
dispatchSwapState({ type: 'FLIP_INDEPENDENT' })
|
|
||||||
}}
|
|
||||||
clickable
|
|
||||||
alt="swap"
|
|
||||||
src={isValid ? ArrowDownBlue : ArrowDownGrey}
|
|
||||||
/>
|
|
||||||
</DownArrowBackground>
|
|
||||||
</OversizedPanel>
|
|
||||||
<CurrencyInputPanel
|
|
||||||
title={t('output')}
|
|
||||||
description={outputValueFormatted && independentField === INPUT ? estimatedText : ''}
|
|
||||||
extraText={outputBalanceFormatted && formatBalance(outputBalanceFormatted)}
|
|
||||||
onCurrencySelected={outputCurrency => {
|
|
||||||
dispatchSwapState({ type: 'SELECT_CURRENCY', payload: { currency: outputCurrency, field: OUTPUT } })
|
|
||||||
}}
|
|
||||||
onValueChange={outputValue => {
|
|
||||||
dispatchSwapState({ type: 'UPDATE_INDEPENDENT', payload: { value: outputValue, field: OUTPUT } })
|
|
||||||
}}
|
|
||||||
selectedTokens={[inputCurrency, outputCurrency]}
|
|
||||||
selectedTokenAddress={outputCurrency}
|
|
||||||
value={outputValueFormatted}
|
|
||||||
errorMessage={independentField === OUTPUT ? independentError : ''}
|
|
||||||
disableUnlock
|
|
||||||
/>
|
|
||||||
<OversizedPanel hideBottom>
|
|
||||||
<ExchangeRateWrapper
|
|
||||||
onClick={() => {
|
|
||||||
setInverted(inverted => !inverted)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ExchangeRate>{t('exchangeRate')}</ExchangeRate>
|
|
||||||
{inverted ? (
|
|
||||||
<span>
|
|
||||||
{exchangeRate
|
|
||||||
? `1 ${inputSymbol} = ${amountFormatter(exchangeRate, 18, 4, false)} ${outputSymbol}`
|
|
||||||
: ' - '}
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span>
|
|
||||||
{exchangeRate
|
|
||||||
? `1 ${outputSymbol} = ${amountFormatter(exchangeRateInverted, 18, 4, false)} ${inputSymbol}`
|
|
||||||
: ' - '}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</ExchangeRateWrapper>
|
|
||||||
</OversizedPanel>
|
|
||||||
<TransactionDetails
|
|
||||||
account={account}
|
|
||||||
setRawSlippage={setRawSlippage}
|
|
||||||
setRawTokenSlippage={setRawTokenSlippage}
|
|
||||||
rawSlippage={rawSlippage}
|
|
||||||
slippageWarning={slippageWarning}
|
|
||||||
highSlippageWarning={highSlippageWarning}
|
|
||||||
inputError={inputError}
|
|
||||||
independentError={independentError}
|
|
||||||
inputCurrency={inputCurrency}
|
|
||||||
outputCurrency={outputCurrency}
|
|
||||||
independentValue={independentValue}
|
|
||||||
independentValueParsed={independentValueParsed}
|
|
||||||
independentField={independentField}
|
|
||||||
INPUT={INPUT}
|
|
||||||
inputValueParsed={inputValueParsed}
|
|
||||||
outputValueParsed={outputValueParsed}
|
|
||||||
inputSymbol={inputSymbol}
|
|
||||||
outputSymbol={outputSymbol}
|
|
||||||
dependentValueMinumum={dependentValueMinumum}
|
|
||||||
dependentValueMaximum={dependentValueMaximum}
|
|
||||||
dependentDecimals={dependentDecimals}
|
|
||||||
independentDecimals={independentDecimals}
|
|
||||||
percentSlippageFormatted={percentSlippageFormatted}
|
|
||||||
setcustomSlippageError={setcustomSlippageError}
|
|
||||||
/>
|
|
||||||
<Flex>
|
|
||||||
<Button
|
|
||||||
disabled={!isValid || customSlippageError === 'invalid'}
|
|
||||||
onClick={onSwap}
|
|
||||||
warning={highSlippageWarning || customSlippageError === 'warning'}
|
|
||||||
>
|
|
||||||
{highSlippageWarning || customSlippageError === 'warning' ? t('swapAnyway') : t('swap')}
|
|
||||||
</Button>
|
|
||||||
</Flex>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user