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)`
|
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
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]
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user