diff --git a/src/assets/svg/QR.svg b/src/assets/svg/QR.svg
new file mode 100644
index 0000000000..dabbb02dff
--- /dev/null
+++ b/src/assets/svg/QR.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/components/Button/index.js b/src/components/Button/index.js
index 8ed04be1da..6537a0996a 100644
--- a/src/components/Button/index.js
+++ b/src/components/Button/index.js
@@ -89,6 +89,27 @@ export const ButtonEmpty = styled(Base)`
}
`
+export const ButtonWhite = styled(Base)`
+ border: 1px solid #edeef2;
+ background-color: ${({ theme }) => theme.panelBackground};
+ };
+ color: black;
+
+ &:focus {
+ box-shadow: 0 0 0 1pt ${({ theme }) => darken(0.05, '#edeef2')};
+ }
+ &:hover {
+ box-shadow: 0 0 0 1pt ${({ theme }) => darken(0.1, '#edeef2')};
+ }
+ &:active {
+ box-shadow: 0 0 0 1pt ${({ theme }) => darken(0.1, '#edeef2')};
+ }
+ &:disabled {
+ opacity: 50%;
+ cursor: auto;
+ }
+`
+
const ButtonConfirmedStyle = styled(Base)`
background-color: ${({ theme }) => lighten(0.5, theme.connectedGreen)};
border: 1px solid ${({ theme }) => theme.connectedGreen};
@@ -160,7 +181,7 @@ export function ButtonDropwdownLight({ disabled, children, ...rest }) {
export function ButtonRadio({ active, children, ...rest }) {
if (!active) {
- return {children}
+ return {children}
} else {
return {children}
}
diff --git a/src/components/CurrencyInputPanel/index.js b/src/components/CurrencyInputPanel/index.js
index 659bbde913..280b51e299 100644
--- a/src/components/CurrencyInputPanel/index.js
+++ b/src/components/CurrencyInputPanel/index.js
@@ -48,10 +48,9 @@ const InputRow = styled.div`
const CurrencySelect = styled.button`
align-items: center;
height: 2.2rem;
-
font-size: 20px;
- background-color: ${({ selected, theme }) => (selected ? theme.buttonBackgroundPlain : theme.zumthorBlue)};
- color: ${({ selected, theme }) => (selected ? theme.textColor : theme.royalBlue)};
+ background-color: ${({ selected, theme }) => (selected ? theme.buttonBackgroundPlain : theme.royalBlue)};
+ color: ${({ selected, theme }) => (selected ? theme.textColor : theme.white)};
border: 1px solid
${({ selected, theme, disableTokenSelect }) =>
disableTokenSelect ? theme.buttonBackgroundPlain : selected ? theme.buttonOutlinePlain : theme.royalBlue};
@@ -70,7 +69,8 @@ const CurrencySelect = styled.button`
}
:active {
- background-color: ${({ theme }) => theme.zumthorBlue};
+ background-color: ${({ selected, theme }) =>
+ selected ? darken(0.1, theme.zumthorBlue) : darken(0.1, theme.royalBlue)};
}
`
@@ -85,20 +85,20 @@ const StyledDropDown = styled(DropDown)`
height: 35%;
path {
- stroke: ${({ selected, theme }) => (selected ? theme.textColor : theme.royalBlue)};
+ stroke: ${({ selected, theme }) => (selected ? theme.textColor : theme.white)};
}
`
const InputPanel = styled.div`
${({ theme }) => theme.flexColumnNoWrap}
position: relative;
- border-radius: 1.25rem;
+ border-radius: ${({ hideInput }) => (hideInput ? '8px' : '20px')};
background-color: ${({ theme }) => theme.inputBackground};
z-index: 1;
`
const Container = styled.div`
- border-radius: 1.25rem;
+ border-radius: ${({ hideInput }) => (hideInput ? '8px' : '20px')};
border: 1px solid ${({ error, theme }) => (error ? theme.salmonRed : theme.mercuryGray)};
background-color: ${({ theme }) => theme.inputBackground};
@@ -174,7 +174,10 @@ export default function CurrencyInputPanel({
hideBalance = false,
isExchange = false,
exchange = null, // used for double token logo
- customBalance = null // used for LP balances instead of token balance
+ customBalance = null, // used for LP balances instead of token balance
+ hideInput = false,
+ showSendWithSwap = false,
+ onTokenSelectSendWithSwap = null
}) {
const { account, chainId } = useWeb3React()
const { t } = useTranslation()
@@ -236,7 +239,7 @@ export default function CurrencyInputPanel({
return (
-
+
{!hideBalance && (
@@ -250,15 +253,19 @@ export default function CurrencyInputPanel({
)}
-
- {
- onUserInput(field, val)
- }}
- />
- {!!token?.address && !atMax && MAX}
- {renderUnlockButton()}
+
+ {!hideInput && (
+ <>
+ {
+ onUserInput(field, val)
+ }}
+ />
+ {!!token?.address && !atMax && MAX}
+ {renderUnlockButton()}
+ >
+ )}
{
@@ -296,6 +303,8 @@ export default function CurrencyInputPanel({
urlAddedTokens={urlAddedTokens}
field={field}
onTokenSelect={onTokenSelection}
+ showSendWithSwap={showSendWithSwap}
+ onTokenSelectSendWithSwap={onTokenSelectSendWithSwap}
/>
)}
diff --git a/src/components/ExchangePage/index.tsx b/src/components/ExchangePage/index.tsx
index b0c5307523..0a93a9872f 100644
--- a/src/components/ExchangePage/index.tsx
+++ b/src/components/ExchangePage/index.tsx
@@ -4,6 +4,7 @@ import { ethers } from 'ethers'
import { parseUnits, parseEther } from '@ethersproject/units'
import { WETH, TradeType, Route, Trade, TokenAmount, JSBI } from '@uniswap/sdk'
+import QR from '../../assets/svg/QR.svg'
import TokenLogo from '../TokenLogo'
import QuestionHelper from '../Question'
import NumericalInput from '../NumericalInput'
@@ -11,23 +12,23 @@ import ConfirmationModal from '../ConfirmationModal'
import CurrencyInputPanel from '../CurrencyInputPanel'
import { Link } from '../../theme/components'
import { Text } from 'rebass'
-import ThemeProvider, { TYPE } from '../../theme'
-import { GreyCard } from '../../components/Card'
+import { TYPE } from '../../theme'
+import { GreyCard, LightCard } from '../../components/Card'
import { ArrowDown, ArrowUp } from 'react-feather'
+import { ButtonPrimary, ButtonError, ButtonRadio } from '../Button'
import { AutoColumn, ColumnCenter } from '../../components/Column'
-import { ButtonError, ButtonRadio } from '../Button'
import Row, { RowBetween, RowFixed } from '../../components/Row'
import { usePopups } from '../../contexts/Application'
import { useToken } from '../../contexts/Tokens'
import { useExchange } from '../../contexts/Exchanges'
-import { useWeb3React } from '../../hooks'
+import { useWeb3React, useTokenContract } from '../../hooks'
import { useAddressBalance } from '../../contexts/Balances'
import { useTransactionAdder } from '../../contexts/Transactions'
import { useAddressAllowance } from '../../contexts/Allowances'
import { ROUTER_ADDRESSES } from '../../constants'
-import { getRouterContract, calculateGasMargin } from '../../utils'
+import { getRouterContract, calculateGasMargin, isAddress, getProviderOrSigner } from '../../utils'
const Wrapper = styled.div`
position: relative;
@@ -64,7 +65,66 @@ const InputWrapper = styled(RowBetween)`
border-radius: 8px;
padding: 4px 8px;
border: 1px solid transparent;
- border: ${({ active, theme }) => active && '1px solid ' + theme.royalBlue};
+ border: ${({ active, error, theme }) =>
+ error ? '1px solid ' + theme.salmonRed : active ? '1px solid ' + theme.royalBlue : ''};
+`
+
+const InputGroup = styled(AutoColumn)`
+ position: relative;
+ padding: 40px 0;
+`
+
+const QRWrapper = styled.div`
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border: 1px solid ${({ theme }) => theme.outlineGrey};
+ background: #fbfbfb;
+ padding: 4px;
+ border-radius: 8px;
+`
+
+const StyledInput = styled.input`
+ width: ${({ width }) => width};
+ border: none;
+ outline: none;
+ font-size: 20px;
+
+ ::placeholder {
+ color: #edeef2;
+ }
+`
+
+const StyledNumerical = styled(NumericalInput)`
+ text-align: center;
+ font-size: 48px;
+ font-weight: 500px;
+ width: 100%;
+
+ ::placeholder {
+ color: #edeef2;
+ }
+`
+
+const MaxButton = styled.button`
+ position: absolute;
+ right: 70px;
+ padding: 0.5rem 1rem;
+ background-color: ${({ theme }) => theme.zumthorBlue};
+ border: 1px solid ${({ theme }) => theme.zumthorBlue};
+ border-radius: 0.5rem;
+ font-size: 1rem;
+ font-weight: 500;
+ cursor: pointer;
+ margin-right: 0.5rem;
+ color: ${({ theme }) => theme.royalBlue};
+ :hover {
+ border: 1px solid ${({ theme }) => theme.royalBlue};
+ }
+ :focus {
+ border: 1px solid ${({ theme }) => theme.royalBlue};
+ outline: none;
+ }
`
enum Field {
@@ -91,7 +151,7 @@ function initializeSwapState(inputAddress?: string, outputAddress?: string): Swa
address: inputAddress
},
[Field.OUTPUT]: {
- address: '0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735'
+ address: outputAddress
}
}
}
@@ -192,22 +252,28 @@ const INITIAL_ALLOWED_SLIPPAGE = 200
const DEFAULT_DEADLINE_FROM_NOW = 60 * 15
// used for warning states based on slippage in bips
-const ALLOWED_IMPACT_MEDIUM = 100
-const ALLOWED_IMPACT_HIGH = 500
+const ALLOWED_SLIPPAGE_MEDIUM = 100
+const ALLOWED_SLIPPAGE_HIGH = 500
-export default function ExchangePage() {
+export default function ExchangePage({ sendingInput = false }) {
const { chainId, account, library } = useWeb3React()
const routerAddress = ROUTER_ADDRESSES[chainId]
// adding notifications on txns
const [, addPopup] = usePopups()
+ const addTransaction = useTransactionAdder()
+
+ // sending state
+ const [sending, setSending] = useState(sendingInput)
+ const [sendingWithSwap, setSendingWithSwap] = useState(false)
+ const [recipient, setRecipient] = useState('')
// input details
const [state, dispatch] = useReducer(reducer, WETH[chainId].address, initializeSwapState)
const { independentField, typedValue, ...fieldData } = state
const dependentField = independentField === Field.INPUT ? Field.OUTPUT : Field.INPUT
const tradeType = independentField === Field.INPUT ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT
- const [tradeError, setTradeError] = useState('') // error for thinsg liek reserve sizes
+ const [tradeError, setTradeError] = useState('') // error for things like reserve size or route
const tokens = {
[Field.INPUT]: useToken(fieldData[Field.INPUT].address),
@@ -217,12 +283,18 @@ export default function ExchangePage() {
const exchange = useExchange(tokens[Field.INPUT], tokens[Field.OUTPUT])
const route = !!exchange ? new Route([exchange], tokens[Field.INPUT]) : undefined
- // modal state
- const addTransaction = useTransactionAdder()
- const [showConfirm, setShowConfirm] = useState(true)
+ // modal and loading
+ const [showConfirm, setShowConfirm] = useState(false)
const [pendingConfirmation, setPendingConfirmation] = useState(true) // waiting for user confirmation
const [attemptingTxn, setAttemptingTxn] = useState(false) // clicked confirmed
+ // advanced settings
+ const [showAdvanced, setShowAdvanced] = useState(false)
+ const [activeIndex, setActiveIndex] = useState(SLIPPAGE_INDEX[3])
+ const [customSlippage, setCustomSlippage] = useState()
+ const [customDeadline, setCustomDeadline] = useState(DEFAULT_DEADLINE_FROM_NOW / 60)
+ const [slippageInputError, setSlippageInputError] = useState(null)
+
// txn values
const [txHash, setTxHash] = useState()
const [deadline, setDeadline] = useState(DEFAULT_DEADLINE_FROM_NOW)
@@ -240,10 +312,9 @@ export default function ExchangePage() {
const parsedAmounts: { [field: number]: TokenAmount } = {}
// try to parse typed value
- // if (typedValue !== '' && typedValue !== '.' && tokens[independentField]) {
- if (tokens[independentField]) {
+ if (typedValue !== '' && typedValue !== '.' && tokens[independentField]) {
try {
- const typedValueParsed = parseUnits('0.0001', tokens[independentField].decimals).toString()
+ const typedValueParsed = parseUnits(typedValue, tokens[independentField].decimals).toString()
if (typedValueParsed !== '0')
parsedAmounts[independentField] = new TokenAmount(tokens[independentField], typedValueParsed)
} catch (error) {
@@ -313,7 +384,7 @@ export default function ExchangePage() {
})
}, [])
- const MIN_ETHER = new TokenAmount(WETH[chainId], JSBI.BigInt(parseEther('.01')))
+ const MIN_ETHER = chainId && new TokenAmount(WETH[chainId], JSBI.BigInt(parseEther('.01')))
const maxAmountInput =
!!userBalances[Field.INPUT] &&
JSBI.greaterThan(
@@ -388,16 +459,14 @@ export default function ExchangePage() {
outputApproval &&
JSBI.greaterThan(parsedAmounts[Field.OUTPUT].raw, outputApproval.raw)
- // modal state
- const [showAdvanced, setShowAdvanced] = useState(true)
- const [activeIndex, setActiveIndex] = useState(SLIPPAGE_INDEX[3])
- const [customSlippage, setCustomSlippage] = useState()
- const [customDeadline, setCustomDeadline] = useState(DEFAULT_DEADLINE_FROM_NOW / 60)
-
- const [slippageInputError, setSlippageInputError] = useState(null)
-
+ // parse the input for custom slippage
function parseCustomInput(val) {
const acceptableValues = [/^$/, /^\d{1,2}$/, /^\d{0,2}\.\d{0,2}$/]
+ if (val > 5) {
+ setSlippageInputError('Your transaction may be front-run.')
+ } else {
+ setSlippageInputError(null)
+ }
if (acceptableValues.some(a => a.test(val))) {
setCustomSlippage(val)
setAllowedSlippage(val * 100)
@@ -412,6 +481,62 @@ export default function ExchangePage() {
}
}
+ const tokenContract = useTokenContract(tokens[Field.INPUT]?.address)
+
+ // function for a pure send
+ async function onSend() {
+ setAttemptingTxn(true)
+
+ const signer = await getProviderOrSigner(library, account)
+ // get token contract if needed
+ let estimate: Function, method: Function, args, value
+ if (tokens[Field.INPUT] === WETH[chainId]) {
+ signer
+ .sendTransaction({ to: recipient.toString(), value: hex(parsedAmounts[Field.INPUT].raw) })
+ .then(response => {
+ console.log(response)
+ setTxHash(response.hash)
+ addTransaction(response)
+ setPendingConfirmation(false)
+ })
+ .catch(e => {
+ addPopup(
+
+ Transaction Failed: try again.
+
+ )
+ 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(
+
+ Transaction Failed: try again.
+
+ )
+ resetModal()
+ setShowConfirm(false)
+ })
+ }
+ }
+
async function onSwap() {
const routerContract = getRouterContract(chainId, library, account)
setAttemptingTxn(true)
@@ -497,7 +622,7 @@ export default function ExchangePage() {
gasLimit: calculateGasMargin(estimatedGasLimit, GAS_MARGIN)
})
.then(response => {
- setTxHash(response)
+ setTxHash(response.hash)
addTransaction(response)
setPendingConfirmation(false)
})
@@ -513,17 +638,38 @@ export default function ExchangePage() {
}
// errors
+ const [generalError, setGeneralError] = useState('')
const [inputError, setInputError] = useState('')
const [outputError, setOutputError] = useState('')
+ const [recipientError, setRecipientError] = useState('')
const [isValid, setIsValid] = useState(false)
+ const ignoreOutput = sending ? !sendingWithSwap : false
+
useEffect(() => {
// reset errors
+ setGeneralError(null)
setInputError(null)
setOutputError(null)
setTradeError(null)
+ setRecipientError(null)
setIsValid(true)
+ if (!isAddress(recipient) && sending) {
+ setRecipientError('Invalid Recipient')
+ setIsValid(false)
+ }
+
+ if (!parsedAmounts[Field.INPUT]) {
+ setGeneralError('Enter an amount')
+ setIsValid(false)
+ }
+
+ if (!parsedAmounts[Field.OUTPUT] && !ignoreOutput) {
+ setGeneralError('Enter an amount')
+ setIsValid(false)
+ }
+
if (
parsedAmounts[Field.INPUT] &&
exchange &&
@@ -534,6 +680,7 @@ export default function ExchangePage() {
}
if (
+ !ignoreOutput &&
parsedAmounts[Field.OUTPUT] &&
exchange &&
JSBI.greaterThan(parsedAmounts[Field.OUTPUT].raw, exchange.reserveOf(tokens[Field.OUTPUT]).raw)
@@ -542,12 +689,12 @@ export default function ExchangePage() {
setIsValid(false)
}
- if (showInputUnlock) {
+ if (showInputUnlock && !(sending && !sendingWithSwap)) {
setInputError('Approval Needed')
setIsValid(false)
}
- if (showOutputUnlock) {
+ if (showOutputUnlock && !ignoreOutput) {
setOutputError('Approval Needed')
setIsValid(false)
}
@@ -560,19 +707,22 @@ export default function ExchangePage() {
setInputError('Insufficient balance.')
setIsValid(false)
}
+ }, [
+ exchange,
+ ignoreOutput,
+ parsedAmounts,
+ recipient,
+ sending,
+ sendingWithSwap,
+ showInputUnlock,
+ showOutputUnlock,
+ tokens,
+ userBalances
+ ])
- if (
- userBalances[Field.OUTPUT] &&
- parsedAmounts[Field.OUTPUT] &&
- JSBI.lessThan(userBalances[Field.OUTPUT].raw, parsedAmounts[Field.OUTPUT]?.raw)
- ) {
- setOutputError('Insufficient balance.')
- setIsValid(false)
- }
- }, [exchange, parsedAmounts, showInputUnlock, showOutputUnlock, tokens, userBalances])
-
- const warningMedium = slippageFromTrade && parseFloat(slippageFromTrade.toFixed(4)) > ALLOWED_IMPACT_MEDIUM / 100
- const warningHigh = slippageFromTrade && parseFloat(slippageFromTrade.toFixed(4)) > ALLOWED_IMPACT_HIGH / 100
+ // warnings on slippage
+ const warningMedium = slippageFromTrade && parseFloat(slippageFromTrade.toFixed(4)) > ALLOWED_SLIPPAGE_MEDIUM / 100
+ const warningHigh = slippageFromTrade && parseFloat(slippageFromTrade.toFixed(4)) > ALLOWED_SLIPPAGE_HIGH / 100
function resetModal() {
setPendingConfirmation(true)
@@ -581,169 +731,221 @@ export default function ExchangePage() {
}
function modalHeader() {
- return (
-
-
-
- {!!slippageAdjustedAmounts[Field.INPUT] && slippageAdjustedAmounts[Field.INPUT].toSignificant(6)}
-
-
-
-
- {tokens[Field.INPUT]?.symbol || ''}
+ if (sending && !sendingWithSwap) {
+ return (
+
+
+
+ {parsedAmounts[Field.INPUT]?.toFixed(8)}
-
-
-
-
-
-
-
- {!!slippageAdjustedAmounts[Field.OUTPUT] && slippageAdjustedAmounts[Field.OUTPUT].toSignificant(6)}
-
-
-
-
- {tokens[Field.OUTPUT]?.symbol || ''}
+
+
+
+
+ {recipient?.slice(0, 6)}...{recipient?.slice(36, 42)}
+
+
+ )
+ }
+
+ if (sending && sendingWithSwap) {
+ }
+
+ if (!sending) {
+ return (
+
+
+
+ {!!slippageAdjustedAmounts[Field.INPUT] && slippageAdjustedAmounts[Field.INPUT].toSignificant(6)}
+
+
+
+ {tokens[Field.INPUT]?.symbol || ''}
+
+
+
+
+
-
-
- )
+
+
+ {!!slippageAdjustedAmounts[Field.OUTPUT] && slippageAdjustedAmounts[Field.OUTPUT].toSignificant(6)}
+
+
+
+
+ {tokens[Field.OUTPUT]?.symbol || ''}
+
+
+
+
+ )
+ }
}
function modalBottom() {
- return showAdvanced ? (
-
- {
- setShowAdvanced(false)
- }}
- >
- back
-
-
- Limit additional price slippage
-
-
-
- {
- setActiveIndex(SLIPPAGE_INDEX[1])
- setAllowedSlippage(10)
- }}
- >
- 0.1%
-
- {
- setActiveIndex(SLIPPAGE_INDEX[2])
- setAllowedSlippage(100)
- }}
- >
- 1%
-
- {
- setActiveIndex(SLIPPAGE_INDEX[3])
- setAllowedSlippage(200)
- }}
- >
- 2% (suggested)
-
-
-
-
- {
- parseCustomInput(val)
- setActiveIndex(SLIPPAGE_INDEX[4])
- }}
- placeHolder="Custom"
- onClick={() => {
- setActiveIndex(SLIPPAGE_INDEX[4])
- if (customSlippage) {
- parseCustomInput(customSlippage)
- }
- }}
- />
- %
-
-
-
- Adjust deadline (minutes from now)
-
-
- {
- parseCustomDeadline(val)
- }}
- />
-
-
- ) : (
- <>
-
-
- Price
-
-
- {`1 ${tokens[Field.INPUT]?.symbol} = ${route && route.midPrice && route.midPrice.adjusted.toFixed(8)} ${
- tokens[Field.OUTPUT]?.symbol
- }`}
-
-
-
-
- Slippage setShowAdvanced(true)}>(edit limits)
-
-
- {slippageFromTrade && slippageFromTrade.toFixed(4)}%
-
-
-
-
- {warningHigh ? 'Swap Anyway' : 'Swap'}
-
-
-
- {`Output is estimated. You will receive at least ${slippageAdjustedAmounts[Field.OUTPUT]?.toSignificant(6)} ${
- tokens[Field.OUTPUT]?.symbol
- } or the transaction will revert.`}
-
-
+ if (sending && !sendingWithSwap) {
+ return (
+
+
+
+ Confirm send
+
+
+
+ )
+ }
+
+ if (sending && sendingWithSwap) {
+ }
+
+ if (showAdvanced) {
+ return (
+
{
- setShowAdvanced(true)
+ setShowAdvanced(false)
}}
>
- Advanced Options
+ back
+
+ Limit additional price impact
+
+
+
+ {
+ setActiveIndex(SLIPPAGE_INDEX[1])
+ setAllowedSlippage(10)
+ }}
+ >
+ 0.1%
+
+ {
+ setActiveIndex(SLIPPAGE_INDEX[2])
+ setAllowedSlippage(100)
+ }}
+ >
+ 1%
+
+ {
+ setActiveIndex(SLIPPAGE_INDEX[3])
+ setAllowedSlippage(200)
+ }}
+ >
+ 2% (suggested)
+
+
+
+
+ {
+ parseCustomInput(val)
+ setActiveIndex(SLIPPAGE_INDEX[4])
+ }}
+ placeHolder="Custom"
+ onClick={() => {
+ setActiveIndex(SLIPPAGE_INDEX[4])
+ if (customSlippage) {
+ parseCustomInput(customSlippage)
+ }
+ }}
+ />
+ %
+
+ {slippageInputError && (
+
+ Your transaction may be front-run
+
+ )}
+
+
+ Adjust deadline (minutes from now)
+
+
+
+ {
+ parseCustomDeadline(val)
+ }}
+ />
+
+
- >
- )
+ )
+ }
+
+ if (!sending) {
+ return (
+ <>
+
+
+ Price
+
+
+ {`1 ${tokens[Field.INPUT]?.symbol} = ${route && route.midPrice && route.midPrice.adjusted.toFixed(8)} ${
+ tokens[Field.OUTPUT]?.symbol
+ }`}
+
+
+
+
+ Slippage setShowAdvanced(true)}>(edit limits)
+
+
+ {slippageFromTrade && slippageFromTrade.toFixed(4)}%
+
+
+
+
+ {warningHigh ? 'Swap Anyway' : 'Swap'}
+
+
+
+
+ {`Output is estimated. You will receive at least ${slippageAdjustedAmounts[Field.OUTPUT]?.toSignificant(
+ 6
+ )} ${tokens[Field.OUTPUT]?.symbol} or the transaction will revert.`}
+
+ {
+ setShowAdvanced(true)
+ }}
+ >
+ Advanced Options
+
+
+ >
+ )
+ }
}
- const pendingText = ` Swapped ${parsedAmounts[Field.INPUT]?.toSignificant(6)} ${
- tokens[Field.INPUT]?.symbol
- } for ${parsedAmounts[Field.OUTPUT]?.toSignificant(6)} ${tokens[Field.OUTPUT]?.symbol}`
+ const pendingText = sending
+ ? `Sending ${parsedAmounts[Field.INPUT]?.toSignificant(6)} ${tokens[Field.INPUT]?.symbol} to ${recipient}`
+ : ` Swapped ${parsedAmounts[Field.INPUT]?.toSignificant(6)} ${tokens[Field.INPUT]?.symbol} for ${parsedAmounts[
+ Field.OUTPUT
+ ]?.toSignificant(6)} ${tokens[Field.OUTPUT]?.symbol}`
return (
@@ -756,69 +958,128 @@ export default function ExchangePage() {
attemptingTxn={attemptingTxn}
pendingConfirmation={pendingConfirmation}
hash={txHash ? txHash : ''}
- topContent={() => modalHeader()}
+ topContent={modalHeader}
bottomContent={modalBottom}
pendingText={pendingText}
- title="Confirm Swap"
+ title={sendingWithSwap ? 'Confirm swap and send' : sending ? 'Confirm Send' : 'Confirm Swap'}
/>
+
+ {sending && !sendingWithSwap && (
+ <>
+
+ {!atMaxAmountInput && (
+ {
+ maxAmountInput && onMaxInput(maxAmountInput.toExact())
+ }}
+ >
+ Max
+
+ )}
+ onUserInput(Field.INPUT, val)} />
+ {!parsedAmounts[Field.INPUT] && Enter an amount.}
+ 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}
+ />
+
+ >
+ )}
+
- {
- maxAmountInput && onMaxInput(maxAmountInput.toExact())
- }}
- atMax={atMaxAmountInput}
- token={tokens[Field.INPUT]}
- onTokenSelection={address => onTokenSelection(Field.INPUT, address)}
- title={'Input'}
- error={inputError}
- exchange={exchange}
- showUnlock={showInputUnlock}
- />
-
-
-
-
-
-
- {
- maxAmountOutput && onMaxOutput(maxAmountOutput.toExact())
- }}
- atMax={atMaxAmountOutput}
- token={tokens[Field.OUTPUT]}
- onTokenSelection={address => onTokenSelection(Field.OUTPUT, address)}
- title={'Output'}
- error={outputError}
- exchange={exchange}
- showUnlock={showOutputUnlock}
- />
-
-
- Price
-
-
- {exchange
- ? `1 ${tokens[Field.INPUT].symbol} = ${route?.midPrice.toSignificant(6)} ${tokens[Field.OUTPUT].symbol}`
- : '-'}
-
-
- {warningMedium && (
-
-
- Slippage
-
-
- {slippageFromTrade.toFixed(4)}%
-
-
+ {(!sending || sendingWithSwap) && (
+ <>
+ {
+ maxAmountInput && onMaxInput(maxAmountInput.toExact())
+ }}
+ atMax={atMaxAmountInput}
+ token={tokens[Field.INPUT]}
+ onTokenSelection={address => onTokenSelection(Field.INPUT, address)}
+ title={'Input'}
+ error={inputError}
+ exchange={exchange}
+ showUnlock={showInputUnlock}
+ />
+
+
+
+
+
+
+ {
+ maxAmountOutput && onMaxOutput(maxAmountOutput.toExact())
+ }}
+ atMax={atMaxAmountOutput}
+ token={tokens[Field.OUTPUT]}
+ onTokenSelection={address => onTokenSelection(Field.OUTPUT, address)}
+ title={'Output'}
+ error={outputError}
+ exchange={exchange}
+ showUnlock={showOutputUnlock}
+ />
+
+
+ Price
+
+
+ {exchange
+ ? `1 ${tokens[Field.INPUT].symbol} = ${route?.midPrice.toSignificant(6)} ${
+ tokens[Field.OUTPUT].symbol
+ }`
+ : '-'}
+
+
+ {warningMedium && (
+
+
+ Slippage
+
+
+ {slippageFromTrade.toFixed(4)}%
+
+
+ )}
+ >
)}
+ {sending && (
+
+
+
+ setRecipient(e.target.value)} />
+
+
+
+
+
+
+ )}
{
setShowConfirm(true)
@@ -827,10 +1088,14 @@ export default function ExchangePage() {
error={!!warningHigh}
>
- {inputError
+ {generalError
+ ? generalError
+ : inputError
? inputError
: outputError
? outputError
+ : recipientError
+ ? recipientError
: tradeError
? tradeError
: warningHigh
@@ -839,6 +1104,7 @@ export default function ExchangePage() {
+
{warningHigh && (
diff --git a/src/components/NumericalInput/index.tsx b/src/components/NumericalInput/index.tsx
index f9eda5a3db..dd0ed67644 100644
--- a/src/components/NumericalInput/index.tsx
+++ b/src/components/NumericalInput/index.tsx
@@ -5,11 +5,11 @@ const StyledInput = styled.input`
color: ${({ error, theme }) => error && theme.salmonRed};
background-color: ${({ theme }) => theme.inputBackground};
color: ${({ theme }) => theme.textColor};
+ width: 0;
font-size: 20px;
outline: none;
border: none;
flex: 1 1 auto;
- width: 0;
background-color: ${({ theme }) => theme.inputBackground};
font-size: ${({ fontSize }) => fontSize && fontSize};
text-align: ${({ align }) => align && align};
diff --git a/src/components/SearchModal/index.js b/src/components/SearchModal/index.js
index b707c2d500..4045211c91 100644
--- a/src/components/SearchModal/index.js
+++ b/src/components/SearchModal/index.js
@@ -1,28 +1,32 @@
import React, { useState, useRef, useMemo, useEffect } from 'react'
-import { withRouter } from 'react-router-dom'
-import { Link } from 'react-router-dom'
-import { Link as StyledLink } from '../../theme/components'
-import { useTranslation } from 'react-i18next'
-import { ethers } from 'ethers'
+import '@reach/tooltip/styles.css'
import styled from 'styled-components'
import escapeStringRegex from 'escape-string-regexp'
-import '@reach/tooltip/styles.css'
+import { Link } from 'react-router-dom'
+import { ethers } from 'ethers'
import { isMobile } from 'react-device-detect'
+import { withRouter } from 'react-router-dom'
+import { JSBI } from '@uniswap/sdk'
+
+import { Link as StyledLink } from '../../theme/components'
-import { Text } from 'rebass'
-import Column, { AutoColumn } from '../Column'
-import { RowBetween, RowFixed } from '../Row'
-import TokenLogo from '../TokenLogo'
-import { CloseIcon } from '../../theme/components'
-import DoubleTokenLogo from '../DoubleLogo'
-import { useWeb3React } from '../../hooks'
-import { isAddress } from '../../utils'
import Modal from '../Modal'
-import { useToken, useAllTokens, INITIAL_TOKENS_CONTEXT } from '../../contexts/Tokens'
-import { Spinner } from '../../theme'
import Circle from '../../assets/images/circle.svg'
+import TokenLogo from '../TokenLogo'
+import DoubleTokenLogo from '../DoubleLogo'
+import Column, { AutoColumn } from '../Column'
+import { Text } from 'rebass'
+import { Spinner } from '../../theme'
+import { CloseIcon } from '../../theme/components'
+import { ColumnCenter } from '../../components/Column'
+import { RowBetween, RowFixed } from '../Row'
+
+import { isAddress } from '../../utils'
+import { useWeb3React } from '../../hooks'
import { useAllBalances } from '../../contexts/Balances'
+import { useTranslation } from 'react-i18next'
import { useAllExchanges } from '../../contexts/Exchanges'
+import { useToken, useAllTokens, INITIAL_TOKENS_CONTEXT } from '../../contexts/Tokens'
const TokenModalInfo = styled.div`
${({ theme }) => theme.flexRowNoWrap}
@@ -108,7 +112,17 @@ const MenuItem = styled(PaddedItem)`
background-color: ${({ theme }) => theme.tokenRowHover};
}
`
-function SearchModal({ history, isOpen, onDismiss, onTokenSelect, urlAddedTokens, filterType, hiddenToken }) {
+function SearchModal({
+ history,
+ isOpen,
+ onDismiss,
+ onTokenSelect,
+ urlAddedTokens,
+ filterType,
+ hiddenToken,
+ showSendWithSwap,
+ onTokenSelectSendWithSwap
+}) {
const { t } = useTranslation()
const { account, chainId } = useWeb3React()
@@ -171,6 +185,7 @@ function SearchModal({ history, isOpen, onDismiss, onTokenSelect, urlAddedTokens
let balance
// only update if we have data
balance = allBalances?.[account]?.[k]
+
return {
name: allTokens[k].name,
symbol: allTokens[k].symbol,
@@ -203,10 +218,16 @@ function SearchModal({ history, isOpen, onDismiss, onTokenSelect, urlAddedTokens
})
}, [tokenList, searchQuery])
- function _onTokenSelect(address) {
- setSearchQuery('')
- onTokenSelect(address)
- onDismiss()
+ function _onTokenSelect(address, sendWithSwap = false) {
+ if (sendWithSwap) {
+ setSearchQuery('')
+ onTokenSelectSendWithSwap(address)
+ onDismiss()
+ } else {
+ setSearchQuery('')
+ onTokenSelect(address)
+ onDismiss()
+ }
}
// manage focus on modal show
@@ -340,8 +361,11 @@ function SearchModal({ history, isOpen, onDismiss, onTokenSelect, urlAddedTokens
INITIAL_TOKENS_CONTEXT[chainId] &&
!INITIAL_TOKENS_CONTEXT[chainId].hasOwnProperty(address) &&
!urlAdded
+
+ const zeroBalance = JSBI.equal(JSBI.BigInt(0), balance.raw)
+
return (
-