Commit 3f3889b9 authored by Ian Lapham's avatar Ian Lapham Committed by GitHub

Merge pull request #1 from ianlapham/combine-swap-send

Combine swap send into one component 
parents 870b3f7e e19e150f
...@@ -80,5 +80,6 @@ ...@@ -80,5 +80,6 @@
"symbol": "Symbol", "symbol": "Symbol",
"decimals": "Decimals", "decimals": "Decimals",
"enterTokenCont": "Enter a token address to continue", "enterTokenCont": "Enter a token address to continue",
"priceChange": "This trade will cause the price to change by" "priceChange": "Expected price slippage",
"forAtLeast" : "for at least "
} }
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="9" cy="9" r="9" fill="#E1E1E1"/>
<path d="M8.06493 10.8317H9.15706V10.7592C9.17233 9.88089 9.42436 9.48757 10.0735 9.08662C10.7571 8.67421 11.1771 8.09378 11.1771 7.23459C11.1771 5.99354 10.2377 5.15344 8.83629 5.15344C7.54942 5.15344 6.51839 5.90571 6.46875 7.28041H7.62961C7.67543 6.47086 8.25204 6.11573 8.83629 6.11573C9.48546 6.11573 10.0124 6.54724 10.0124 7.22313C10.0124 7.79211 9.65729 8.19306 9.20288 8.47564C8.49262 8.91096 8.07257 9.34246 8.06493 10.7592V10.8317ZM8.64154 13.1534C9.05777 13.1534 9.40527 12.8136 9.40527 12.3897C9.40527 11.9735 9.05777 11.6298 8.64154 11.6298C8.22149 11.6298 7.87782 11.9735 7.87782 12.3897C7.87782 12.8136 8.22149 13.1534 8.64154 13.1534Z" fill="#737373"/>
</svg>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="8" cy="8" r="8" fill="#E1E1E1"/>
<path d="M7.09618 9.67828H8.18831V9.60573C8.20358 8.72745 8.45561 8.33413 9.10477 7.93317C9.78831 7.52076 10.2084 6.94033 10.2084 6.08115C10.2084 4.8401 9.26897 4 7.86754 4C6.58067 4 5.54964 4.75227 5.5 6.12697H6.66086C6.70668 5.31742 7.28329 4.96229 7.86754 4.96229C8.51671 4.96229 9.04368 5.39379 9.04368 6.06969C9.04368 6.63866 8.68854 7.03962 8.23413 7.3222C7.52387 7.75752 7.10382 8.18902 7.09618 9.60573V9.67828ZM7.67279 12C8.08902 12 8.43652 11.6601 8.43652 11.2363C8.43652 10.82 8.08902 10.4764 7.67279 10.4764C7.25274 10.4764 6.90907 10.82 6.90907 11.2363C6.90907 11.6601 7.25274 12 7.67279 12Z" fill="#737373"/>
</svg>
...@@ -33,6 +33,7 @@ const SummaryWrapperContainer = styled.div` ...@@ -33,6 +33,7 @@ const SummaryWrapperContainer = styled.div`
const Details = styled.div` const Details = styled.div`
background-color: ${({ theme }) => theme.concreteGray}; background-color: ${({ theme }) => theme.concreteGray};
padding: 1.5rem; padding: 1.5rem;
padding-bottom: 1rem;
border-radius: 1rem; border-radius: 1rem;
font-size: 0.75rem; font-size: 0.75rem;
margin-top: 1rem; margin-top: 1rem;
...@@ -92,10 +93,10 @@ export default function ContextualInfo({ ...@@ -92,10 +93,10 @@ export default function ContextualInfo({
renderTransactionDetails = () => {}, renderTransactionDetails = () => {},
isError = false, isError = false,
slippageWarning, slippageWarning,
highSlippageWarning highSlippageWarning,
dropDownContent
}) { }) {
const [showDetails, setShowDetails] = useState(false) const [showDetails, setShowDetails] = useState(false)
return !allowExpand ? ( return !allowExpand ? (
<SummaryWrapper>{contextualInfo}</SummaryWrapper> <SummaryWrapper>{contextualInfo}</SummaryWrapper>
) : ( ) : (
...@@ -117,7 +118,7 @@ export default function ContextualInfo({ ...@@ -117,7 +118,7 @@ export default function ContextualInfo({
)} )}
</> </>
</SummaryWrapperContainer> </SummaryWrapperContainer>
{showDetails && <Details>{renderTransactionDetails()}</Details>} {showDetails && <Details>{dropDownContent()}</Details>}
</> </>
) )
} }
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>
</>
)
}
...@@ -6,7 +6,7 @@ import '@reach/dialog/styles.css' ...@@ -6,7 +6,7 @@ import '@reach/dialog/styles.css'
const AnimatedDialogOverlay = animated(DialogOverlay) const AnimatedDialogOverlay = animated(DialogOverlay)
const StyledDialogOverlay = styled(AnimatedDialogOverlay).attrs({ const StyledDialogOverlay = styled(AnimatedDialogOverlay).attrs({
suppressClassNameWarning: true suppressclassnamewarning: 'true'
})` })`
&[data-reach-dialog-overlay] { &[data-reach-dialog-overlay] {
z-index: 2; z-index: 2;
......
import React, { useState } from 'react'
import ReactGA from 'react-ga'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { isAddress, amountFormatter } from '../../utils'
import questionMark from '../../assets/images/question-mark.svg'
import NewContextualInfo from '../../components/ContextualInfoNew'
const Flex = styled.div`
display: flex;
justify-content: center;
button {
max-width: 20rem;
}
`
const SlippageRow = styled(Flex)`
position: relative;
width: 100%;
flex-direction: row;
justify-content: flex-start;
align-items: center;
font-size: 0.8rem;
padding: 0;
height: 24px;
margin-bottom: 14px;
`
const QuestionWrapper = styled.div`
margin-left: 0.4rem;
margin-top: 0.2rem;
&:hover {
cursor: pointer;
}
`
const Popup = styled(Flex)`
position: absolute;
width: 228px;
left: -78px;
top: -124px;
flex-direction: column;
aligm-items: center;
padding: 1rem;
line-height: 183.52%;
background: #404040;
border-radius: 8px;
color: white;
font-style: italic;
`
const Option = styled(Flex)`
align-items: center;
min-width: 55px;
height: 24px;
margin-right: 4px;
border-radius: 36px;
border: 1px solid #f2f2f2;
${({ active }) =>
active &&
`
background-color: #2f80ed;
color: white;
border: 1px solid #2f80ed;
`}
&:hover {
cursor: pointer;
}
`
const Input = styled.input`
width: 123.27px;
background: #ffffff;
height: 2rem;
outline: none;
margin-left: 20px;
border: 1px solid #f2f2f2;
box-sizing: border-box;
border-radius: 36px;
color: #aeaeae;
&:focus {
}
text-align: left;
padding-left: 0.9rem;
&::-webkit-outer-spin-button,
&::-webkit-inner-spin-button {
-webkit-appearance: none;
}
${({ active }) =>
active &&
`
border: 1px solid #2f80ed;
text-align: right;
padding-right 1.5rem;
padding-left 0rem;
color : inherit;
`}
${({ warning }) =>
warning === 'red' &&
`
color : #FF6871;
border: 1px solid #FF6871;
`}
`
const BottomError = styled.div`
margin-top: 1rem;
color: #aeaeae;
${({ color }) =>
color === 'red' &&
`
color : #FF6871;
`}
`
const Break = styled.div`
border: 1px solid #f2f2f2;
width: 100%;
margin-top: 1rem;
`
const OptionLarge = styled(Option)`
width: 120px;
`
const Bold = styled.span`
font-weight: 500;
`
const LastSummaryText = styled.div`
margin-top: 0.6rem;
`
const SlippageSelector = styled.div`
margin-top: 1rem;
`
const InputGroup = styled.div`
position: relative;
`
const Percent = styled.div`
right: 14px;
top: 9px;
position: absolute;
color: inherit;
font-size: 0, 8rem;
${({ color }) =>
(color === 'faded' &&
`
color : #AEAEAE
`) ||
(color === 'red' &&
`
color : #FF6871
`)}
`
const Faded = styled.span`
opacity: 0.7;
`
const ErrorEmoji = styled.span`
left: 30px;
top: 4px;
position: absolute;
`
export default function TransactionDetails(props) {
const { t } = useTranslation()
function renderSummary() {
let contextualInfo = ''
let isError = false
if (props.inputError || props.independentError) {
contextualInfo = props.inputError || props.independentError
isError = true
} else if (!props.inputCurrency || !props.outputCurrency) {
contextualInfo = t('selectTokenCont')
} else if (!props.independentValue) {
contextualInfo = t('enterValueCont')
} else if (props.sending && !props.recipientAddress) {
contextualInfo = t('noRecipient')
} else if (props.sending && !isAddress(props.recipientAddress)) {
contextualInfo = t('invalidRecipient')
} else if (!props.account) {
contextualInfo = t('noWallet')
isError = true
}
const slippageWarningText = props.highSlippageWarning
? t('highSlippageWarning')
: props.slippageWarning
? t('slippageWarning')
: ''
return (
<NewContextualInfo
openDetailsText={t('transactionDetails')}
closeDetailsText={t('hideDetails')}
contextualInfo={contextualInfo ? contextualInfo : slippageWarningText}
allowExpand={
!!(
props.inputCurrency &&
props.outputCurrency &&
props.inputValueParsed &&
props.outputValueParsed &&
(props.sending ? props.recipientAddress : true)
)
}
isError={isError}
slippageWarning={props.slippageWarning && !contextualInfo}
highSlippageWarning={props.highSlippageWarning && !contextualInfo}
renderTransactionDetails={renderTransactionDetails}
dropDownContent={dropDownContent}
/>
)
}
const [activeIndex, setActiveIndex] = useState(3)
const [placeHolder, setplaceHolder] = useState('Custom')
const [warningType, setWarningType] = useState('none')
const [showPopup, setPopup] = useState(false)
const dropDownContent = () => {
return (
<>
{renderTransactionDetails()}
<Break />
<SlippageSelector>
<SlippageRow>
Limit addtional price slippage
<QuestionWrapper
onMouseEnter={() => {
setPopup(true)
}}
onMouseLeave={() => {
setPopup(false)
}}
>
<img src={questionMark} alt="question mark" />
</QuestionWrapper>
{showPopup ? (
<Popup>
Lowering this limit decreases your risk of frontrunning. This makes it more likely that your transaction
will fail due to normal price movements.
</Popup>
) : (
''
)}
</SlippageRow>
<SlippageRow>
<Option
onClick={() => {
updateSlippage(0.1)
setWarningType('none')
setActiveIndex(1)
props.setcustomSlippageError('valid')
setplaceHolder('Custom')
}}
active={activeIndex === 1 ? true : false}
>
0.1%
</Option>
<Option
onClick={() => {
updateSlippage(1)
setWarningType('none')
setActiveIndex(2)
props.setcustomSlippageError('valid')
setplaceHolder('Custom')
}}
active={activeIndex === 2 ? true : false}
>
1%
</Option>
<OptionLarge
onClick={() => {
updateSlippage(2)
setWarningType('none')
setActiveIndex(3)
props.setcustomSlippageError('valid')
setplaceHolder('Custom')
}}
active={activeIndex === 3 ? true : false}
>
2%
<Faded>(suggested)</Faded>
</OptionLarge>
<InputGroup>
{warningType !== 'none' ? <ErrorEmoji>⚠️</ErrorEmoji> : ''}
<Input
placeholder={placeHolder}
value={userInput || ''}
onChange={parseInput}
onClick={e => {
setActiveIndex(4)
setplaceHolder('')
parseInput(e)
}}
active={activeIndex === 4 ? true : false}
warning={
warningType === 'emptyInput'
? ''
: warningType !== 'none' && warningType !== 'riskyEntryLow'
? 'red'
: ''
}
/>
<Percent
color={
warningType === 'emptyInput'
? 'faded'
: warningType !== 'none' && warningType !== 'riskyEntryLow'
? 'red'
: activeIndex !== 4
? 'faded'
: ''
}
>
%
</Percent>
</InputGroup>
</SlippageRow>
<SlippageRow>
<BottomError
color={
warningType === 'emptyInput'
? ''
: warningType !== 'none' && warningType !== 'riskyEntryLow'
? 'red'
: ''
}
>
{warningType === 'emptyInput' ? 'Enter a slippage percentage.' : ''}
{warningType === 'invalidEntry' ? 'Please input a valid percentage.' : ''}
{warningType === 'invalidEntryBound' ? 'Pleae select value less than 50%' : ''}
{warningType === 'riskyEntryHigh' ? 'Your transaction may be frontrun.' : ''}
{warningType === 'riskyEntryLow' ? 'Your transaction may fail.' : ''}
</BottomError>
</SlippageRow>
</SlippageSelector>
</>
)
}
const [userInput, setUserInput] = useState()
const parseInput = e => {
let input = e.target.value
if (input === '') {
setUserInput(input)
props.setcustomSlippageError('invalid')
return setWarningType('emptyInput')
}
//check for decimal
let isValid = /^[+]?\d*\.?\d{1,2}$/.test(input) || /^[+]?\d*\.$/.test(input)
let decimalLimit = /^\d+\.?\d{0,2}$/.test(input) || input === ''
if (decimalLimit) {
setUserInput(input)
} else {
return
}
if (isValid) {
checkAcceptablePercentValue(input)
} else {
setWarningType('invalidEntry')
}
}
const checkAcceptablePercentValue = input => {
setTimeout(function() {
setWarningType('none')
props.setcustomSlippageError('valid')
if (input < 0 || input > 50) {
props.setcustomSlippageError('invalid')
return setWarningType('invalidEntryBound')
}
if (input >= 0 && input < 0.1) {
props.setcustomSlippageError('valid')
setWarningType('riskyEntryLow')
}
if (input >= 5) {
props.setcustomSlippageError('warning')
setWarningType('riskyEntryHigh')
}
updateSlippage(input)
}, 300)
}
const updateSlippage = newSlippage => {
let numParsed = parseFloat((newSlippage * 100).toFixed(2))
props.setRawSlippage(numParsed)
props.setRawTokenSlippage(numParsed)
}
const b = text => <Bold>{text}</Bold>
const renderTransactionDetails = () => {
ReactGA.event({
category: 'TransactionDetail',
action: 'Open'
})
if (props.independentField === props.INPUT) {
return props.sending ? (
<div>
<div>
{t('youAreSelling')}{' '}
{b(
`${amountFormatter(
props.independentValueParsed,
props.independentDecimals,
Math.min(4, props.independentDecimals)
)} ${props.inputSymbol}`
)}
.
</div>
<LastSummaryText>
{b(props.recipientAddress)} {t('willReceive')}{' '}
{b(
`${amountFormatter(
props.dependentValueMinumum,
props.dependentDecimals,
Math.min(4, props.dependentDecimals)
)} ${props.outputSymbol}`
)}{' '}
</LastSummaryText>
<LastSummaryText>
{t('priceChange')} {b(`${props.percentSlippageFormatted}%`)}.
</LastSummaryText>
</div>
) : (
<div>
<div>
{t('youAreSelling')}{' '}
{b(
`${amountFormatter(
props.independentValueParsed,
props.independentDecimals,
Math.min(4, props.independentDecimals)
)} ${props.inputSymbol}`
)}{' '}
{t('forAtLeast')}
{b(
`${amountFormatter(
props.dependentValueMinumum,
props.dependentDecimals,
Math.min(4, props.dependentDecimals)
)} ${props.outputSymbol}`
)}
.
</div>
<LastSummaryText>
{t('priceChange')} {b(`${props.percentSlippageFormatted}%`)}.
</LastSummaryText>
</div>
)
} else {
return props.sending ? (
<div>
<div>
{t('youAreSending')}{' '}
{b(
`${amountFormatter(
props.independentValueParsed,
props.independentDecimals,
Math.min(4, props.independentDecimals)
)} ${props.outputSymbol}`
)}{' '}
{t('to')} {b(props.recipientAddress)}.
</div>
<LastSummaryText>
{t('itWillCost')}{' '}
{b(
`${amountFormatter(
props.dependentValueMaximum,
props.dependentDecimals,
Math.min(4, props.dependentDecimals)
)} ${props.inputSymbol}`
)}{' '}
</LastSummaryText>
<LastSummaryText>
{t('priceChange')} {b(`${props.percentSlippageFormatted}%`)}.
</LastSummaryText>
</div>
) : (
<div>
<div>
{t('youAreBuying')}{' '}
{b(
`${amountFormatter(
props.independentValueParsed,
props.independentDecimals,
Math.min(4, props.independentDecimals)
)} ${props.outputSymbol}`
)}
.
</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>
)
}
}
return <>{renderSummary()}</>
}
...@@ -46,7 +46,7 @@ export default function App() { ...@@ -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() { ...@@ -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
......
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}
renderTransactionDetails={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 ${outputSymbol} = ${amountFormatter(exchangeRateInverted, 18, 4, false)} ${inputSymbol}`
: ' - '}
</span>
) : (
<span>
{exchangeRate
? `1 ${inputSymbol} = ${amountFormatter(exchangeRate, 18, 4, false)} ${outputSymbol}`
: ' - '}
</span>
)}
</ExchangeRateWrapper>
</OversizedPanel>
{renderSummary()}
<Flex>
<Button disabled={!isValid} onClick={onSwap} warning={highSlippageWarning}>
{highSlippageWarning ? t('sendAnyway') : t('send')}
</Button>
</Flex>
</>
)
} }
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 NewContextualInfo from '../../components/ContextualInfoNew'
import OversizedPanel from '../../components/OversizedPanel'
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 = 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 LastSummaryText = styled.div`
margin-top: 1rem;
`
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) {
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
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()
// 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
)
// 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}`
}
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>
{t('youWillReceive')}{' '}
{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('youAreBuying')}{' '}
{b(
`${amountFormatter(
independentValueParsed,
independentDecimals,
Math.min(4, independentDecimals)
)} ${outputSymbol}`
)}
.
</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 (!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)}
isError={isError}
slippageWarning={slippageWarning && !contextualInfo}
highSlippageWarning={highSlippageWarning && !contextualInfo}
renderTransactionDetails={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: '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)
})
}
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 ${outputSymbol} = ${amountFormatter(exchangeRateInverted, 18, 4, false)} ${inputSymbol}`
: ' - '}
</span>
) : (
<span>
{exchangeRate
? `1 ${inputSymbol} = ${amountFormatter(exchangeRate, 18, 4, false)} ${outputSymbol}`
: ' - '}
</span>
)}
</ExchangeRateWrapper>
</OversizedPanel>
{renderSummary()}
<Flex>
<Button disabled={!isValid} onClick={onSwap} warning={highSlippageWarning}>
{highSlippageWarning ? t('swapAnyway') : t('swap')}
</Button>
</Flex>
</>
)
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment