basic send added

This commit is contained in:
ianlapham 2020-03-17 16:23:27 -04:00
parent 4e2c5c1e84
commit 655b79569b
14 changed files with 899 additions and 326 deletions

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)` const ButtonConfirmedStyle = styled(Base)`
background-color: ${({ theme }) => lighten(0.5, theme.connectedGreen)}; background-color: ${({ theme }) => lighten(0.5, theme.connectedGreen)};
border: 1px solid ${({ theme }) => theme.connectedGreen}; border: 1px solid ${({ theme }) => theme.connectedGreen};
@ -160,7 +181,7 @@ export function ButtonDropwdownLight({ disabled, children, ...rest }) {
export function ButtonRadio({ active, children, ...rest }) { export function ButtonRadio({ active, children, ...rest }) {
if (!active) { if (!active) {
return <ButtonEmpty {...rest}>{children}</ButtonEmpty> return <ButtonWhite {...rest}>{children}</ButtonWhite>
} else { } else {
return <ButtonPrimary {...rest}>{children}</ButtonPrimary> return <ButtonPrimary {...rest}>{children}</ButtonPrimary>
} }

@ -48,10 +48,9 @@ const InputRow = styled.div`
const CurrencySelect = styled.button` const CurrencySelect = styled.button`
align-items: center; align-items: center;
height: 2.2rem; height: 2.2rem;
font-size: 20px; font-size: 20px;
background-color: ${({ selected, theme }) => (selected ? theme.buttonBackgroundPlain : theme.zumthorBlue)}; background-color: ${({ selected, theme }) => (selected ? theme.buttonBackgroundPlain : theme.royalBlue)};
color: ${({ selected, theme }) => (selected ? theme.textColor : theme.royalBlue)}; color: ${({ selected, theme }) => (selected ? theme.textColor : theme.white)};
border: 1px solid border: 1px solid
${({ selected, theme, disableTokenSelect }) => ${({ selected, theme, disableTokenSelect }) =>
disableTokenSelect ? theme.buttonBackgroundPlain : selected ? theme.buttonOutlinePlain : theme.royalBlue}; disableTokenSelect ? theme.buttonBackgroundPlain : selected ? theme.buttonOutlinePlain : theme.royalBlue};
@ -70,7 +69,8 @@ const CurrencySelect = styled.button`
} }
:active { :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%; height: 35%;
path { path {
stroke: ${({ selected, theme }) => (selected ? theme.textColor : theme.royalBlue)}; stroke: ${({ selected, theme }) => (selected ? theme.textColor : theme.white)};
} }
` `
const InputPanel = styled.div` const InputPanel = styled.div`
${({ theme }) => theme.flexColumnNoWrap} ${({ theme }) => theme.flexColumnNoWrap}
position: relative; position: relative;
border-radius: 1.25rem; border-radius: ${({ hideInput }) => (hideInput ? '8px' : '20px')};
background-color: ${({ theme }) => theme.inputBackground}; background-color: ${({ theme }) => theme.inputBackground};
z-index: 1; z-index: 1;
` `
const Container = styled.div` const Container = styled.div`
border-radius: 1.25rem; border-radius: ${({ hideInput }) => (hideInput ? '8px' : '20px')};
border: 1px solid ${({ error, theme }) => (error ? theme.salmonRed : theme.mercuryGray)}; border: 1px solid ${({ error, theme }) => (error ? theme.salmonRed : theme.mercuryGray)};
background-color: ${({ theme }) => theme.inputBackground}; background-color: ${({ theme }) => theme.inputBackground};
@ -174,7 +174,10 @@ export default function CurrencyInputPanel({
hideBalance = false, hideBalance = false,
isExchange = false, isExchange = false,
exchange = null, // used for double token logo 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 { account, chainId } = useWeb3React()
const { t } = useTranslation() const { t } = useTranslation()
@ -236,7 +239,7 @@ export default function CurrencyInputPanel({
return ( return (
<InputPanel> <InputPanel>
<Container error={!!error}> <Container error={!!error} hideInput={hideInput}>
{!hideBalance && ( {!hideBalance && (
<LabelRow> <LabelRow>
<RowBetween> <RowBetween>
@ -250,15 +253,19 @@ export default function CurrencyInputPanel({
</RowBetween> </RowBetween>
</LabelRow> </LabelRow>
)} )}
<InputRow> <InputRow style={hideInput ? { padding: '0', borderRadius: '8px' } : {}} hideInput={hideInput}>
<NumericalInput {!hideInput && (
value={value} <>
onUserInput={val => { <NumericalInput
onUserInput(field, val) value={value}
}} onUserInput={val => {
/> onUserInput(field, val)
{!!token?.address && !atMax && <StyledBalanceMax onClick={onMax}>MAX</StyledBalanceMax>} }}
{renderUnlockButton()} />
{!!token?.address && !atMax && <StyledBalanceMax onClick={onMax}>MAX</StyledBalanceMax>}
{renderUnlockButton()}
</>
)}
<CurrencySelect <CurrencySelect
selected={!!token?.address} selected={!!token?.address}
onClick={() => { onClick={() => {
@ -296,6 +303,8 @@ export default function CurrencyInputPanel({
urlAddedTokens={urlAddedTokens} urlAddedTokens={urlAddedTokens}
field={field} field={field}
onTokenSelect={onTokenSelection} onTokenSelect={onTokenSelection}
showSendWithSwap={showSendWithSwap}
onTokenSelectSendWithSwap={onTokenSelectSendWithSwap}
/> />
)} )}
</InputPanel> </InputPanel>

@ -4,6 +4,7 @@ import { ethers } from 'ethers'
import { parseUnits, parseEther } from '@ethersproject/units' import { parseUnits, parseEther } from '@ethersproject/units'
import { WETH, TradeType, Route, Trade, TokenAmount, JSBI } from '@uniswap/sdk' import { WETH, TradeType, Route, Trade, TokenAmount, JSBI } from '@uniswap/sdk'
import QR from '../../assets/svg/QR.svg'
import TokenLogo from '../TokenLogo' import TokenLogo from '../TokenLogo'
import QuestionHelper from '../Question' import QuestionHelper from '../Question'
import NumericalInput from '../NumericalInput' import NumericalInput from '../NumericalInput'
@ -11,23 +12,23 @@ import ConfirmationModal from '../ConfirmationModal'
import CurrencyInputPanel from '../CurrencyInputPanel' import CurrencyInputPanel from '../CurrencyInputPanel'
import { Link } from '../../theme/components' import { Link } from '../../theme/components'
import { Text } from 'rebass' import { Text } from 'rebass'
import ThemeProvider, { TYPE } from '../../theme' import { TYPE } from '../../theme'
import { GreyCard } from '../../components/Card' import { GreyCard, LightCard } from '../../components/Card'
import { ArrowDown, ArrowUp } from 'react-feather' import { ArrowDown, ArrowUp } from 'react-feather'
import { ButtonPrimary, ButtonError, ButtonRadio } from '../Button'
import { AutoColumn, ColumnCenter } from '../../components/Column' import { AutoColumn, ColumnCenter } from '../../components/Column'
import { ButtonError, ButtonRadio } from '../Button'
import Row, { RowBetween, RowFixed } from '../../components/Row' import Row, { RowBetween, RowFixed } from '../../components/Row'
import { usePopups } from '../../contexts/Application' import { usePopups } from '../../contexts/Application'
import { useToken } from '../../contexts/Tokens' import { useToken } from '../../contexts/Tokens'
import { useExchange } from '../../contexts/Exchanges' import { useExchange } from '../../contexts/Exchanges'
import { useWeb3React } from '../../hooks' import { useWeb3React, useTokenContract } from '../../hooks'
import { useAddressBalance } from '../../contexts/Balances' import { useAddressBalance } from '../../contexts/Balances'
import { useTransactionAdder } from '../../contexts/Transactions' import { useTransactionAdder } from '../../contexts/Transactions'
import { useAddressAllowance } from '../../contexts/Allowances' import { useAddressAllowance } from '../../contexts/Allowances'
import { ROUTER_ADDRESSES } from '../../constants' import { ROUTER_ADDRESSES } from '../../constants'
import { getRouterContract, calculateGasMargin } from '../../utils' import { getRouterContract, calculateGasMargin, isAddress, getProviderOrSigner } from '../../utils'
const Wrapper = styled.div` const Wrapper = styled.div`
position: relative; position: relative;
@ -64,7 +65,66 @@ const InputWrapper = styled(RowBetween)`
border-radius: 8px; border-radius: 8px;
padding: 4px 8px; padding: 4px 8px;
border: 1px solid transparent; 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 { enum Field {
@ -91,7 +151,7 @@ function initializeSwapState(inputAddress?: string, outputAddress?: string): Swa
address: inputAddress address: inputAddress
}, },
[Field.OUTPUT]: { [Field.OUTPUT]: {
address: '0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735' address: outputAddress
} }
} }
} }
@ -192,22 +252,28 @@ const INITIAL_ALLOWED_SLIPPAGE = 200
const DEFAULT_DEADLINE_FROM_NOW = 60 * 15 const DEFAULT_DEADLINE_FROM_NOW = 60 * 15
// used for warning states based on slippage in bips // used for warning states based on slippage in bips
const ALLOWED_IMPACT_MEDIUM = 100 const ALLOWED_SLIPPAGE_MEDIUM = 100
const ALLOWED_IMPACT_HIGH = 500 const ALLOWED_SLIPPAGE_HIGH = 500
export default function ExchangePage() { export default function ExchangePage({ sendingInput = false }) {
const { chainId, account, library } = useWeb3React() const { chainId, account, library } = useWeb3React()
const routerAddress = ROUTER_ADDRESSES[chainId] const routerAddress = ROUTER_ADDRESSES[chainId]
// adding notifications on txns // adding notifications on txns
const [, addPopup] = usePopups() const [, addPopup] = usePopups()
const addTransaction = useTransactionAdder()
// sending state
const [sending, setSending] = useState(sendingInput)
const [sendingWithSwap, setSendingWithSwap] = useState(false)
const [recipient, setRecipient] = useState('')
// input details // input details
const [state, dispatch] = useReducer(reducer, WETH[chainId].address, initializeSwapState) const [state, dispatch] = useReducer(reducer, WETH[chainId].address, initializeSwapState)
const { independentField, typedValue, ...fieldData } = state const { independentField, typedValue, ...fieldData } = state
const dependentField = independentField === Field.INPUT ? Field.OUTPUT : Field.INPUT const dependentField = independentField === Field.INPUT ? Field.OUTPUT : Field.INPUT
const tradeType = independentField === Field.INPUT ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT 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 = { const tokens = {
[Field.INPUT]: useToken(fieldData[Field.INPUT].address), [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 exchange = useExchange(tokens[Field.INPUT], tokens[Field.OUTPUT])
const route = !!exchange ? new Route([exchange], tokens[Field.INPUT]) : undefined const route = !!exchange ? new Route([exchange], tokens[Field.INPUT]) : undefined
// modal state // modal and loading
const addTransaction = useTransactionAdder() const [showConfirm, setShowConfirm] = useState(false)
const [showConfirm, setShowConfirm] = useState(true)
const [pendingConfirmation, setPendingConfirmation] = useState(true) // waiting for user confirmation const [pendingConfirmation, setPendingConfirmation] = useState(true) // waiting for user confirmation
const [attemptingTxn, setAttemptingTxn] = useState(false) // clicked confirmed 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 // txn values
const [txHash, setTxHash] = useState() const [txHash, setTxHash] = useState()
const [deadline, setDeadline] = useState(DEFAULT_DEADLINE_FROM_NOW) const [deadline, setDeadline] = useState(DEFAULT_DEADLINE_FROM_NOW)
@ -240,10 +312,9 @@ export default function ExchangePage() {
const parsedAmounts: { [field: number]: TokenAmount } = {} const parsedAmounts: { [field: number]: TokenAmount } = {}
// try to parse typed value // try to parse typed value
// if (typedValue !== '' && typedValue !== '.' && tokens[independentField]) { if (typedValue !== '' && typedValue !== '.' && tokens[independentField]) {
if (tokens[independentField]) {
try { try {
const typedValueParsed = parseUnits('0.0001', tokens[independentField].decimals).toString() const typedValueParsed = parseUnits(typedValue, tokens[independentField].decimals).toString()
if (typedValueParsed !== '0') if (typedValueParsed !== '0')
parsedAmounts[independentField] = new TokenAmount(tokens[independentField], typedValueParsed) parsedAmounts[independentField] = new TokenAmount(tokens[independentField], typedValueParsed)
} catch (error) { } 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 = const maxAmountInput =
!!userBalances[Field.INPUT] && !!userBalances[Field.INPUT] &&
JSBI.greaterThan( JSBI.greaterThan(
@ -388,16 +459,14 @@ export default function ExchangePage() {
outputApproval && outputApproval &&
JSBI.greaterThan(parsedAmounts[Field.OUTPUT].raw, outputApproval.raw) JSBI.greaterThan(parsedAmounts[Field.OUTPUT].raw, outputApproval.raw)
// modal state // parse the input for custom slippage
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)
function parseCustomInput(val) { function parseCustomInput(val) {
const acceptableValues = [/^$/, /^\d{1,2}$/, /^\d{0,2}\.\d{0,2}$/] 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))) { if (acceptableValues.some(a => a.test(val))) {
setCustomSlippage(val) setCustomSlippage(val)
setAllowedSlippage(val * 100) 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() { async function onSwap() {
const routerContract = getRouterContract(chainId, library, account) const routerContract = getRouterContract(chainId, library, account)
setAttemptingTxn(true) setAttemptingTxn(true)
@ -497,7 +622,7 @@ export default function ExchangePage() {
gasLimit: calculateGasMargin(estimatedGasLimit, GAS_MARGIN) gasLimit: calculateGasMargin(estimatedGasLimit, GAS_MARGIN)
}) })
.then(response => { .then(response => {
setTxHash(response) setTxHash(response.hash)
addTransaction(response) addTransaction(response)
setPendingConfirmation(false) setPendingConfirmation(false)
}) })
@ -513,17 +638,38 @@ export default function ExchangePage() {
} }
// errors // errors
const [generalError, setGeneralError] = useState('')
const [inputError, setInputError] = useState('') const [inputError, setInputError] = useState('')
const [outputError, setOutputError] = useState('') const [outputError, setOutputError] = useState('')
const [recipientError, setRecipientError] = useState('')
const [isValid, setIsValid] = useState(false) const [isValid, setIsValid] = useState(false)
const ignoreOutput = sending ? !sendingWithSwap : false
useEffect(() => { useEffect(() => {
// reset errors // reset errors
setGeneralError(null)
setInputError(null) setInputError(null)
setOutputError(null) setOutputError(null)
setTradeError(null) setTradeError(null)
setRecipientError(null)
setIsValid(true) 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 ( if (
parsedAmounts[Field.INPUT] && parsedAmounts[Field.INPUT] &&
exchange && exchange &&
@ -534,6 +680,7 @@ export default function ExchangePage() {
} }
if ( if (
!ignoreOutput &&
parsedAmounts[Field.OUTPUT] && parsedAmounts[Field.OUTPUT] &&
exchange && exchange &&
JSBI.greaterThan(parsedAmounts[Field.OUTPUT].raw, exchange.reserveOf(tokens[Field.OUTPUT]).raw) JSBI.greaterThan(parsedAmounts[Field.OUTPUT].raw, exchange.reserveOf(tokens[Field.OUTPUT]).raw)
@ -542,12 +689,12 @@ export default function ExchangePage() {
setIsValid(false) setIsValid(false)
} }
if (showInputUnlock) { if (showInputUnlock && !(sending && !sendingWithSwap)) {
setInputError('Approval Needed') setInputError('Approval Needed')
setIsValid(false) setIsValid(false)
} }
if (showOutputUnlock) { if (showOutputUnlock && !ignoreOutput) {
setOutputError('Approval Needed') setOutputError('Approval Needed')
setIsValid(false) setIsValid(false)
} }
@ -560,19 +707,22 @@ export default function ExchangePage() {
setInputError('Insufficient balance.') setInputError('Insufficient balance.')
setIsValid(false) setIsValid(false)
} }
}, [
exchange,
ignoreOutput,
parsedAmounts,
recipient,
sending,
sendingWithSwap,
showInputUnlock,
showOutputUnlock,
tokens,
userBalances
])
if ( // warnings on slippage
userBalances[Field.OUTPUT] && const warningMedium = slippageFromTrade && parseFloat(slippageFromTrade.toFixed(4)) > ALLOWED_SLIPPAGE_MEDIUM / 100
parsedAmounts[Field.OUTPUT] && const warningHigh = slippageFromTrade && parseFloat(slippageFromTrade.toFixed(4)) > ALLOWED_SLIPPAGE_HIGH / 100
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
function resetModal() { function resetModal() {
setPendingConfirmation(true) setPendingConfirmation(true)
@ -581,169 +731,221 @@ export default function ExchangePage() {
} }
function modalHeader() { function modalHeader() {
return ( if (sending && !sendingWithSwap) {
<AutoColumn gap={'20px'} style={{ marginTop: '40px' }}> return (
<RowBetween align="flex-end"> <AutoColumn gap="30px" style={{ marginTop: '40px' }}>
<Text fontSize={36} fontWeight={500}> <RowBetween>
{!!slippageAdjustedAmounts[Field.INPUT] && slippageAdjustedAmounts[Field.INPUT].toSignificant(6)} <Text fontSize={36} fontWeight={500}>
</Text> {parsedAmounts[Field.INPUT]?.toFixed(8)}
<RowFixed gap="10px">
<TokenLogo address={tokens[Field.INPUT]?.address} size={'24px'} />
<Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}>
{tokens[Field.INPUT]?.symbol || ''}
</Text> </Text>
</RowFixed> <TokenLogo address={tokens[Field.INPUT]?.address} size={'30px'} />
</RowBetween> </RowBetween>
<RowFixed> <ArrowDown size={24} color="#888D9B" />
<ArrowDown size="16" color="#888D9B" /> <TYPE.blue fontSize={36}>
</RowFixed> {recipient?.slice(0, 6)}...{recipient?.slice(36, 42)}
<RowBetween align="flex-end"> </TYPE.blue>
<Text fontSize={36} fontWeight={500} color={warningHigh ? '#FF6871' : '#2172E5'}> </AutoColumn>
{!!slippageAdjustedAmounts[Field.OUTPUT] && slippageAdjustedAmounts[Field.OUTPUT].toSignificant(6)} )
</Text> }
<RowFixed gap="10px">
<TokenLogo address={tokens[Field.OUTPUT]?.address} size={'24px'} /> if (sending && sendingWithSwap) {
<Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}> }
{tokens[Field.OUTPUT]?.symbol || ''}
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> </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> </RowFixed>
</RowBetween> <RowBetween align="flex-end">
</AutoColumn> <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() { function modalBottom() {
return showAdvanced ? ( if (sending && !sendingWithSwap) {
<AutoColumn gap="20px"> return (
<Link <AutoColumn>
onClick={() => { <ButtonPrimary onClick={onSend}>
setShowAdvanced(false) <Text color="white" fontSize={20}>
}} Confirm send
> </Text>
back </ButtonPrimary>
</Link> </AutoColumn>
<RowBetween> )
<TYPE.main>Limit additional price slippage</TYPE.main> }
<QuestionHelper text="" />
</RowBetween> if (sending && sendingWithSwap) {
<Row> }
<ButtonRadio
active={SLIPPAGE_INDEX[1] === activeIndex} if (showAdvanced) {
padding="4px 6px" return (
borderRadius="8px" <AutoColumn gap="20px">
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">
<Link <Link
onClick={() => { onClick={() => {
setShowAdvanced(true) setShowAdvanced(false)
}} }}
> >
Advanced Options back
</Link> </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> </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)} ${ const pendingText = sending
tokens[Field.INPUT]?.symbol ? `Sending ${parsedAmounts[Field.INPUT]?.toSignificant(6)} ${tokens[Field.INPUT]?.symbol} to ${recipient}`
} for ${parsedAmounts[Field.OUTPUT]?.toSignificant(6)} ${tokens[Field.OUTPUT]?.symbol}` : ` Swapped ${parsedAmounts[Field.INPUT]?.toSignificant(6)} ${tokens[Field.INPUT]?.symbol} for ${parsedAmounts[
Field.OUTPUT
]?.toSignificant(6)} ${tokens[Field.OUTPUT]?.symbol}`
return ( return (
<Wrapper> <Wrapper>
@ -756,69 +958,128 @@ export default function ExchangePage() {
attemptingTxn={attemptingTxn} attemptingTxn={attemptingTxn}
pendingConfirmation={pendingConfirmation} pendingConfirmation={pendingConfirmation}
hash={txHash ? txHash : ''} hash={txHash ? txHash : ''}
topContent={() => modalHeader()} topContent={modalHeader}
bottomContent={modalBottom} bottomContent={modalBottom}
pendingText={pendingText} 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'}> <AutoColumn gap={'20px'}>
<CurrencyInputPanel {(!sending || sendingWithSwap) && (
field={Field.INPUT} <>
value={formattedAmounts[Field.INPUT]} <CurrencyInputPanel
onUserInput={onUserInput} field={Field.INPUT}
onMax={() => { value={formattedAmounts[Field.INPUT]}
maxAmountInput && onMaxInput(maxAmountInput.toExact()) onUserInput={onUserInput}
}} onMax={() => {
atMax={atMaxAmountInput} maxAmountInput && onMaxInput(maxAmountInput.toExact())
token={tokens[Field.INPUT]} }}
onTokenSelection={address => onTokenSelection(Field.INPUT, address)} atMax={atMaxAmountInput}
title={'Input'} token={tokens[Field.INPUT]}
error={inputError} onTokenSelection={address => onTokenSelection(Field.INPUT, address)}
exchange={exchange} title={'Input'}
showUnlock={showInputUnlock} error={inputError}
/> exchange={exchange}
<ColumnCenter> showUnlock={showInputUnlock}
<ArrowWrapper onClick={onSwapTokens}> />
<ArrowDown size="16" color="#2F80ED" /> <ColumnCenter>
<ArrowUp size="16" color="#2F80ED" /> <ArrowWrapper onClick={onSwapTokens}>
</ArrowWrapper> <ArrowDown size="16" color="#2F80ED" />
</ColumnCenter> <ArrowUp size="16" color="#2F80ED" />
<CurrencyInputPanel </ArrowWrapper>
field={Field.OUTPUT} </ColumnCenter>
value={formattedAmounts[Field.OUTPUT]} <CurrencyInputPanel
onUserInput={onUserInput} field={Field.OUTPUT}
onMax={() => { value={formattedAmounts[Field.OUTPUT]}
maxAmountOutput && onMaxOutput(maxAmountOutput.toExact()) onUserInput={onUserInput}
}} onMax={() => {
atMax={atMaxAmountOutput} maxAmountOutput && onMaxOutput(maxAmountOutput.toExact())
token={tokens[Field.OUTPUT]} }}
onTokenSelection={address => onTokenSelection(Field.OUTPUT, address)} atMax={atMaxAmountOutput}
title={'Output'} token={tokens[Field.OUTPUT]}
error={outputError} onTokenSelection={address => onTokenSelection(Field.OUTPUT, address)}
exchange={exchange} title={'Output'}
showUnlock={showOutputUnlock} error={outputError}
/> exchange={exchange}
<RowBetween> showUnlock={showOutputUnlock}
<Text fontWeight={500} color="#565A69"> />
Price <RowBetween>
</Text> <Text fontWeight={500} color="#565A69">
<Text fontWeight={500} color="#565A69"> Price
{exchange </Text>
? `1 ${tokens[Field.INPUT].symbol} = ${route?.midPrice.toSignificant(6)} ${tokens[Field.OUTPUT].symbol}` <Text fontWeight={500} color="#565A69">
: '-'} {exchange
</Text> ? `1 ${tokens[Field.INPUT].symbol} = ${route?.midPrice.toSignificant(6)} ${
</RowBetween> tokens[Field.OUTPUT].symbol
{warningMedium && ( }`
<RowBetween> : '-'}
<Text fontWeight={500} color="#565A69"> </Text>
Slippage </RowBetween>
</Text> {warningMedium && (
<ErrorText fontWeight={500} warningMedium={warningMedium} warningHigh={warningHigh}> <RowBetween>
{slippageFromTrade.toFixed(4)}% <Text fontWeight={500} color="#565A69">
</ErrorText> Slippage
</RowBetween> </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 <ButtonError
onClick={() => { onClick={() => {
setShowConfirm(true) setShowConfirm(true)
@ -827,10 +1088,14 @@ export default function ExchangePage() {
error={!!warningHigh} error={!!warningHigh}
> >
<Text fontSize={20} fontWeight={500}> <Text fontSize={20} fontWeight={500}>
{inputError {generalError
? generalError
: inputError
? inputError ? inputError
: outputError : outputError
? outputError ? outputError
: recipientError
? recipientError
: tradeError : tradeError
? tradeError ? tradeError
: warningHigh : warningHigh
@ -839,6 +1104,7 @@ export default function ExchangePage() {
</Text> </Text>
</ButtonError> </ButtonError>
</AutoColumn> </AutoColumn>
{warningHigh && ( {warningHigh && (
<FixedBottom> <FixedBottom>
<GreyCard> <GreyCard>

@ -5,11 +5,11 @@ const StyledInput = styled.input`
color: ${({ error, theme }) => error && theme.salmonRed}; color: ${({ error, theme }) => error && theme.salmonRed};
background-color: ${({ theme }) => theme.inputBackground}; background-color: ${({ theme }) => theme.inputBackground};
color: ${({ theme }) => theme.textColor}; color: ${({ theme }) => theme.textColor};
width: 0;
font-size: 20px; font-size: 20px;
outline: none; outline: none;
border: none; border: none;
flex: 1 1 auto; flex: 1 1 auto;
width: 0;
background-color: ${({ theme }) => theme.inputBackground}; background-color: ${({ theme }) => theme.inputBackground};
font-size: ${({ fontSize }) => fontSize && fontSize}; font-size: ${({ fontSize }) => fontSize && fontSize};
text-align: ${({ align }) => align && align}; text-align: ${({ align }) => align && align};

@ -1,28 +1,32 @@
import React, { useState, useRef, useMemo, useEffect } from 'react' import React, { useState, useRef, useMemo, useEffect } from 'react'
import { withRouter } from 'react-router-dom' import '@reach/tooltip/styles.css'
import { Link } from 'react-router-dom'
import { Link as StyledLink } from '../../theme/components'
import { useTranslation } from 'react-i18next'
import { ethers } from 'ethers'
import styled from 'styled-components' import styled from 'styled-components'
import escapeStringRegex from 'escape-string-regexp' 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 { 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 Modal from '../Modal'
import { useToken, useAllTokens, INITIAL_TOKENS_CONTEXT } from '../../contexts/Tokens'
import { Spinner } from '../../theme'
import Circle from '../../assets/images/circle.svg' 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 { useAllBalances } from '../../contexts/Balances'
import { useTranslation } from 'react-i18next'
import { useAllExchanges } from '../../contexts/Exchanges' import { useAllExchanges } from '../../contexts/Exchanges'
import { useToken, useAllTokens, INITIAL_TOKENS_CONTEXT } from '../../contexts/Tokens'
const TokenModalInfo = styled.div` const TokenModalInfo = styled.div`
${({ theme }) => theme.flexRowNoWrap} ${({ theme }) => theme.flexRowNoWrap}
@ -108,7 +112,17 @@ const MenuItem = styled(PaddedItem)`
background-color: ${({ theme }) => theme.tokenRowHover}; 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 { t } = useTranslation()
const { account, chainId } = useWeb3React() const { account, chainId } = useWeb3React()
@ -171,6 +185,7 @@ function SearchModal({ history, isOpen, onDismiss, onTokenSelect, urlAddedTokens
let balance let balance
// only update if we have data // only update if we have data
balance = allBalances?.[account]?.[k] balance = allBalances?.[account]?.[k]
return { return {
name: allTokens[k].name, name: allTokens[k].name,
symbol: allTokens[k].symbol, symbol: allTokens[k].symbol,
@ -203,10 +218,16 @@ function SearchModal({ history, isOpen, onDismiss, onTokenSelect, urlAddedTokens
}) })
}, [tokenList, searchQuery]) }, [tokenList, searchQuery])
function _onTokenSelect(address) { function _onTokenSelect(address, sendWithSwap = false) {
setSearchQuery('') if (sendWithSwap) {
onTokenSelect(address) setSearchQuery('')
onDismiss() onTokenSelectSendWithSwap(address)
onDismiss()
} else {
setSearchQuery('')
onTokenSelect(address)
onDismiss()
}
} }
// manage focus on modal show // manage focus on modal show
@ -340,8 +361,11 @@ function SearchModal({ history, isOpen, onDismiss, onTokenSelect, urlAddedTokens
INITIAL_TOKENS_CONTEXT[chainId] && INITIAL_TOKENS_CONTEXT[chainId] &&
!INITIAL_TOKENS_CONTEXT[chainId].hasOwnProperty(address) && !INITIAL_TOKENS_CONTEXT[chainId].hasOwnProperty(address) &&
!urlAdded !urlAdded
const zeroBalance = JSBI.equal(JSBI.BigInt(0), balance.raw)
return ( return (
<MenuItem key={address} onClick={() => _onTokenSelect(address)}> <MenuItem key={address} onClick={() => (zeroBalance ? _onTokenSelect(address, true) : _onTokenSelect(address))}>
<RowFixed> <RowFixed>
<TokenLogo address={address} size={'24px'} style={{ marginRight: '14px' }} /> <TokenLogo address={address} size={'24px'} style={{ marginRight: '14px' }} />
<Column> <Column>
@ -353,7 +377,22 @@ function SearchModal({ history, isOpen, onDismiss, onTokenSelect, urlAddedTokens
</RowFixed> </RowFixed>
<AutoColumn gap="4px" justify="end"> <AutoColumn gap="4px" justify="end">
{balance ? ( {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 ? ( ) : account ? (
<SpinnerWrapper src={Circle} alt="loader" /> <SpinnerWrapper src={Circle} alt="loader" />
) : ( ) : (

@ -71,7 +71,7 @@ export default function InputSlider({ value, onChange }) {
value={typeof value === 'number' ? value : 0} value={typeof value === 'number' ? value : 0}
onChange={onChange} onChange={onChange}
aria-labelledby="input-slider" aria-labelledby="input-slider"
marks={marks} // marks={marks}
/> />
) )
} }

@ -52,7 +52,7 @@ export default function TokenLogo({ address, size = '24px', ...rest }) {
if (address === 'ETH') { if (address === 'ETH') {
return <StyledEthereumLogo size={size} {...rest} /> return <StyledEthereumLogo size={size} {...rest} />
} else if (!error && !BAD_IMAGES[address]) { } else if (!error && !BAD_IMAGES[address]) {
path = TOKEN_ICON_API(address.toLowerCase()) path = TOKEN_ICON_API(address?.toLowerCase())
} else { } else {
return ( return (
<Emoji {...rest} size={size}> <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

@ -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` const FixedBottom = styled.div`
position: absolute; position: absolute;
bottom: -240px; bottom: -200px;
width: 100%; width: 100%;
` `

@ -2,5 +2,5 @@ import React from 'react'
import ExchangePage from '../../components/ExchangePage' import ExchangePage from '../../components/ExchangePage'
export default function Swap({ initialCurrency, params }) { 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}> <Text fontWeight={500} color={theme().royalBlue} {...rest}>
{children} {children}
</Text> </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]
}