basic send added
This commit is contained in:
parent
4e2c5c1e84
commit
655b79569b
3
src/assets/svg/QR.svg
Normal file
3
src/assets/svg/QR.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.5 1.5H1.5V7.5H7.5V1.5ZM1.5 0H0V1.5V7.5V9H1.5H7.5H9V7.5V1.5V0H7.5H1.5ZM4.5 3H3V4.5V6H4.5H6V4.5V3H4.5ZM1.5 19.5V13.5H7.5V19.5H1.5ZM0 12H1.5H7.5H9V13.5V19.5V21H7.5H1.5H0V19.5V13.5V12ZM4.5 15H3V16.5V18H4.5H6V16.5V15H4.5ZM13.5 1.5H19.5V7.5H13.5V1.5ZM12 0H13.5H19.5H21V1.5V7.5V9H19.5H13.5H12V7.5V1.5V0ZM16.5 3H15V4.5V6H16.5H18V4.5V3H16.5ZM16.5 12H12V21H13.5V16.5H15V18H21V12H19.5V13.5H16.5V12ZM18 19.5H16.5V21H18V19.5ZM19.5 19.5H21V21H19.5V19.5Z" fill="black"/>
|
||||
</svg>
|
After Width: | Height: | Size: 611 B |
@ -89,6 +89,27 @@ export const ButtonEmpty = styled(Base)`
|
||||
}
|
||||
`
|
||||
|
||||
export const ButtonWhite = styled(Base)`
|
||||
border: 1px solid #edeef2;
|
||||
background-color: ${({ theme }) => theme.panelBackground};
|
||||
};
|
||||
color: black;
|
||||
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 1pt ${({ theme }) => darken(0.05, '#edeef2')};
|
||||
}
|
||||
&:hover {
|
||||
box-shadow: 0 0 0 1pt ${({ theme }) => darken(0.1, '#edeef2')};
|
||||
}
|
||||
&:active {
|
||||
box-shadow: 0 0 0 1pt ${({ theme }) => darken(0.1, '#edeef2')};
|
||||
}
|
||||
&:disabled {
|
||||
opacity: 50%;
|
||||
cursor: auto;
|
||||
}
|
||||
`
|
||||
|
||||
const ButtonConfirmedStyle = styled(Base)`
|
||||
background-color: ${({ theme }) => lighten(0.5, theme.connectedGreen)};
|
||||
border: 1px solid ${({ theme }) => theme.connectedGreen};
|
||||
@ -160,7 +181,7 @@ export function ButtonDropwdownLight({ disabled, children, ...rest }) {
|
||||
|
||||
export function ButtonRadio({ active, children, ...rest }) {
|
||||
if (!active) {
|
||||
return <ButtonEmpty {...rest}>{children}</ButtonEmpty>
|
||||
return <ButtonWhite {...rest}>{children}</ButtonWhite>
|
||||
} else {
|
||||
return <ButtonPrimary {...rest}>{children}</ButtonPrimary>
|
||||
}
|
||||
|
@ -48,10 +48,9 @@ const InputRow = styled.div`
|
||||
const CurrencySelect = styled.button`
|
||||
align-items: center;
|
||||
height: 2.2rem;
|
||||
|
||||
font-size: 20px;
|
||||
background-color: ${({ selected, theme }) => (selected ? theme.buttonBackgroundPlain : theme.zumthorBlue)};
|
||||
color: ${({ selected, theme }) => (selected ? theme.textColor : theme.royalBlue)};
|
||||
background-color: ${({ selected, theme }) => (selected ? theme.buttonBackgroundPlain : theme.royalBlue)};
|
||||
color: ${({ selected, theme }) => (selected ? theme.textColor : theme.white)};
|
||||
border: 1px solid
|
||||
${({ selected, theme, disableTokenSelect }) =>
|
||||
disableTokenSelect ? theme.buttonBackgroundPlain : selected ? theme.buttonOutlinePlain : theme.royalBlue};
|
||||
@ -70,7 +69,8 @@ const CurrencySelect = styled.button`
|
||||
}
|
||||
|
||||
:active {
|
||||
background-color: ${({ theme }) => theme.zumthorBlue};
|
||||
background-color: ${({ selected, theme }) =>
|
||||
selected ? darken(0.1, theme.zumthorBlue) : darken(0.1, theme.royalBlue)};
|
||||
}
|
||||
`
|
||||
|
||||
@ -85,20 +85,20 @@ const StyledDropDown = styled(DropDown)`
|
||||
height: 35%;
|
||||
|
||||
path {
|
||||
stroke: ${({ selected, theme }) => (selected ? theme.textColor : theme.royalBlue)};
|
||||
stroke: ${({ selected, theme }) => (selected ? theme.textColor : theme.white)};
|
||||
}
|
||||
`
|
||||
|
||||
const InputPanel = styled.div`
|
||||
${({ theme }) => theme.flexColumnNoWrap}
|
||||
position: relative;
|
||||
border-radius: 1.25rem;
|
||||
border-radius: ${({ hideInput }) => (hideInput ? '8px' : '20px')};
|
||||
background-color: ${({ theme }) => theme.inputBackground};
|
||||
z-index: 1;
|
||||
`
|
||||
|
||||
const Container = styled.div`
|
||||
border-radius: 1.25rem;
|
||||
border-radius: ${({ hideInput }) => (hideInput ? '8px' : '20px')};
|
||||
border: 1px solid ${({ error, theme }) => (error ? theme.salmonRed : theme.mercuryGray)};
|
||||
|
||||
background-color: ${({ theme }) => theme.inputBackground};
|
||||
@ -174,7 +174,10 @@ export default function CurrencyInputPanel({
|
||||
hideBalance = false,
|
||||
isExchange = false,
|
||||
exchange = null, // used for double token logo
|
||||
customBalance = null // used for LP balances instead of token balance
|
||||
customBalance = null, // used for LP balances instead of token balance
|
||||
hideInput = false,
|
||||
showSendWithSwap = false,
|
||||
onTokenSelectSendWithSwap = null
|
||||
}) {
|
||||
const { account, chainId } = useWeb3React()
|
||||
const { t } = useTranslation()
|
||||
@ -236,7 +239,7 @@ export default function CurrencyInputPanel({
|
||||
|
||||
return (
|
||||
<InputPanel>
|
||||
<Container error={!!error}>
|
||||
<Container error={!!error} hideInput={hideInput}>
|
||||
{!hideBalance && (
|
||||
<LabelRow>
|
||||
<RowBetween>
|
||||
@ -250,15 +253,19 @@ export default function CurrencyInputPanel({
|
||||
</RowBetween>
|
||||
</LabelRow>
|
||||
)}
|
||||
<InputRow>
|
||||
<NumericalInput
|
||||
value={value}
|
||||
onUserInput={val => {
|
||||
onUserInput(field, val)
|
||||
}}
|
||||
/>
|
||||
{!!token?.address && !atMax && <StyledBalanceMax onClick={onMax}>MAX</StyledBalanceMax>}
|
||||
{renderUnlockButton()}
|
||||
<InputRow style={hideInput ? { padding: '0', borderRadius: '8px' } : {}} hideInput={hideInput}>
|
||||
{!hideInput && (
|
||||
<>
|
||||
<NumericalInput
|
||||
value={value}
|
||||
onUserInput={val => {
|
||||
onUserInput(field, val)
|
||||
}}
|
||||
/>
|
||||
{!!token?.address && !atMax && <StyledBalanceMax onClick={onMax}>MAX</StyledBalanceMax>}
|
||||
{renderUnlockButton()}
|
||||
</>
|
||||
)}
|
||||
<CurrencySelect
|
||||
selected={!!token?.address}
|
||||
onClick={() => {
|
||||
@ -296,6 +303,8 @@ export default function CurrencyInputPanel({
|
||||
urlAddedTokens={urlAddedTokens}
|
||||
field={field}
|
||||
onTokenSelect={onTokenSelection}
|
||||
showSendWithSwap={showSendWithSwap}
|
||||
onTokenSelectSendWithSwap={onTokenSelectSendWithSwap}
|
||||
/>
|
||||
)}
|
||||
</InputPanel>
|
||||
|
@ -4,6 +4,7 @@ import { ethers } from 'ethers'
|
||||
import { parseUnits, parseEther } from '@ethersproject/units'
|
||||
import { WETH, TradeType, Route, Trade, TokenAmount, JSBI } from '@uniswap/sdk'
|
||||
|
||||
import QR from '../../assets/svg/QR.svg'
|
||||
import TokenLogo from '../TokenLogo'
|
||||
import QuestionHelper from '../Question'
|
||||
import NumericalInput from '../NumericalInput'
|
||||
@ -11,23 +12,23 @@ import ConfirmationModal from '../ConfirmationModal'
|
||||
import CurrencyInputPanel from '../CurrencyInputPanel'
|
||||
import { Link } from '../../theme/components'
|
||||
import { Text } from 'rebass'
|
||||
import ThemeProvider, { TYPE } from '../../theme'
|
||||
import { GreyCard } from '../../components/Card'
|
||||
import { TYPE } from '../../theme'
|
||||
import { GreyCard, LightCard } from '../../components/Card'
|
||||
import { ArrowDown, ArrowUp } from 'react-feather'
|
||||
import { ButtonPrimary, ButtonError, ButtonRadio } from '../Button'
|
||||
import { AutoColumn, ColumnCenter } from '../../components/Column'
|
||||
import { ButtonError, ButtonRadio } from '../Button'
|
||||
import Row, { RowBetween, RowFixed } from '../../components/Row'
|
||||
|
||||
import { usePopups } from '../../contexts/Application'
|
||||
import { useToken } from '../../contexts/Tokens'
|
||||
import { useExchange } from '../../contexts/Exchanges'
|
||||
import { useWeb3React } from '../../hooks'
|
||||
import { useWeb3React, useTokenContract } from '../../hooks'
|
||||
import { useAddressBalance } from '../../contexts/Balances'
|
||||
import { useTransactionAdder } from '../../contexts/Transactions'
|
||||
import { useAddressAllowance } from '../../contexts/Allowances'
|
||||
|
||||
import { ROUTER_ADDRESSES } from '../../constants'
|
||||
import { getRouterContract, calculateGasMargin } from '../../utils'
|
||||
import { getRouterContract, calculateGasMargin, isAddress, getProviderOrSigner } from '../../utils'
|
||||
|
||||
const Wrapper = styled.div`
|
||||
position: relative;
|
||||
@ -64,7 +65,66 @@ const InputWrapper = styled(RowBetween)`
|
||||
border-radius: 8px;
|
||||
padding: 4px 8px;
|
||||
border: 1px solid transparent;
|
||||
border: ${({ active, theme }) => active && '1px solid ' + theme.royalBlue};
|
||||
border: ${({ active, error, theme }) =>
|
||||
error ? '1px solid ' + theme.salmonRed : active ? '1px solid ' + theme.royalBlue : ''};
|
||||
`
|
||||
|
||||
const InputGroup = styled(AutoColumn)`
|
||||
position: relative;
|
||||
padding: 40px 0;
|
||||
`
|
||||
|
||||
const QRWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid ${({ theme }) => theme.outlineGrey};
|
||||
background: #fbfbfb;
|
||||
padding: 4px;
|
||||
border-radius: 8px;
|
||||
`
|
||||
|
||||
const StyledInput = styled.input`
|
||||
width: ${({ width }) => width};
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: 20px;
|
||||
|
||||
::placeholder {
|
||||
color: #edeef2;
|
||||
}
|
||||
`
|
||||
|
||||
const StyledNumerical = styled(NumericalInput)`
|
||||
text-align: center;
|
||||
font-size: 48px;
|
||||
font-weight: 500px;
|
||||
width: 100%;
|
||||
|
||||
::placeholder {
|
||||
color: #edeef2;
|
||||
}
|
||||
`
|
||||
|
||||
const MaxButton = styled.button`
|
||||
position: absolute;
|
||||
right: 70px;
|
||||
padding: 0.5rem 1rem;
|
||||
background-color: ${({ theme }) => theme.zumthorBlue};
|
||||
border: 1px solid ${({ theme }) => theme.zumthorBlue};
|
||||
border-radius: 0.5rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
margin-right: 0.5rem;
|
||||
color: ${({ theme }) => theme.royalBlue};
|
||||
:hover {
|
||||
border: 1px solid ${({ theme }) => theme.royalBlue};
|
||||
}
|
||||
:focus {
|
||||
border: 1px solid ${({ theme }) => theme.royalBlue};
|
||||
outline: none;
|
||||
}
|
||||
`
|
||||
|
||||
enum Field {
|
||||
@ -91,7 +151,7 @@ function initializeSwapState(inputAddress?: string, outputAddress?: string): Swa
|
||||
address: inputAddress
|
||||
},
|
||||
[Field.OUTPUT]: {
|
||||
address: '0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735'
|
||||
address: outputAddress
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -192,22 +252,28 @@ const INITIAL_ALLOWED_SLIPPAGE = 200
|
||||
const DEFAULT_DEADLINE_FROM_NOW = 60 * 15
|
||||
|
||||
// used for warning states based on slippage in bips
|
||||
const ALLOWED_IMPACT_MEDIUM = 100
|
||||
const ALLOWED_IMPACT_HIGH = 500
|
||||
const ALLOWED_SLIPPAGE_MEDIUM = 100
|
||||
const ALLOWED_SLIPPAGE_HIGH = 500
|
||||
|
||||
export default function ExchangePage() {
|
||||
export default function ExchangePage({ sendingInput = false }) {
|
||||
const { chainId, account, library } = useWeb3React()
|
||||
const routerAddress = ROUTER_ADDRESSES[chainId]
|
||||
|
||||
// adding notifications on txns
|
||||
const [, addPopup] = usePopups()
|
||||
const addTransaction = useTransactionAdder()
|
||||
|
||||
// sending state
|
||||
const [sending, setSending] = useState(sendingInput)
|
||||
const [sendingWithSwap, setSendingWithSwap] = useState(false)
|
||||
const [recipient, setRecipient] = useState('')
|
||||
|
||||
// input details
|
||||
const [state, dispatch] = useReducer(reducer, WETH[chainId].address, initializeSwapState)
|
||||
const { independentField, typedValue, ...fieldData } = state
|
||||
const dependentField = independentField === Field.INPUT ? Field.OUTPUT : Field.INPUT
|
||||
const tradeType = independentField === Field.INPUT ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT
|
||||
const [tradeError, setTradeError] = useState('') // error for thinsg liek reserve sizes
|
||||
const [tradeError, setTradeError] = useState('') // error for things like reserve size or route
|
||||
|
||||
const tokens = {
|
||||
[Field.INPUT]: useToken(fieldData[Field.INPUT].address),
|
||||
@ -217,12 +283,18 @@ export default function ExchangePage() {
|
||||
const exchange = useExchange(tokens[Field.INPUT], tokens[Field.OUTPUT])
|
||||
const route = !!exchange ? new Route([exchange], tokens[Field.INPUT]) : undefined
|
||||
|
||||
// modal state
|
||||
const addTransaction = useTransactionAdder()
|
||||
const [showConfirm, setShowConfirm] = useState(true)
|
||||
// modal and loading
|
||||
const [showConfirm, setShowConfirm] = useState(false)
|
||||
const [pendingConfirmation, setPendingConfirmation] = useState(true) // waiting for user confirmation
|
||||
const [attemptingTxn, setAttemptingTxn] = useState(false) // clicked confirmed
|
||||
|
||||
// advanced settings
|
||||
const [showAdvanced, setShowAdvanced] = useState(false)
|
||||
const [activeIndex, setActiveIndex] = useState(SLIPPAGE_INDEX[3])
|
||||
const [customSlippage, setCustomSlippage] = useState()
|
||||
const [customDeadline, setCustomDeadline] = useState(DEFAULT_DEADLINE_FROM_NOW / 60)
|
||||
const [slippageInputError, setSlippageInputError] = useState(null)
|
||||
|
||||
// txn values
|
||||
const [txHash, setTxHash] = useState()
|
||||
const [deadline, setDeadline] = useState(DEFAULT_DEADLINE_FROM_NOW)
|
||||
@ -240,10 +312,9 @@ export default function ExchangePage() {
|
||||
|
||||
const parsedAmounts: { [field: number]: TokenAmount } = {}
|
||||
// try to parse typed value
|
||||
// if (typedValue !== '' && typedValue !== '.' && tokens[independentField]) {
|
||||
if (tokens[independentField]) {
|
||||
if (typedValue !== '' && typedValue !== '.' && tokens[independentField]) {
|
||||
try {
|
||||
const typedValueParsed = parseUnits('0.0001', tokens[independentField].decimals).toString()
|
||||
const typedValueParsed = parseUnits(typedValue, tokens[independentField].decimals).toString()
|
||||
if (typedValueParsed !== '0')
|
||||
parsedAmounts[independentField] = new TokenAmount(tokens[independentField], typedValueParsed)
|
||||
} catch (error) {
|
||||
@ -313,7 +384,7 @@ export default function ExchangePage() {
|
||||
})
|
||||
}, [])
|
||||
|
||||
const MIN_ETHER = new TokenAmount(WETH[chainId], JSBI.BigInt(parseEther('.01')))
|
||||
const MIN_ETHER = chainId && new TokenAmount(WETH[chainId], JSBI.BigInt(parseEther('.01')))
|
||||
const maxAmountInput =
|
||||
!!userBalances[Field.INPUT] &&
|
||||
JSBI.greaterThan(
|
||||
@ -388,16 +459,14 @@ export default function ExchangePage() {
|
||||
outputApproval &&
|
||||
JSBI.greaterThan(parsedAmounts[Field.OUTPUT].raw, outputApproval.raw)
|
||||
|
||||
// modal state
|
||||
const [showAdvanced, setShowAdvanced] = useState(true)
|
||||
const [activeIndex, setActiveIndex] = useState(SLIPPAGE_INDEX[3])
|
||||
const [customSlippage, setCustomSlippage] = useState()
|
||||
const [customDeadline, setCustomDeadline] = useState(DEFAULT_DEADLINE_FROM_NOW / 60)
|
||||
|
||||
const [slippageInputError, setSlippageInputError] = useState(null)
|
||||
|
||||
// parse the input for custom slippage
|
||||
function parseCustomInput(val) {
|
||||
const acceptableValues = [/^$/, /^\d{1,2}$/, /^\d{0,2}\.\d{0,2}$/]
|
||||
if (val > 5) {
|
||||
setSlippageInputError('Your transaction may be front-run.')
|
||||
} else {
|
||||
setSlippageInputError(null)
|
||||
}
|
||||
if (acceptableValues.some(a => a.test(val))) {
|
||||
setCustomSlippage(val)
|
||||
setAllowedSlippage(val * 100)
|
||||
@ -412,6 +481,62 @@ export default function ExchangePage() {
|
||||
}
|
||||
}
|
||||
|
||||
const tokenContract = useTokenContract(tokens[Field.INPUT]?.address)
|
||||
|
||||
// function for a pure send
|
||||
async function onSend() {
|
||||
setAttemptingTxn(true)
|
||||
|
||||
const signer = await getProviderOrSigner(library, account)
|
||||
// get token contract if needed
|
||||
let estimate: Function, method: Function, args, value
|
||||
if (tokens[Field.INPUT] === WETH[chainId]) {
|
||||
signer
|
||||
.sendTransaction({ to: recipient.toString(), value: hex(parsedAmounts[Field.INPUT].raw) })
|
||||
.then(response => {
|
||||
console.log(response)
|
||||
setTxHash(response.hash)
|
||||
addTransaction(response)
|
||||
setPendingConfirmation(false)
|
||||
})
|
||||
.catch(e => {
|
||||
addPopup(
|
||||
<AutoColumn gap="10px">
|
||||
<Text>Transaction Failed: try again.</Text>
|
||||
</AutoColumn>
|
||||
)
|
||||
resetModal()
|
||||
setShowConfirm(false)
|
||||
})
|
||||
} else {
|
||||
estimate = tokenContract.estimate.transfer
|
||||
method = tokenContract.transfer
|
||||
args = [recipient, parsedAmounts[Field.INPUT].raw.toString()]
|
||||
value = ethers.constants.Zero
|
||||
const estimatedGasLimit = await estimate(...args, { value }).catch(e => {
|
||||
console.log('error getting gas limit')
|
||||
})
|
||||
method(...args, {
|
||||
value,
|
||||
gasLimit: calculateGasMargin(estimatedGasLimit, GAS_MARGIN)
|
||||
})
|
||||
.then(response => {
|
||||
setTxHash(response.hash)
|
||||
addTransaction(response)
|
||||
setPendingConfirmation(false)
|
||||
})
|
||||
.catch(e => {
|
||||
addPopup(
|
||||
<AutoColumn gap="10px">
|
||||
<Text>Transaction Failed: try again.</Text>
|
||||
</AutoColumn>
|
||||
)
|
||||
resetModal()
|
||||
setShowConfirm(false)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function onSwap() {
|
||||
const routerContract = getRouterContract(chainId, library, account)
|
||||
setAttemptingTxn(true)
|
||||
@ -497,7 +622,7 @@ export default function ExchangePage() {
|
||||
gasLimit: calculateGasMargin(estimatedGasLimit, GAS_MARGIN)
|
||||
})
|
||||
.then(response => {
|
||||
setTxHash(response)
|
||||
setTxHash(response.hash)
|
||||
addTransaction(response)
|
||||
setPendingConfirmation(false)
|
||||
})
|
||||
@ -513,17 +638,38 @@ export default function ExchangePage() {
|
||||
}
|
||||
|
||||
// errors
|
||||
const [generalError, setGeneralError] = useState('')
|
||||
const [inputError, setInputError] = useState('')
|
||||
const [outputError, setOutputError] = useState('')
|
||||
const [recipientError, setRecipientError] = useState('')
|
||||
const [isValid, setIsValid] = useState(false)
|
||||
|
||||
const ignoreOutput = sending ? !sendingWithSwap : false
|
||||
|
||||
useEffect(() => {
|
||||
// reset errors
|
||||
setGeneralError(null)
|
||||
setInputError(null)
|
||||
setOutputError(null)
|
||||
setTradeError(null)
|
||||
setRecipientError(null)
|
||||
setIsValid(true)
|
||||
|
||||
if (!isAddress(recipient) && sending) {
|
||||
setRecipientError('Invalid Recipient')
|
||||
setIsValid(false)
|
||||
}
|
||||
|
||||
if (!parsedAmounts[Field.INPUT]) {
|
||||
setGeneralError('Enter an amount')
|
||||
setIsValid(false)
|
||||
}
|
||||
|
||||
if (!parsedAmounts[Field.OUTPUT] && !ignoreOutput) {
|
||||
setGeneralError('Enter an amount')
|
||||
setIsValid(false)
|
||||
}
|
||||
|
||||
if (
|
||||
parsedAmounts[Field.INPUT] &&
|
||||
exchange &&
|
||||
@ -534,6 +680,7 @@ export default function ExchangePage() {
|
||||
}
|
||||
|
||||
if (
|
||||
!ignoreOutput &&
|
||||
parsedAmounts[Field.OUTPUT] &&
|
||||
exchange &&
|
||||
JSBI.greaterThan(parsedAmounts[Field.OUTPUT].raw, exchange.reserveOf(tokens[Field.OUTPUT]).raw)
|
||||
@ -542,12 +689,12 @@ export default function ExchangePage() {
|
||||
setIsValid(false)
|
||||
}
|
||||
|
||||
if (showInputUnlock) {
|
||||
if (showInputUnlock && !(sending && !sendingWithSwap)) {
|
||||
setInputError('Approval Needed')
|
||||
setIsValid(false)
|
||||
}
|
||||
|
||||
if (showOutputUnlock) {
|
||||
if (showOutputUnlock && !ignoreOutput) {
|
||||
setOutputError('Approval Needed')
|
||||
setIsValid(false)
|
||||
}
|
||||
@ -560,19 +707,22 @@ export default function ExchangePage() {
|
||||
setInputError('Insufficient balance.')
|
||||
setIsValid(false)
|
||||
}
|
||||
}, [
|
||||
exchange,
|
||||
ignoreOutput,
|
||||
parsedAmounts,
|
||||
recipient,
|
||||
sending,
|
||||
sendingWithSwap,
|
||||
showInputUnlock,
|
||||
showOutputUnlock,
|
||||
tokens,
|
||||
userBalances
|
||||
])
|
||||
|
||||
if (
|
||||
userBalances[Field.OUTPUT] &&
|
||||
parsedAmounts[Field.OUTPUT] &&
|
||||
JSBI.lessThan(userBalances[Field.OUTPUT].raw, parsedAmounts[Field.OUTPUT]?.raw)
|
||||
) {
|
||||
setOutputError('Insufficient balance.')
|
||||
setIsValid(false)
|
||||
}
|
||||
}, [exchange, parsedAmounts, showInputUnlock, showOutputUnlock, tokens, userBalances])
|
||||
|
||||
const warningMedium = slippageFromTrade && parseFloat(slippageFromTrade.toFixed(4)) > ALLOWED_IMPACT_MEDIUM / 100
|
||||
const warningHigh = slippageFromTrade && parseFloat(slippageFromTrade.toFixed(4)) > ALLOWED_IMPACT_HIGH / 100
|
||||
// warnings on slippage
|
||||
const warningMedium = slippageFromTrade && parseFloat(slippageFromTrade.toFixed(4)) > ALLOWED_SLIPPAGE_MEDIUM / 100
|
||||
const warningHigh = slippageFromTrade && parseFloat(slippageFromTrade.toFixed(4)) > ALLOWED_SLIPPAGE_HIGH / 100
|
||||
|
||||
function resetModal() {
|
||||
setPendingConfirmation(true)
|
||||
@ -581,169 +731,221 @@ export default function ExchangePage() {
|
||||
}
|
||||
|
||||
function modalHeader() {
|
||||
return (
|
||||
<AutoColumn gap={'20px'} style={{ marginTop: '40px' }}>
|
||||
<RowBetween align="flex-end">
|
||||
<Text fontSize={36} fontWeight={500}>
|
||||
{!!slippageAdjustedAmounts[Field.INPUT] && slippageAdjustedAmounts[Field.INPUT].toSignificant(6)}
|
||||
</Text>
|
||||
<RowFixed gap="10px">
|
||||
<TokenLogo address={tokens[Field.INPUT]?.address} size={'24px'} />
|
||||
<Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}>
|
||||
{tokens[Field.INPUT]?.symbol || ''}
|
||||
if (sending && !sendingWithSwap) {
|
||||
return (
|
||||
<AutoColumn gap="30px" style={{ marginTop: '40px' }}>
|
||||
<RowBetween>
|
||||
<Text fontSize={36} fontWeight={500}>
|
||||
{parsedAmounts[Field.INPUT]?.toFixed(8)}
|
||||
</Text>
|
||||
</RowFixed>
|
||||
</RowBetween>
|
||||
<RowFixed>
|
||||
<ArrowDown size="16" color="#888D9B" />
|
||||
</RowFixed>
|
||||
<RowBetween align="flex-end">
|
||||
<Text fontSize={36} fontWeight={500} color={warningHigh ? '#FF6871' : '#2172E5'}>
|
||||
{!!slippageAdjustedAmounts[Field.OUTPUT] && slippageAdjustedAmounts[Field.OUTPUT].toSignificant(6)}
|
||||
</Text>
|
||||
<RowFixed gap="10px">
|
||||
<TokenLogo address={tokens[Field.OUTPUT]?.address} size={'24px'} />
|
||||
<Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}>
|
||||
{tokens[Field.OUTPUT]?.symbol || ''}
|
||||
<TokenLogo address={tokens[Field.INPUT]?.address} size={'30px'} />
|
||||
</RowBetween>
|
||||
<ArrowDown size={24} color="#888D9B" />
|
||||
<TYPE.blue fontSize={36}>
|
||||
{recipient?.slice(0, 6)}...{recipient?.slice(36, 42)}
|
||||
</TYPE.blue>
|
||||
</AutoColumn>
|
||||
)
|
||||
}
|
||||
|
||||
if (sending && sendingWithSwap) {
|
||||
}
|
||||
|
||||
if (!sending) {
|
||||
return (
|
||||
<AutoColumn gap={'20px'} style={{ marginTop: '40px' }}>
|
||||
<RowBetween align="flex-end">
|
||||
<Text fontSize={36} fontWeight={500}>
|
||||
{!!slippageAdjustedAmounts[Field.INPUT] && slippageAdjustedAmounts[Field.INPUT].toSignificant(6)}
|
||||
</Text>
|
||||
<RowFixed gap="10px">
|
||||
<TokenLogo address={tokens[Field.INPUT]?.address} size={'24px'} />
|
||||
<Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}>
|
||||
{tokens[Field.INPUT]?.symbol || ''}
|
||||
</Text>
|
||||
</RowFixed>
|
||||
</RowBetween>
|
||||
<RowFixed>
|
||||
<ArrowDown size="16" color="#888D9B" />
|
||||
</RowFixed>
|
||||
</RowBetween>
|
||||
</AutoColumn>
|
||||
)
|
||||
<RowBetween align="flex-end">
|
||||
<Text fontSize={36} fontWeight={500} color={warningHigh ? '#FF6871' : '#2172E5'}>
|
||||
{!!slippageAdjustedAmounts[Field.OUTPUT] && slippageAdjustedAmounts[Field.OUTPUT].toSignificant(6)}
|
||||
</Text>
|
||||
<RowFixed gap="10px">
|
||||
<TokenLogo address={tokens[Field.OUTPUT]?.address} size={'24px'} />
|
||||
<Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}>
|
||||
{tokens[Field.OUTPUT]?.symbol || ''}
|
||||
</Text>
|
||||
</RowFixed>
|
||||
</RowBetween>
|
||||
</AutoColumn>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function modalBottom() {
|
||||
return showAdvanced ? (
|
||||
<AutoColumn gap="20px">
|
||||
<Link
|
||||
onClick={() => {
|
||||
setShowAdvanced(false)
|
||||
}}
|
||||
>
|
||||
back
|
||||
</Link>
|
||||
<RowBetween>
|
||||
<TYPE.main>Limit additional price slippage</TYPE.main>
|
||||
<QuestionHelper text="" />
|
||||
</RowBetween>
|
||||
<Row>
|
||||
<ButtonRadio
|
||||
active={SLIPPAGE_INDEX[1] === activeIndex}
|
||||
padding="4px 6px"
|
||||
borderRadius="8px"
|
||||
style={{ marginRight: '16px' }}
|
||||
width={'60px'}
|
||||
onClick={() => {
|
||||
setActiveIndex(SLIPPAGE_INDEX[1])
|
||||
setAllowedSlippage(10)
|
||||
}}
|
||||
>
|
||||
0.1%
|
||||
</ButtonRadio>
|
||||
<ButtonRadio
|
||||
active={SLIPPAGE_INDEX[2] === activeIndex}
|
||||
padding="4px 6px"
|
||||
borderRadius="8px"
|
||||
style={{ marginRight: '16px' }}
|
||||
width={'60px'}
|
||||
onClick={() => {
|
||||
setActiveIndex(SLIPPAGE_INDEX[2])
|
||||
setAllowedSlippage(100)
|
||||
}}
|
||||
>
|
||||
1%
|
||||
</ButtonRadio>
|
||||
<ButtonRadio
|
||||
active={SLIPPAGE_INDEX[3] === activeIndex}
|
||||
padding="4px"
|
||||
borderRadius="8px"
|
||||
width={'140px'}
|
||||
onClick={() => {
|
||||
setActiveIndex(SLIPPAGE_INDEX[3])
|
||||
setAllowedSlippage(200)
|
||||
}}
|
||||
>
|
||||
2% (suggested)
|
||||
</ButtonRadio>
|
||||
</Row>
|
||||
<RowFixed>
|
||||
<InputWrapper active={SLIPPAGE_INDEX[4] === activeIndex}>
|
||||
<NumericalInput
|
||||
align={customSlippage ? 'right' : 'left'}
|
||||
value={customSlippage || ''}
|
||||
onUserInput={val => {
|
||||
parseCustomInput(val)
|
||||
setActiveIndex(SLIPPAGE_INDEX[4])
|
||||
}}
|
||||
placeHolder="Custom"
|
||||
onClick={() => {
|
||||
setActiveIndex(SLIPPAGE_INDEX[4])
|
||||
if (customSlippage) {
|
||||
parseCustomInput(customSlippage)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
%
|
||||
</InputWrapper>
|
||||
</RowFixed>
|
||||
<RowBetween>
|
||||
<TYPE.main>Adjust deadline (minutes from now)</TYPE.main>
|
||||
</RowBetween>
|
||||
<RowFixed>
|
||||
<NumericalInput
|
||||
value={customDeadline}
|
||||
onUserInput={val => {
|
||||
parseCustomDeadline(val)
|
||||
}}
|
||||
/>
|
||||
</RowFixed>
|
||||
</AutoColumn>
|
||||
) : (
|
||||
<>
|
||||
<RowBetween>
|
||||
<Text color="#565A69" fontWeight={500} fontSize={16}>
|
||||
Price
|
||||
</Text>
|
||||
<Text fontWeight={500} fontSize={16}>
|
||||
{`1 ${tokens[Field.INPUT]?.symbol} = ${route && route.midPrice && route.midPrice.adjusted.toFixed(8)} ${
|
||||
tokens[Field.OUTPUT]?.symbol
|
||||
}`}
|
||||
</Text>
|
||||
</RowBetween>
|
||||
<RowBetween>
|
||||
<Text color="#565A69" fontWeight={500} fontSize={16}>
|
||||
Slippage <Link onClick={() => setShowAdvanced(true)}>(edit limits)</Link>
|
||||
</Text>
|
||||
<ErrorText warningHigh={warningHigh} fontWeight={500}>
|
||||
{slippageFromTrade && slippageFromTrade.toFixed(4)}%
|
||||
</ErrorText>
|
||||
</RowBetween>
|
||||
<ButtonError onClick={onSwap} error={!!warningHigh} style={{ margin: '10px 0' }}>
|
||||
<Text fontSize={20} fontWeight={500}>
|
||||
{warningHigh ? 'Swap Anyway' : 'Swap'}
|
||||
</Text>
|
||||
</ButtonError>
|
||||
<Text fontSize={12} color="#565A69" textAlign="center">
|
||||
{`Output is estimated. You will receive at least ${slippageAdjustedAmounts[Field.OUTPUT]?.toSignificant(6)} ${
|
||||
tokens[Field.OUTPUT]?.symbol
|
||||
} or the transaction will revert.`}
|
||||
</Text>
|
||||
<AutoColumn justify="center">
|
||||
if (sending && !sendingWithSwap) {
|
||||
return (
|
||||
<AutoColumn>
|
||||
<ButtonPrimary onClick={onSend}>
|
||||
<Text color="white" fontSize={20}>
|
||||
Confirm send
|
||||
</Text>
|
||||
</ButtonPrimary>
|
||||
</AutoColumn>
|
||||
)
|
||||
}
|
||||
|
||||
if (sending && sendingWithSwap) {
|
||||
}
|
||||
|
||||
if (showAdvanced) {
|
||||
return (
|
||||
<AutoColumn gap="20px">
|
||||
<Link
|
||||
onClick={() => {
|
||||
setShowAdvanced(true)
|
||||
setShowAdvanced(false)
|
||||
}}
|
||||
>
|
||||
Advanced Options
|
||||
back
|
||||
</Link>
|
||||
<RowBetween>
|
||||
<TYPE.main>Limit additional price impact</TYPE.main>
|
||||
<QuestionHelper text="" />
|
||||
</RowBetween>
|
||||
<Row>
|
||||
<ButtonRadio
|
||||
active={SLIPPAGE_INDEX[1] === activeIndex}
|
||||
padding="4px 6px"
|
||||
borderRadius="8px"
|
||||
style={{ marginRight: '16px' }}
|
||||
width={'60px'}
|
||||
onClick={() => {
|
||||
setActiveIndex(SLIPPAGE_INDEX[1])
|
||||
setAllowedSlippage(10)
|
||||
}}
|
||||
>
|
||||
0.1%
|
||||
</ButtonRadio>
|
||||
<ButtonRadio
|
||||
active={SLIPPAGE_INDEX[2] === activeIndex}
|
||||
padding="4px 6px"
|
||||
borderRadius="8px"
|
||||
style={{ marginRight: '16px' }}
|
||||
width={'60px'}
|
||||
onClick={() => {
|
||||
setActiveIndex(SLIPPAGE_INDEX[2])
|
||||
setAllowedSlippage(100)
|
||||
}}
|
||||
>
|
||||
1%
|
||||
</ButtonRadio>
|
||||
<ButtonRadio
|
||||
active={SLIPPAGE_INDEX[3] === activeIndex}
|
||||
padding="4px"
|
||||
borderRadius="8px"
|
||||
width={'140px'}
|
||||
onClick={() => {
|
||||
setActiveIndex(SLIPPAGE_INDEX[3])
|
||||
setAllowedSlippage(200)
|
||||
}}
|
||||
>
|
||||
2% (suggested)
|
||||
</ButtonRadio>
|
||||
</Row>
|
||||
<RowFixed>
|
||||
<InputWrapper active={SLIPPAGE_INDEX[4] === activeIndex} error={slippageInputError}>
|
||||
<NumericalInput
|
||||
align={customSlippage ? 'right' : 'left'}
|
||||
value={customSlippage || ''}
|
||||
onUserInput={val => {
|
||||
parseCustomInput(val)
|
||||
setActiveIndex(SLIPPAGE_INDEX[4])
|
||||
}}
|
||||
placeHolder="Custom"
|
||||
onClick={() => {
|
||||
setActiveIndex(SLIPPAGE_INDEX[4])
|
||||
if (customSlippage) {
|
||||
parseCustomInput(customSlippage)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
%
|
||||
</InputWrapper>
|
||||
{slippageInputError && (
|
||||
<TYPE.error error={true} fontSize={12} style={{ marginLeft: '10px' }}>
|
||||
Your transaction may be front-run
|
||||
</TYPE.error>
|
||||
)}
|
||||
</RowFixed>
|
||||
<RowBetween>
|
||||
<TYPE.main>Adjust deadline (minutes from now)</TYPE.main>
|
||||
</RowBetween>
|
||||
<RowFixed>
|
||||
<InputWrapper>
|
||||
<NumericalInput
|
||||
value={customDeadline}
|
||||
onUserInput={val => {
|
||||
parseCustomDeadline(val)
|
||||
}}
|
||||
/>
|
||||
</InputWrapper>
|
||||
</RowFixed>
|
||||
</AutoColumn>
|
||||
</>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (!sending) {
|
||||
return (
|
||||
<>
|
||||
<RowBetween>
|
||||
<Text color="#565A69" fontWeight={500} fontSize={16}>
|
||||
Price
|
||||
</Text>
|
||||
<Text fontWeight={500} fontSize={16}>
|
||||
{`1 ${tokens[Field.INPUT]?.symbol} = ${route && route.midPrice && route.midPrice.adjusted.toFixed(8)} ${
|
||||
tokens[Field.OUTPUT]?.symbol
|
||||
}`}
|
||||
</Text>
|
||||
</RowBetween>
|
||||
<RowBetween>
|
||||
<Text color="#565A69" fontWeight={500} fontSize={16}>
|
||||
Slippage <Link onClick={() => setShowAdvanced(true)}>(edit limits)</Link>
|
||||
</Text>
|
||||
<ErrorText warningHigh={warningHigh} fontWeight={500}>
|
||||
{slippageFromTrade && slippageFromTrade.toFixed(4)}%
|
||||
</ErrorText>
|
||||
</RowBetween>
|
||||
<ButtonError onClick={onSwap} error={!!warningHigh} style={{ margin: '10px 0' }}>
|
||||
<Text fontSize={20} fontWeight={500}>
|
||||
{warningHigh ? 'Swap Anyway' : 'Swap'}
|
||||
</Text>
|
||||
</ButtonError>
|
||||
<AutoColumn justify="center" gap="20px">
|
||||
<TYPE.italic textAlign="center" style={{ width: '80%' }}>
|
||||
{`Output is estimated. You will receive at least ${slippageAdjustedAmounts[Field.OUTPUT]?.toSignificant(
|
||||
6
|
||||
)} ${tokens[Field.OUTPUT]?.symbol} or the transaction will revert.`}
|
||||
</TYPE.italic>
|
||||
<Link
|
||||
onClick={() => {
|
||||
setShowAdvanced(true)
|
||||
}}
|
||||
>
|
||||
Advanced Options
|
||||
</Link>
|
||||
</AutoColumn>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const pendingText = ` Swapped ${parsedAmounts[Field.INPUT]?.toSignificant(6)} ${
|
||||
tokens[Field.INPUT]?.symbol
|
||||
} for ${parsedAmounts[Field.OUTPUT]?.toSignificant(6)} ${tokens[Field.OUTPUT]?.symbol}`
|
||||
const pendingText = sending
|
||||
? `Sending ${parsedAmounts[Field.INPUT]?.toSignificant(6)} ${tokens[Field.INPUT]?.symbol} to ${recipient}`
|
||||
: ` Swapped ${parsedAmounts[Field.INPUT]?.toSignificant(6)} ${tokens[Field.INPUT]?.symbol} for ${parsedAmounts[
|
||||
Field.OUTPUT
|
||||
]?.toSignificant(6)} ${tokens[Field.OUTPUT]?.symbol}`
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
@ -756,69 +958,128 @@ export default function ExchangePage() {
|
||||
attemptingTxn={attemptingTxn}
|
||||
pendingConfirmation={pendingConfirmation}
|
||||
hash={txHash ? txHash : ''}
|
||||
topContent={() => modalHeader()}
|
||||
topContent={modalHeader}
|
||||
bottomContent={modalBottom}
|
||||
pendingText={pendingText}
|
||||
title="Confirm Swap"
|
||||
title={sendingWithSwap ? 'Confirm swap and send' : sending ? 'Confirm Send' : 'Confirm Swap'}
|
||||
/>
|
||||
|
||||
{sending && !sendingWithSwap && (
|
||||
<>
|
||||
<InputGroup gap="24px" justify="center">
|
||||
{!atMaxAmountInput && (
|
||||
<MaxButton
|
||||
onClick={() => {
|
||||
maxAmountInput && onMaxInput(maxAmountInput.toExact())
|
||||
}}
|
||||
>
|
||||
Max
|
||||
</MaxButton>
|
||||
)}
|
||||
<StyledNumerical value={formattedAmounts[Field.INPUT]} onUserInput={val => onUserInput(Field.INPUT, val)} />
|
||||
{!parsedAmounts[Field.INPUT] && <TYPE.gray>Enter an amount.</TYPE.gray>}
|
||||
<CurrencyInputPanel
|
||||
field={Field.INPUT}
|
||||
value={formattedAmounts[Field.INPUT]}
|
||||
onUserInput={val => onUserInput(Field.INPUT, val)}
|
||||
onMax={() => {
|
||||
maxAmountInput && onMaxInput(maxAmountInput.toExact())
|
||||
}}
|
||||
atMax={atMaxAmountInput}
|
||||
token={tokens[Field.INPUT]}
|
||||
onTokenSelection={address => onTokenSelection(Field.INPUT, address)}
|
||||
onTokenSelectSendWithSwap={address => {
|
||||
onTokenSelection(Field.OUTPUT, address)
|
||||
setSendingWithSwap(true)
|
||||
}}
|
||||
title={'Input'}
|
||||
error={inputError}
|
||||
exchange={exchange}
|
||||
showUnlock={showInputUnlock}
|
||||
hideBalance={true}
|
||||
hideInput={true}
|
||||
showSendWithSwap={true}
|
||||
/>
|
||||
</InputGroup>
|
||||
</>
|
||||
)}
|
||||
|
||||
<AutoColumn gap={'20px'}>
|
||||
<CurrencyInputPanel
|
||||
field={Field.INPUT}
|
||||
value={formattedAmounts[Field.INPUT]}
|
||||
onUserInput={onUserInput}
|
||||
onMax={() => {
|
||||
maxAmountInput && onMaxInput(maxAmountInput.toExact())
|
||||
}}
|
||||
atMax={atMaxAmountInput}
|
||||
token={tokens[Field.INPUT]}
|
||||
onTokenSelection={address => onTokenSelection(Field.INPUT, address)}
|
||||
title={'Input'}
|
||||
error={inputError}
|
||||
exchange={exchange}
|
||||
showUnlock={showInputUnlock}
|
||||
/>
|
||||
<ColumnCenter>
|
||||
<ArrowWrapper onClick={onSwapTokens}>
|
||||
<ArrowDown size="16" color="#2F80ED" />
|
||||
<ArrowUp size="16" color="#2F80ED" />
|
||||
</ArrowWrapper>
|
||||
</ColumnCenter>
|
||||
<CurrencyInputPanel
|
||||
field={Field.OUTPUT}
|
||||
value={formattedAmounts[Field.OUTPUT]}
|
||||
onUserInput={onUserInput}
|
||||
onMax={() => {
|
||||
maxAmountOutput && onMaxOutput(maxAmountOutput.toExact())
|
||||
}}
|
||||
atMax={atMaxAmountOutput}
|
||||
token={tokens[Field.OUTPUT]}
|
||||
onTokenSelection={address => onTokenSelection(Field.OUTPUT, address)}
|
||||
title={'Output'}
|
||||
error={outputError}
|
||||
exchange={exchange}
|
||||
showUnlock={showOutputUnlock}
|
||||
/>
|
||||
<RowBetween>
|
||||
<Text fontWeight={500} color="#565A69">
|
||||
Price
|
||||
</Text>
|
||||
<Text fontWeight={500} color="#565A69">
|
||||
{exchange
|
||||
? `1 ${tokens[Field.INPUT].symbol} = ${route?.midPrice.toSignificant(6)} ${tokens[Field.OUTPUT].symbol}`
|
||||
: '-'}
|
||||
</Text>
|
||||
</RowBetween>
|
||||
{warningMedium && (
|
||||
<RowBetween>
|
||||
<Text fontWeight={500} color="#565A69">
|
||||
Slippage
|
||||
</Text>
|
||||
<ErrorText fontWeight={500} warningMedium={warningMedium} warningHigh={warningHigh}>
|
||||
{slippageFromTrade.toFixed(4)}%
|
||||
</ErrorText>
|
||||
</RowBetween>
|
||||
{(!sending || sendingWithSwap) && (
|
||||
<>
|
||||
<CurrencyInputPanel
|
||||
field={Field.INPUT}
|
||||
value={formattedAmounts[Field.INPUT]}
|
||||
onUserInput={onUserInput}
|
||||
onMax={() => {
|
||||
maxAmountInput && onMaxInput(maxAmountInput.toExact())
|
||||
}}
|
||||
atMax={atMaxAmountInput}
|
||||
token={tokens[Field.INPUT]}
|
||||
onTokenSelection={address => onTokenSelection(Field.INPUT, address)}
|
||||
title={'Input'}
|
||||
error={inputError}
|
||||
exchange={exchange}
|
||||
showUnlock={showInputUnlock}
|
||||
/>
|
||||
<ColumnCenter>
|
||||
<ArrowWrapper onClick={onSwapTokens}>
|
||||
<ArrowDown size="16" color="#2F80ED" />
|
||||
<ArrowUp size="16" color="#2F80ED" />
|
||||
</ArrowWrapper>
|
||||
</ColumnCenter>
|
||||
<CurrencyInputPanel
|
||||
field={Field.OUTPUT}
|
||||
value={formattedAmounts[Field.OUTPUT]}
|
||||
onUserInput={onUserInput}
|
||||
onMax={() => {
|
||||
maxAmountOutput && onMaxOutput(maxAmountOutput.toExact())
|
||||
}}
|
||||
atMax={atMaxAmountOutput}
|
||||
token={tokens[Field.OUTPUT]}
|
||||
onTokenSelection={address => onTokenSelection(Field.OUTPUT, address)}
|
||||
title={'Output'}
|
||||
error={outputError}
|
||||
exchange={exchange}
|
||||
showUnlock={showOutputUnlock}
|
||||
/>
|
||||
<RowBetween>
|
||||
<Text fontWeight={500} color="#565A69">
|
||||
Price
|
||||
</Text>
|
||||
<Text fontWeight={500} color="#565A69">
|
||||
{exchange
|
||||
? `1 ${tokens[Field.INPUT].symbol} = ${route?.midPrice.toSignificant(6)} ${
|
||||
tokens[Field.OUTPUT].symbol
|
||||
}`
|
||||
: '-'}
|
||||
</Text>
|
||||
</RowBetween>
|
||||
{warningMedium && (
|
||||
<RowBetween>
|
||||
<Text fontWeight={500} color="#565A69">
|
||||
Slippage
|
||||
</Text>
|
||||
<ErrorText fontWeight={500} warningMedium={warningMedium} warningHigh={warningHigh}>
|
||||
{slippageFromTrade.toFixed(4)}%
|
||||
</ErrorText>
|
||||
</RowBetween>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{sending && (
|
||||
<AutoColumn gap="10px">
|
||||
<LightCard borderRadius={'20px'}>
|
||||
<RowBetween>
|
||||
<StyledInput placeholder="Recipient Address" onChange={e => setRecipient(e.target.value)} />
|
||||
<QRWrapper>
|
||||
<img src={QR} alt="" />
|
||||
</QRWrapper>
|
||||
</RowBetween>
|
||||
</LightCard>
|
||||
</AutoColumn>
|
||||
)}
|
||||
<ButtonError
|
||||
onClick={() => {
|
||||
setShowConfirm(true)
|
||||
@ -827,10 +1088,14 @@ export default function ExchangePage() {
|
||||
error={!!warningHigh}
|
||||
>
|
||||
<Text fontSize={20} fontWeight={500}>
|
||||
{inputError
|
||||
{generalError
|
||||
? generalError
|
||||
: inputError
|
||||
? inputError
|
||||
: outputError
|
||||
? outputError
|
||||
: recipientError
|
||||
? recipientError
|
||||
: tradeError
|
||||
? tradeError
|
||||
: warningHigh
|
||||
@ -839,6 +1104,7 @@ export default function ExchangePage() {
|
||||
</Text>
|
||||
</ButtonError>
|
||||
</AutoColumn>
|
||||
|
||||
{warningHigh && (
|
||||
<FixedBottom>
|
||||
<GreyCard>
|
||||
|
@ -5,11 +5,11 @@ const StyledInput = styled.input`
|
||||
color: ${({ error, theme }) => error && theme.salmonRed};
|
||||
background-color: ${({ theme }) => theme.inputBackground};
|
||||
color: ${({ theme }) => theme.textColor};
|
||||
width: 0;
|
||||
font-size: 20px;
|
||||
outline: none;
|
||||
border: none;
|
||||
flex: 1 1 auto;
|
||||
width: 0;
|
||||
background-color: ${({ theme }) => theme.inputBackground};
|
||||
font-size: ${({ fontSize }) => fontSize && fontSize};
|
||||
text-align: ${({ align }) => align && align};
|
||||
|
@ -1,28 +1,32 @@
|
||||
import React, { useState, useRef, useMemo, useEffect } from 'react'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Link as StyledLink } from '../../theme/components'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ethers } from 'ethers'
|
||||
import '@reach/tooltip/styles.css'
|
||||
import styled from 'styled-components'
|
||||
import escapeStringRegex from 'escape-string-regexp'
|
||||
import '@reach/tooltip/styles.css'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { ethers } from 'ethers'
|
||||
import { isMobile } from 'react-device-detect'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
import { JSBI } from '@uniswap/sdk'
|
||||
|
||||
import { Link as StyledLink } from '../../theme/components'
|
||||
|
||||
import { Text } from 'rebass'
|
||||
import Column, { AutoColumn } from '../Column'
|
||||
import { RowBetween, RowFixed } from '../Row'
|
||||
import TokenLogo from '../TokenLogo'
|
||||
import { CloseIcon } from '../../theme/components'
|
||||
import DoubleTokenLogo from '../DoubleLogo'
|
||||
import { useWeb3React } from '../../hooks'
|
||||
import { isAddress } from '../../utils'
|
||||
import Modal from '../Modal'
|
||||
import { useToken, useAllTokens, INITIAL_TOKENS_CONTEXT } from '../../contexts/Tokens'
|
||||
import { Spinner } from '../../theme'
|
||||
import Circle from '../../assets/images/circle.svg'
|
||||
import TokenLogo from '../TokenLogo'
|
||||
import DoubleTokenLogo from '../DoubleLogo'
|
||||
import Column, { AutoColumn } from '../Column'
|
||||
import { Text } from 'rebass'
|
||||
import { Spinner } from '../../theme'
|
||||
import { CloseIcon } from '../../theme/components'
|
||||
import { ColumnCenter } from '../../components/Column'
|
||||
import { RowBetween, RowFixed } from '../Row'
|
||||
|
||||
import { isAddress } from '../../utils'
|
||||
import { useWeb3React } from '../../hooks'
|
||||
import { useAllBalances } from '../../contexts/Balances'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useAllExchanges } from '../../contexts/Exchanges'
|
||||
import { useToken, useAllTokens, INITIAL_TOKENS_CONTEXT } from '../../contexts/Tokens'
|
||||
|
||||
const TokenModalInfo = styled.div`
|
||||
${({ theme }) => theme.flexRowNoWrap}
|
||||
@ -108,7 +112,17 @@ const MenuItem = styled(PaddedItem)`
|
||||
background-color: ${({ theme }) => theme.tokenRowHover};
|
||||
}
|
||||
`
|
||||
function SearchModal({ history, isOpen, onDismiss, onTokenSelect, urlAddedTokens, filterType, hiddenToken }) {
|
||||
function SearchModal({
|
||||
history,
|
||||
isOpen,
|
||||
onDismiss,
|
||||
onTokenSelect,
|
||||
urlAddedTokens,
|
||||
filterType,
|
||||
hiddenToken,
|
||||
showSendWithSwap,
|
||||
onTokenSelectSendWithSwap
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { account, chainId } = useWeb3React()
|
||||
@ -171,6 +185,7 @@ function SearchModal({ history, isOpen, onDismiss, onTokenSelect, urlAddedTokens
|
||||
let balance
|
||||
// only update if we have data
|
||||
balance = allBalances?.[account]?.[k]
|
||||
|
||||
return {
|
||||
name: allTokens[k].name,
|
||||
symbol: allTokens[k].symbol,
|
||||
@ -203,10 +218,16 @@ function SearchModal({ history, isOpen, onDismiss, onTokenSelect, urlAddedTokens
|
||||
})
|
||||
}, [tokenList, searchQuery])
|
||||
|
||||
function _onTokenSelect(address) {
|
||||
setSearchQuery('')
|
||||
onTokenSelect(address)
|
||||
onDismiss()
|
||||
function _onTokenSelect(address, sendWithSwap = false) {
|
||||
if (sendWithSwap) {
|
||||
setSearchQuery('')
|
||||
onTokenSelectSendWithSwap(address)
|
||||
onDismiss()
|
||||
} else {
|
||||
setSearchQuery('')
|
||||
onTokenSelect(address)
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
|
||||
// manage focus on modal show
|
||||
@ -340,8 +361,11 @@ function SearchModal({ history, isOpen, onDismiss, onTokenSelect, urlAddedTokens
|
||||
INITIAL_TOKENS_CONTEXT[chainId] &&
|
||||
!INITIAL_TOKENS_CONTEXT[chainId].hasOwnProperty(address) &&
|
||||
!urlAdded
|
||||
|
||||
const zeroBalance = JSBI.equal(JSBI.BigInt(0), balance.raw)
|
||||
|
||||
return (
|
||||
<MenuItem key={address} onClick={() => _onTokenSelect(address)}>
|
||||
<MenuItem key={address} onClick={() => (zeroBalance ? _onTokenSelect(address, true) : _onTokenSelect(address))}>
|
||||
<RowFixed>
|
||||
<TokenLogo address={address} size={'24px'} style={{ marginRight: '14px' }} />
|
||||
<Column>
|
||||
@ -353,7 +377,22 @@ function SearchModal({ history, isOpen, onDismiss, onTokenSelect, urlAddedTokens
|
||||
</RowFixed>
|
||||
<AutoColumn gap="4px" justify="end">
|
||||
{balance ? (
|
||||
<Text>{balance ? balance.toSignificant(6) : '-'}</Text>
|
||||
<Text>
|
||||
{zeroBalance && showSendWithSwap ? (
|
||||
<ColumnCenter
|
||||
justify="center"
|
||||
style={{ backgroundColor: '#EBF4FF', padding: '8px', borderRadius: '12px' }}
|
||||
>
|
||||
<Text textAlign="center" fontWeight={500} color="#2172E5">
|
||||
Send With Swap
|
||||
</Text>
|
||||
</ColumnCenter>
|
||||
) : balance ? (
|
||||
balance.toSignificant(6)
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</Text>
|
||||
) : account ? (
|
||||
<SpinnerWrapper src={Circle} alt="loader" />
|
||||
) : (
|
||||
|
@ -71,7 +71,7 @@ export default function InputSlider({ value, onChange }) {
|
||||
value={typeof value === 'number' ? value : 0}
|
||||
onChange={onChange}
|
||||
aria-labelledby="input-slider"
|
||||
marks={marks}
|
||||
// marks={marks}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -52,7 +52,7 @@ export default function TokenLogo({ address, size = '24px', ...rest }) {
|
||||
if (address === 'ETH') {
|
||||
return <StyledEthereumLogo size={size} {...rest} />
|
||||
} else if (!error && !BAD_IMAGES[address]) {
|
||||
path = TOKEN_ICON_API(address.toLowerCase())
|
||||
path = TOKEN_ICON_API(address?.toLowerCase())
|
||||
} else {
|
||||
return (
|
||||
<Emoji {...rest} size={size}>
|
||||
|
@ -1,6 +0,0 @@
|
||||
import React from 'react'
|
||||
import ExchangePage from '../../components/ExchangePage'
|
||||
|
||||
export default function Send({ initialCurrency, params }) {
|
||||
return <ExchangePage initialCurrency={initialCurrency} params={params} sending={true} />
|
||||
}
|
251
src/pages/Send/index.tsx
Normal file
251
src/pages/Send/index.tsx
Normal file
@ -0,0 +1,251 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { darken } from 'polished'
|
||||
import { TokenAmount, JSBI } from '@uniswap/sdk'
|
||||
|
||||
import QR from '../../assets/svg/QR.svg'
|
||||
import TokenLogo from '../../components/TokenLogo'
|
||||
|
||||
import SearchModal from '../../components/SearchModal'
|
||||
import ExchangePage from '../../components/ExchangePage'
|
||||
import NumericalInput from '../../components/NumericalInput'
|
||||
import ConfirmationModal from '../../components/ConfirmationModal'
|
||||
import { Text } from 'rebass'
|
||||
import { TYPE } from '../../theme'
|
||||
import { LightCard } from '../../components/Card'
|
||||
import { ArrowDown } from 'react-feather'
|
||||
import { AutoColumn } from '../../components/Column'
|
||||
import { ButtonPrimary } from '../../components/Button'
|
||||
import { ReactComponent as DropDown } from '../../assets/images/dropdown.svg'
|
||||
|
||||
import { useToken } from '../../contexts/Tokens'
|
||||
import { RowBetween } from '../../components/Row'
|
||||
import { useENSName } from '../../hooks'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { useAddressBalance } from '../../contexts/Balances'
|
||||
|
||||
import { parseUnits } from '@ethersproject/units'
|
||||
import { isAddress } from '../../utils'
|
||||
|
||||
const CurrencySelect = styled.button`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 20px;
|
||||
width: ${({ selected }) => (selected ? '128px' : '180px')}
|
||||
padding: 8px 12px;
|
||||
background-color: ${({ selected, theme }) => (selected ? theme.buttonBackgroundPlain : theme.royalBlue)};
|
||||
color: ${({ selected, theme }) => (selected ? theme.textColor : theme.white)};
|
||||
border: 1px solid
|
||||
${({ selected, theme }) => (selected ? theme.outlineGrey : theme.royalBlue)};
|
||||
border-radius: 8px;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
:hover {
|
||||
border: 1px solid
|
||||
${({ selected, theme }) => (selected ? darken(0.1, theme.outlineGrey) : darken(0.1, theme.royalBlue))};
|
||||
}
|
||||
|
||||
:focus {
|
||||
border: 1px solid ${({ selected, theme }) =>
|
||||
selected ? darken(0.1, theme.outlineGrey) : darken(0.1, theme.royalBlue)};
|
||||
}
|
||||
|
||||
:active {
|
||||
background-color: ${({ selected, theme }) => (selected ? theme.buttonBackgroundPlain : theme.royalBlue)};
|
||||
}
|
||||
`
|
||||
|
||||
const StyledDropDown = styled(DropDown)`
|
||||
height: 35%;
|
||||
|
||||
path {
|
||||
stroke: ${({ selected, theme }) => (selected ? theme.textColor : theme.white)};
|
||||
}
|
||||
`
|
||||
|
||||
const InputGroup = styled(AutoColumn)`
|
||||
position: relative;
|
||||
padding: 40px 0;
|
||||
`
|
||||
|
||||
const QRWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid ${({ theme }) => theme.outlineGrey};
|
||||
background: #fbfbfb;
|
||||
padding: 4px;
|
||||
border-radius: 8px;
|
||||
`
|
||||
|
||||
const StyledInput = styled.input`
|
||||
width: ${({ width }) => width};
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: 20px;
|
||||
|
||||
::placeholder {
|
||||
color: #edeef2;
|
||||
}
|
||||
`
|
||||
|
||||
const StyledNumerical = styled(NumericalInput)`
|
||||
text-align: center;
|
||||
font-size: 48px;
|
||||
font-weight: 500px;
|
||||
width: 100%;
|
||||
|
||||
::placeholder {
|
||||
color: #edeef2;
|
||||
}
|
||||
`
|
||||
|
||||
const MaxButton = styled.button`
|
||||
position: absolute;
|
||||
right: 70px;
|
||||
padding: 0.5rem 1rem;
|
||||
background-color: ${({ theme }) => theme.zumthorBlue};
|
||||
border: 1px solid ${({ theme }) => theme.zumthorBlue};
|
||||
border-radius: 0.5rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
margin-right: 0.5rem;
|
||||
color: ${({ theme }) => theme.royalBlue};
|
||||
:hover {
|
||||
border: 1px solid ${({ theme }) => theme.royalBlue};
|
||||
}
|
||||
:focus {
|
||||
border: 1px solid ${({ theme }) => theme.royalBlue};
|
||||
outline: none;
|
||||
}
|
||||
`
|
||||
|
||||
export default function Send() {
|
||||
const { account } = useWeb3React()
|
||||
|
||||
// setting for send with swap or regular swap
|
||||
const [withSwap, setWithSwap] = useState(true)
|
||||
|
||||
// modals
|
||||
const [modalOpen, setModalOpen] = useState(false)
|
||||
const [showConfirm, setShowConfirm] = useState(false)
|
||||
|
||||
// token selected
|
||||
const [activeTokenAddress, setActiveTokenAddress] = useState()
|
||||
const token = useToken(activeTokenAddress)
|
||||
|
||||
// user inputs
|
||||
const [typedValue, setTypedValue] = useState('')
|
||||
const [amount, setAmount] = useState(null)
|
||||
const [recipient, setRecipient] = useState('0x74Aa01d162E6dC6A657caC857418C403D48E2D77')
|
||||
|
||||
//ENS
|
||||
const recipientENS = useENSName(recipient)
|
||||
|
||||
// balances
|
||||
const userBalance = useAddressBalance(account, token)
|
||||
|
||||
//errors
|
||||
const [generalError, setGeneralError] = useState('')
|
||||
const [amountError, setAmountError] = useState('')
|
||||
const [recipientError, setRecipientError] = useState('')
|
||||
|
||||
function parseInputAmount(newtypedValue) {
|
||||
setTypedValue(newtypedValue)
|
||||
if (!!token && newtypedValue !== '' && newtypedValue !== '.') {
|
||||
const typedValueParsed = parseUnits(newtypedValue, token.decimals).toString()
|
||||
setAmount(new TokenAmount(token, typedValueParsed))
|
||||
}
|
||||
}
|
||||
|
||||
function onMax() {
|
||||
if (userBalance) {
|
||||
setTypedValue(userBalance.toExact())
|
||||
setAmount(userBalance)
|
||||
}
|
||||
}
|
||||
|
||||
const atMax = amount && userBalance && JSBI.equal(amount.raw, userBalance.raw) ? true : false
|
||||
|
||||
//error detection
|
||||
useEffect(() => {
|
||||
setGeneralError('')
|
||||
setRecipientError('')
|
||||
setAmountError('')
|
||||
|
||||
if (!amount) {
|
||||
setGeneralError('Enter an amount')
|
||||
}
|
||||
if (!isAddress(recipient)) {
|
||||
setRecipientError('Enter a valid address')
|
||||
}
|
||||
if (!!!token) {
|
||||
setGeneralError('Select a token')
|
||||
}
|
||||
if (amount && userBalance && JSBI.greaterThan(amount.raw, userBalance.raw)) {
|
||||
setAmountError('Insufficient Balance')
|
||||
}
|
||||
}, [recipient, token, amount, userBalance])
|
||||
|
||||
const TopContent = () => {
|
||||
return (
|
||||
<AutoColumn gap="30px" style={{ marginTop: '40px' }}>
|
||||
<RowBetween>
|
||||
<Text fontSize={36} fontWeight={500}>
|
||||
{amount?.toFixed(8)}
|
||||
</Text>
|
||||
<TokenLogo address={activeTokenAddress} size={'30px'} />
|
||||
</RowBetween>
|
||||
<ArrowDown size={24} color="#888D9B" />
|
||||
<TYPE.blue fontSize={36}>
|
||||
{recipient?.slice(0, 6)}...{recipient?.slice(36, 42)}
|
||||
</TYPE.blue>
|
||||
</AutoColumn>
|
||||
)
|
||||
}
|
||||
|
||||
const BottomContent = () => {
|
||||
return (
|
||||
<AutoColumn>
|
||||
<ButtonPrimary>
|
||||
<Text color="white" fontSize={20}>
|
||||
Confirm send
|
||||
</Text>
|
||||
</ButtonPrimary>
|
||||
</AutoColumn>
|
||||
)
|
||||
}
|
||||
|
||||
const [attemptedSend, setAttemptedSend] = useState(false) // clicke confirm
|
||||
const [pendingConfirmation, setPendingConfirmation] = useState(true) // waiting for
|
||||
|
||||
return withSwap ? (
|
||||
<ExchangePage sendingInput={true} />
|
||||
) : (
|
||||
<>
|
||||
<SearchModal
|
||||
isOpen={modalOpen}
|
||||
onDismiss={() => {
|
||||
setModalOpen(false)
|
||||
}}
|
||||
filterType="tokens"
|
||||
onTokenSelect={tokenAddress => setActiveTokenAddress(tokenAddress)}
|
||||
/>
|
||||
<ConfirmationModal
|
||||
isOpen={showConfirm}
|
||||
onDismiss={() => setShowConfirm(false)}
|
||||
hash=""
|
||||
title="Confirm Send"
|
||||
topContent={TopContent}
|
||||
bottomContent={BottomContent}
|
||||
attemptingTxn={attemptedSend}
|
||||
pendingConfirmation={pendingConfirmation}
|
||||
pendingText=""
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
@ -41,7 +41,7 @@ const Wrapper = styled.div`
|
||||
|
||||
const FixedBottom = styled.div`
|
||||
position: absolute;
|
||||
bottom: -240px;
|
||||
bottom: -200px;
|
||||
width: 100%;
|
||||
`
|
||||
|
||||
|
@ -2,5 +2,5 @@ import React from 'react'
|
||||
import ExchangePage from '../../components/ExchangePage'
|
||||
|
||||
export default function Swap({ initialCurrency, params }) {
|
||||
return <ExchangePage initialCurrency={initialCurrency} params={params} />
|
||||
return <ExchangePage sendingInput={false} />
|
||||
}
|
||||
|
@ -128,6 +128,21 @@ export const TYPE = {
|
||||
<Text fontWeight={500} color={theme().royalBlue} {...rest}>
|
||||
{children}
|
||||
</Text>
|
||||
),
|
||||
gray: ({ children, ...rest }) => (
|
||||
<Text fontWeight={500} color={theme().outlineGrey} {...rest}>
|
||||
{children}
|
||||
</Text>
|
||||
),
|
||||
italic: ({ children, ...rest }) => (
|
||||
<Text fontWeight={500} fontSize={12} fontStyle={'italic'} color={theme().mineshaftGray} {...rest}>
|
||||
{children}
|
||||
</Text>
|
||||
),
|
||||
error: ({ children, error, ...rest }) => (
|
||||
<Text fontWeight={500} color={error ? theme().salmonRed : theme().mineshaftGray} {...rest}>
|
||||
{children}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1,25 +0,0 @@
|
||||
import { getMarketDetails } from '@uniswap/sdk'
|
||||
import { getMedian, getMean } from './math'
|
||||
|
||||
const DAI = 'DAI'
|
||||
const USDC = 'USDC'
|
||||
const TUSD = 'TUSD'
|
||||
|
||||
const USD_STABLECOINS = [DAI, USDC, TUSD]
|
||||
|
||||
function forEachStablecoin(runner) {
|
||||
return USD_STABLECOINS.map((stablecoin, index) => runner(index, stablecoin))
|
||||
}
|
||||
|
||||
export function getUSDPrice(reserves) {
|
||||
const marketDetails = forEachStablecoin(i => getMarketDetails(reserves[i], undefined))
|
||||
const ethPrices = forEachStablecoin(i => marketDetails[i].marketRate.rateInverted)
|
||||
|
||||
const [median] = getMedian(ethPrices)
|
||||
const [mean] = getMean(ethPrices)
|
||||
const [weightedMean] = getMean(
|
||||
ethPrices,
|
||||
forEachStablecoin(i => reserves[i].ethReserve.amount)
|
||||
)
|
||||
return getMean([median, mean, weightedMean])[0]
|
||||
}
|
Loading…
Reference in New Issue
Block a user