Commit 4e2c5c1e authored by ianlapham's avatar ianlapham

slippage without micro warnings on swap

parent e43d9e03
......@@ -13,6 +13,7 @@ const Base = styled(RebassButton)`
font-weight: 500;
text-align: center;
border-radius: 20px;
border-radius: ${({ borderRadius }) => borderRadius && borderRadius};
outline: none;
border: 1px solid transparent;
color: white;
......@@ -40,8 +41,6 @@ export const ButtonPrimary = styled(Base)`
background-color: ${({ theme }) => theme.outlineGrey};
color: ${({ theme }) => theme.darkGrey}
cursor: auto;
outline: none;
border: none;
box-shadow: none;
}
`
......@@ -100,6 +99,27 @@ const ButtonConfirmedStyle = styled(Base)`
}
`
const ButtonErrorStyle = styled(Base)`
background-color: ${({ theme }) => theme.salmonRed};
border: 1px solid ${({ theme }) => theme.salmonRed};
&:focus {
box-shadow: 0 0 0 1pt ${({ theme }) => darken(0.05, theme.salmonRed)};
background-color: ${({ theme }) => darken(0.05, theme.salmonRed)};
}
&:hover {
background-color: ${({ theme }) => darken(0.05, theme.salmonRed)};
}
&:active {
box-shadow: 0 0 0 1pt ${({ theme }) => darken(0.1, theme.salmonRed)};
background-color: ${({ theme }) => darken(0.1, theme.salmonRed)};
}
&:disabled {
opacity: 50%;
cursor: auto;
}
`
export function ButtonConfirmed({ children, confirmed, ...rest }) {
if (confirmed) {
return <ButtonConfirmedStyle {...rest}>{children}</ButtonConfirmedStyle>
......@@ -108,6 +128,14 @@ export function ButtonConfirmed({ children, confirmed, ...rest }) {
}
}
export function ButtonError({ children, error, ...rest }) {
if (error) {
return <ButtonErrorStyle {...rest}>{children}</ButtonErrorStyle>
} else {
return <ButtonPrimary {...rest}>{children}</ButtonPrimary>
}
}
export function ButtonDropwdown({ disabled, children, ...rest }) {
return (
<ButtonPrimary {...rest}>
......@@ -129,3 +157,11 @@ export function ButtonDropwdownLight({ disabled, children, ...rest }) {
</ButtonEmpty>
)
}
export function ButtonRadio({ active, children, ...rest }) {
if (!active) {
return <ButtonEmpty {...rest}>{children}</ButtonEmpty>
} else {
return <ButtonPrimary {...rest}>{children}</ButtonPrimary>
}
}
......@@ -3,7 +3,7 @@ import { Box } from 'rebass/styled-components'
const Card = styled(Box)`
width: 100%;
border-radius: 20px;
border-radius: 8px;
padding: 1rem;
padding: ${({ padding }) => padding};
border: ${({ border }) => border};
......@@ -16,5 +16,5 @@ export const LightCard = styled(Card)`
`
export const GreyCard = styled(Card)`
background-color: rgba(255, 255, 255, 0.6);
background-color: rgba(255, 255, 255, 0.9);
`
import React from 'react'
import styled from 'styled-components'
import { ButtonPrimary } from '../Button'
import { AutoColumn, ColumnCenter } from '../Column'
import Row, { RowBetween, RowFlat, RowFixed } from '../Row'
import { ArrowDown } from 'react-feather'
import { ButtonConfirmed } from '../Button'
import { Text } from 'rebass'
import { LightCard } from '../Card'
import Modal from '../Modal'
import { CheckCircle } from 'react-feather'
import DoubleTokenLogo from '../DoubleLogo'
import TokenLogo from '../TokenLogo'
import { CloseIcon } from '../../theme/components'
import Loader from '../Loader'
import { Link } from '../../theme'
import { Text } from 'rebass'
import { CloseIcon } from '../../theme/components'
import { RowBetween } from '../Row'
import { CheckCircle } from 'react-feather'
import { ButtonPrimary } from '../Button'
import { AutoColumn, ColumnCenter } from '../Column'
import { useWeb3React } from '../../hooks'
import { getEtherscanLink } from '../../utils'
import { TRANSACTION_TYPE } from '../../constants'
const Wrapper = styled.div`
width: 100%;
`
const Section = styled(AutoColumn)`
padding: 2rem;
padding: 26px;
`
const BottomSection = styled(Section)`
......@@ -36,29 +29,17 @@ const ConfirmedIcon = styled(ColumnCenter)`
padding: 60px 0;
`
const ConfirmedText = styled(Text)`
color: ${({ theme, confirmed }) => (confirmed ? theme.connectedGreen : theme.white)};
`
export default function ConfirmationModal({
isOpen,
onDismiss,
liquidityAmount = undefined,
poolTokenPercentage = undefined,
amount0,
amount1,
price,
transactionType,
hash,
signed = false,
contractCall,
attemptedRemoval = false,
topContent,
bottomContent,
attemptingTxn,
pendingConfirmation,
extraCall = undefined
pendingText,
title = ''
}) {
const { address: address0, symbol: symbol0 } = amount0?.token || {}
const { address: address1, symbol: symbol1 } = amount1?.token || {}
const { chainId } = useWeb3React()
function WrappedOnDismissed() {
......@@ -66,208 +47,19 @@ export default function ConfirmationModal({
}
return (
<Modal isOpen={isOpen} onDismiss={WrappedOnDismissed}>
{!attemptedRemoval ? (
<Modal isOpen={isOpen} onDismiss={WrappedOnDismissed} maxHeight={90}>
{!attemptingTxn ? (
<Wrapper>
<Section gap="40px">
<Section>
<RowBetween>
<Text fontWeight={500} fontSize={'20px'}>
{transactionType === TRANSACTION_TYPE.SWAP ? 'Confirm Swap' : 'You will receive'}
<Text fontWeight={500} fontSize={20}>
{title}
</Text>
<CloseIcon onClick={WrappedOnDismissed} />
</RowBetween>
{transactionType === TRANSACTION_TYPE.SWAP && (
<AutoColumn gap={'20px'}>
<LightCard>
<RowBetween>
<Text fontSize={24} fontWeight={500}>
{!!amount0 && amount0?.toSignificant(6)}
</Text>
<RowFixed gap="10px">
<TokenLogo address={amount0?.token?.address} size={'24px'} />
<Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}>
{symbol0}
</Text>
</RowFixed>
</RowBetween>
</LightCard>
<ColumnCenter>
<ArrowDown size="16" color="#888D9B" />
</ColumnCenter>
<LightCard>
<RowBetween>
<Text fontSize={24} fontWeight={500}>
{!!amount1 && amount1?.toSignificant(6)}
</Text>
<RowFixed gap="10px">
<TokenLogo address={amount1?.token?.address} size={'24px'} />
<Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}>
{symbol1}
</Text>
</RowFixed>
</RowBetween>
</LightCard>
</AutoColumn>
)}
{transactionType === TRANSACTION_TYPE.ADD && (
<AutoColumn gap="16px">
<RowFlat>
<Text fontSize="48px" fontWeight={500} lineHeight="32px" marginRight={10}>
{liquidityAmount?.toFixed(6)}
</Text>
<DoubleTokenLogo a0={address0 || ''} a1={address1 || ''} size={20} />
</RowFlat>
<Row>
<Text fontSize="24px">{symbol0 + ':' + symbol1 + ' Pool Tokens'}</Text>
</Row>
</AutoColumn>
)}
{transactionType === TRANSACTION_TYPE.REMOVE && (
<AutoColumn gap="16px">
<Row>
<TokenLogo address={address0} size={'30px'} />
<Text fontSize="24px" marginLeft={10}>
{symbol0} {!!amount0 && amount0?.toSignificant(8)}
</Text>
</Row>
<Row>
<TokenLogo address={address1} size={'30px'} />
<Text fontSize="24px" marginLeft={10}>
{symbol1} {!!amount1 && amount1?.toSignificant(8)}
</Text>
</Row>
</AutoColumn>
)}
{topContent()}
</Section>
<BottomSection gap="12px">
<AutoColumn gap="12px">
{transactionType === TRANSACTION_TYPE.ADD && (
<>
<RowBetween>
<Text color="#565A69" fontWeight={500} fontSize={16}>
{symbol0} Deposited
</Text>
<RowFixed>
<TokenLogo address={address0 || ''} style={{ marginRight: '8px' }} />
<Text fontWeight={500} fontSize={16}>
{!!amount0 && amount0?.toSignificant(6)}
</Text>
</RowFixed>
</RowBetween>
<RowBetween>
<Text color="#565A69" fontWeight={500} fontSize={16}>
{symbol1} Deposited
</Text>
<RowFixed>
<TokenLogo address={address1 || ''} style={{ marginRight: '8px' }} />
<Text fontWeight={500} fontSize={16}>
{amount1?.toSignificant(6)}
</Text>
</RowFixed>
</RowBetween>
</>
)}
{transactionType === TRANSACTION_TYPE.REMOVE && (
<RowBetween>
<Text color="#565A69" fontWeight={500} fontSize={16}>
{'UNI ' + symbol0 + ':' + symbol1} Burned
</Text>
<RowFixed>
<DoubleTokenLogo a0={address0 || ''} a1={address1 || ''} margin={true} />
<Text fontWeight={500} fontSize={16}>
{liquidityAmount?.toSignificant(6)}
</Text>
</RowFixed>
</RowBetween>
)}
{price && price?.adjusted && (
<RowBetween>
<Text color="#565A69" fontWeight={500} fontSize={16}>
Rate
</Text>
<Text fontWeight={500} fontSize={16}>
{`1 ${symbol0} = ${price.adjusted.toFixed(8)} ${symbol1}`}
</Text>
</RowBetween>
)}
{transactionType === TRANSACTION_TYPE.ADD && poolTokenPercentage && (
<RowBetween>
<Text color="#565A69" fontWeight={500} fontSize={16}>
Minted Pool Share:
</Text>
<Text fontWeight={500} fontSize={16}>
{poolTokenPercentage?.toFixed(6) + '%'}
</Text>
</RowBetween>
)}
{transactionType === TRANSACTION_TYPE.REMOVE ? (
<RowBetween gap="20px">
<ButtonConfirmed
style={{ margin: '20px 0' }}
width="48%"
onClick={() => {
extraCall()
}}
confirmed={signed}
disabled={signed}
>
<ConfirmedText fontWeight={500} fontSize={20} confirmed={signed}>
{signed ? 'Signed' : 'Sign'}
</ConfirmedText>
</ButtonConfirmed>
<ButtonPrimary
width="48%"
disabled={!signed}
style={{ margin: '20px 0' }}
onClick={() => {
contractCall()
}}
>
<Text fontWeight={500} fontSize={20}>
Confirm Remove
</Text>
</ButtonPrimary>
</RowBetween>
) : (
<ButtonPrimary
style={{ margin: '20px 0' }}
onClick={() => {
contractCall()
}}
>
<Text fontWeight={500} fontSize={20}>
Confirm{' '}
{transactionType === TRANSACTION_TYPE.ADD
? 'Supply'
: transactionType === TRANSACTION_TYPE.REMOVE
? 'Remove'
: 'Swap'}
</Text>
</ButtonPrimary>
)}
{transactionType === TRANSACTION_TYPE.ADD && (
<Text fontSize={12} color="#565A69" textAlign="center">
{`Output is estimated. You will receive at least ${liquidityAmount?.toFixed(
6
)} UNI ${symbol0}/${symbol1} or the transaction will revert.`}
</Text>
)}
{transactionType === TRANSACTION_TYPE.REMOVE && (
<Text fontSize={12} color="#565A69" textAlign="center">
{`Output is estimated. You will receive at least ${amount0?.toSignificant(
6
)} ${symbol0} at least ${amount1?.toSignificant(6)} ${symbol1} or the transaction will revert.`}
</Text>
)}
{transactionType === TRANSACTION_TYPE.SWAP && (
<Text fontSize={12} color="#565A69" textAlign="center">
{`Output is estimated. You will receive at least ${amount1?.toSignificant(
6
)} ${symbol1} or the transaction will revert.`}
</Text>
)}
</AutoColumn>
</BottomSection>
<BottomSection gap="12px">{bottomContent()}</BottomSection>
</Wrapper>
) : (
<Wrapper>
......@@ -284,17 +76,8 @@ export default function ConfirmationModal({
{!pendingConfirmation ? 'Transaction Submitted' : 'Waiting For Confirmation'}
</Text>
<AutoColumn gap="12px" justify={'center'}>
<Text fontWeight={500} fontSize={16} color="#2172E5">
{transactionType === TRANSACTION_TYPE.ADD
? 'Supplied'
: transactionType === TRANSACTION_TYPE.REMOVE
? 'Removed'
: 'Swapped'}
</Text>
<Text fontWeight={600} fontSize={16} color="#2172E5">
{`${amount0?.toSignificant(6)} ${symbol0} ${
transactionType === TRANSACTION_TYPE.SWAP ? 'for' : 'and'
} ${amount1?.toSignificant(6)} ${symbol1}`}
{pendingText}
</Text>
</AutoColumn>
{!pendingConfirmation && (
......
......@@ -251,7 +251,12 @@ export default function CurrencyInputPanel({
</LabelRow>
)}
<InputRow>
<NumericalInput field={field} value={value} onUserInput={onUserInput} />
<NumericalInput
value={value}
onUserInput={val => {
onUserInput(field, val)
}}
/>
{!!token?.address && !atMax && <StyledBalanceMax onClick={onMax}>MAX</StyledBalanceMax>}
{renderUnlockButton()}
<CurrencySelect
......
......@@ -4,6 +4,21 @@ import { ethers } from 'ethers'
import { parseUnits, parseEther } from '@ethersproject/units'
import { WETH, TradeType, Route, Trade, TokenAmount, JSBI } from '@uniswap/sdk'
import TokenLogo from '../TokenLogo'
import QuestionHelper from '../Question'
import NumericalInput from '../NumericalInput'
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 { ArrowDown, ArrowUp } from 'react-feather'
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'
......@@ -11,17 +26,13 @@ import { useAddressBalance } from '../../contexts/Balances'
import { useTransactionAdder } from '../../contexts/Transactions'
import { useAddressAllowance } from '../../contexts/Allowances'
import ConfirmationModal from '../ConfirmationModal'
import CurrencyInputPanel from '../CurrencyInputPanel'
import { Text } from 'rebass'
import { RowBetween } from '../../components/Row'
import { ButtonPrimary } from '../Button'
import { ArrowDown, ArrowUp } from 'react-feather'
import { AutoColumn, ColumnCenter } from '../../components/Column'
import { TRANSACTION_TYPE, ROUTER_ADDRESSES } from '../../constants'
import { ROUTER_ADDRESSES } from '../../constants'
import { getRouterContract, calculateGasMargin } from '../../utils'
const Wrapper = styled.div`
position: relative;
`
const ArrowWrapper = styled.div`
padding: 4px;
border: 1px solid ${({ theme }) => theme.malibuBlue};
......@@ -36,8 +47,24 @@ const ArrowWrapper = styled.div`
}
`
const FixedBottom = styled.div`
position: absolute;
bottom: -200px;
width: 100%;
`
const ErrorText = styled(Text)`
color: ${({ theme, error }) => (error ? theme.salmonRed : theme.chaliceGray)};
color: ${({ theme, warningMedium, warningHigh }) =>
warningHigh ? theme.salmonRed : warningMedium ? theme.warningYellow : theme.textColor};
`
const InputWrapper = styled(RowBetween)`
width: 200px;
background-color: ${({ theme }) => theme.inputBackground};
border-radius: 8px;
padding: 4px 8px;
border: 1px solid transparent;
border: ${({ active, theme }) => active && '1px solid ' + theme.royalBlue};
`
enum Field {
......@@ -136,25 +163,74 @@ function reducer(
}
}
function hex(value: JSBI) {
return ethers.utils.bigNumberify(value.toString())
}
const SLIPPAGE_INDEX = {
1: 1,
2: 2,
3: 3,
4: 4
}
const SWAP_TYPE = {
EXACT_TOKENS_FOR_TOKENS: 'EXACT_TOKENS_FOR_TOKENS',
EXACT_TOKENS_FOR_ETH: 'EXACT_TOKENS_FOR_ETH',
EXACT_ETH_FOR_TOKENS: 'EXACT_ETH_FOR_TOKENS',
TOKENS_FOR_EXACT_TOKENS: 'TOKENS_FOR_EXACT_TOKENS',
TOKENS_FOR_EXACT_ETH: 'TOKENS_FOR_EXACT_ETH',
ETH_FOR_EXACT_TOKENS: 'ETH_FOR_EXACT_TOKENS'
}
const GAS_MARGIN = ethers.utils.bigNumberify(1000)
// default allowed slippage, in bips
const INITIAL_ALLOWED_SLIPPAGE = 200
// 15 minutes, denominated in seconds
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
export default function ExchangePage() {
const { chainId, account, library } = useWeb3React()
const routerAddress = ROUTER_ADDRESSES[chainId]
// adding notifications on txns
const [, addPopup] = usePopups()
// input details
const [state, dispatch] = useReducer(reducer, WETH[chainId].address, initializeSwapState)
const { independentField, typedValue, ...fieldData } = state
// get derived 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
// get basic SDK entities
const tokens = {
[Field.INPUT]: useToken(fieldData[Field.INPUT].address),
[Field.OUTPUT]: useToken(fieldData[Field.OUTPUT].address)
}
const exchange = useExchange(tokens[Field.INPUT], tokens[Field.OUTPUT])
const route = !!exchange ? new Route([exchange], tokens[Field.INPUT]) : undefined // no useRoute hook
const route = !!exchange ? new Route([exchange], tokens[Field.INPUT]) : undefined
// modal state
const addTransaction = useTransactionAdder()
const [showConfirm, setShowConfirm] = useState(true)
const [pendingConfirmation, setPendingConfirmation] = useState(true) // waiting for user confirmation
const [attemptingTxn, setAttemptingTxn] = useState(false) // clicked confirmed
// txn values
const [txHash, setTxHash] = useState()
const [deadline, setDeadline] = useState(DEFAULT_DEADLINE_FROM_NOW)
const [allowedSlippage, setAllowedSlippage] = useState(INITIAL_ALLOWED_SLIPPAGE)
// approvals
const inputApproval = useAddressAllowance(account, tokens[Field.INPUT], routerAddress)
const outputApproval = useAddressAllowance(account, tokens[Field.OUTPUT], routerAddress)
// get user- and token-specific lookup data
const userBalances = {
......@@ -164,14 +240,18 @@ export default function ExchangePage() {
const parsedAmounts: { [field: number]: TokenAmount } = {}
// try to parse typed value
if (typedValue !== '' && typedValue !== '.' && tokens[independentField]) {
// if (typedValue !== '' && typedValue !== '.' && tokens[independentField]) {
if (tokens[independentField]) {
try {
const typedValueParsed = parseUnits(typedValue, tokens[independentField].decimals).toString()
const typedValueParsed = parseUnits('0.0001', tokens[independentField].decimals).toString()
if (typedValueParsed !== '0')
parsedAmounts[independentField] = new TokenAmount(tokens[independentField], typedValueParsed)
} catch (error) {
// should only fail if the user specifies too many decimal places of precision (or maybe exceed max uint?)
console.error(error)
/**
* @todo reserve limit error here
*/
console.error('found error here ')
}
}
......@@ -182,9 +262,9 @@ export default function ExchangePage() {
!!route && !!parsedAmounts[independentField]
? new Trade(route, parsedAmounts[independentField], tradeType)
: undefined
} catch (error) {
console.error(error)
}
} catch (error) {}
const slippageFromTrade = trade && trade.slippage
if (trade)
parsedAmounts[dependentField] = tradeType === TradeType.EXACT_INPUT ? trade.outputAmount : trade.inputAmount
......@@ -250,14 +330,8 @@ export default function ExchangePage() {
: undefined
const maxAmountOutput =
!!userBalances[Field.OUTPUT] &&
JSBI.greaterThan(
userBalances[Field.OUTPUT].raw,
tokens[Field.OUTPUT].equals(WETH[chainId]) ? MIN_ETHER.raw : JSBI.BigInt(0)
)
? tokens[Field.OUTPUT].equals(WETH[chainId])
? userBalances[Field.OUTPUT].subtract(MIN_ETHER)
: userBalances[Field.OUTPUT]
!!userBalances[Field.OUTPUT] && JSBI.greaterThan(userBalances[Field.OUTPUT].raw, JSBI.BigInt(0))
? userBalances[Field.OUTPUT]
: undefined
const atMaxAmountOutput =
......@@ -265,23 +339,6 @@ export default function ExchangePage() {
? JSBI.equal(maxAmountOutput.raw, parsedAmounts[Field.OUTPUT].raw)
: undefined
const [showConfirm, setShowConfirm] = useState(false)
const [pendingConfirmation, toggelPendingConfirmation] = useState(true)
// state for txn
const addTransaction = useTransactionAdder()
const [txHash, setTxHash] = useState()
const SWAP_TYPE = {
EXACT_TOKENS_FOR_TOKENS: 'EXACT_TOKENS_FOR_TOKENS',
EXACT_TOKENS_FOR_ETH: 'EXACT_TOKENS_FOR_ETH',
EXACT_ETH_FOR_TOKENS: 'EXACT_ETH_FOR_TOKENS',
TOKENS_FOR_EXACT_TOKENS: 'TOKENS_FOR_EXACT_TOKENS',
TOKENS_FOR_EXACT_ETH: 'TOKENS_FOR_EXACT_ETH',
ETH_FOR_EXACT_TOKENS: 'ETH_FOR_EXACT_TOKENS'
}
function getSwapType() {
if (tradeType === TradeType.EXACT_INPUT) {
if (tokens[Field.INPUT] === WETH[chainId]) {
......@@ -302,51 +359,62 @@ export default function ExchangePage() {
}
}
const ALLOWED_SLIPPAGE = 100
function calculateSlippageAmount(value: TokenAmount): JSBI[] {
if (value && value.raw) {
const offset = JSBI.divide(JSBI.multiply(JSBI.BigInt(ALLOWED_SLIPPAGE), value.raw), JSBI.BigInt(10000))
const offset = JSBI.divide(JSBI.multiply(JSBI.BigInt(allowedSlippage), value.raw), JSBI.BigInt(10000))
return [JSBI.subtract(value.raw, offset), JSBI.add(value.raw, offset)]
}
return null
}
function hex(value: JSBI) {
return ethers.utils.bigNumberify(value.toString())
}
const slippageAdjustedAmountsRaw = {
const slippageAdjustedAmounts = {
[Field.INPUT]:
Field.INPUT === independentField
? parsedAmounts[Field.INPUT]?.raw
: calculateSlippageAmount(parsedAmounts[Field.INPUT])?.[1],
? parsedAmounts[Field.INPUT]
: calculateSlippageAmount(parsedAmounts[Field.INPUT])?.[0] &&
new TokenAmount(tokens[Field.INPUT], calculateSlippageAmount(parsedAmounts[Field.INPUT])?.[1]),
[Field.OUTPUT]:
Field.OUTPUT === independentField
? parsedAmounts[Field.OUTPUT]?.raw
: calculateSlippageAmount(parsedAmounts[Field.OUTPUT])?.[0]
? parsedAmounts[Field.OUTPUT]
: calculateSlippageAmount(parsedAmounts[Field.OUTPUT])?.[0] &&
new TokenAmount(tokens[Field.INPUT], calculateSlippageAmount(parsedAmounts[Field.OUTPUT])?.[0])
}
const inputApproval = useAddressAllowance(account, tokens[Field.INPUT], routerAddress)
const outputApproval = useAddressAllowance(account, tokens[Field.OUTPUT], routerAddress)
const showInputUnlock =
parsedAmounts[Field.INPUT] && inputApproval && JSBI.greaterThan(parsedAmounts[Field.INPUT].raw, inputApproval.raw)
const [showInputUnlock, setShowInputUnlock] = useState(false)
const showOutputUnlock =
parsedAmounts[Field.OUTPUT] &&
outputApproval &&
JSBI.greaterThan(parsedAmounts[Field.OUTPUT].raw, outputApproval.raw)
// monitor parsed amounts and update unlocked buttons
useEffect(() => {
if (
parsedAmounts[Field.INPUT] &&
inputApproval &&
JSBI.greaterThan(parsedAmounts[Field.INPUT].raw, inputApproval.raw)
) {
setShowInputUnlock(true)
} else {
setShowInputUnlock(false)
// 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)
function parseCustomInput(val) {
const acceptableValues = [/^$/, /^\d{1,2}$/, /^\d{0,2}\.\d{0,2}$/]
if (acceptableValues.some(a => a.test(val))) {
setCustomSlippage(val)
setAllowedSlippage(val * 100)
}
}
function parseCustomDeadline(val) {
const acceptableValues = [/^$/, /^\d+$/]
if (acceptableValues.some(re => re.test(val))) {
setCustomDeadline(val)
setDeadline(val * 60)
}
}
}, [inputApproval, outputApproval, parsedAmounts])
async function onSwap() {
const routerContract = getRouterContract(chainId, library, account)
setAttemptingTxn(true)
const path = Object.keys(route.path).map(key => {
return route.path[key].address
......@@ -354,7 +422,7 @@ export default function ExchangePage() {
let estimate: Function, method: Function, args, value
const deadline = 1739591241
const deadlineFromNow = Math.ceil(Date.now() / 1000) + deadline
const swapType = getSwapType()
switch (swapType) {
......@@ -362,11 +430,11 @@ export default function ExchangePage() {
estimate = routerContract.estimate.swapExactTokensForTokens
method = routerContract.swapExactTokensForTokens
args = [
slippageAdjustedAmountsRaw[Field.INPUT].toString(),
slippageAdjustedAmountsRaw[Field.OUTPUT].toString(),
slippageAdjustedAmounts[Field.INPUT].raw.toString(),
slippageAdjustedAmounts[Field.OUTPUT].raw.toString(),
path,
account,
deadline
deadlineFromNow
]
value = ethers.constants.Zero
break
......@@ -374,29 +442,29 @@ export default function ExchangePage() {
estimate = routerContract.estimate.swapTokensForExactTokens
method = routerContract.swapTokensForExactTokens
args = [
slippageAdjustedAmountsRaw[Field.OUTPUT].toString(),
slippageAdjustedAmountsRaw[Field.INPUT].toString(),
slippageAdjustedAmounts[Field.OUTPUT].raw.toString(),
slippageAdjustedAmounts[Field.INPUT].raw.toString(),
path,
account,
deadline
deadlineFromNow
]
value = ethers.constants.Zero
break
case SWAP_TYPE.EXACT_ETH_FOR_TOKENS:
estimate = routerContract.estimate.swapExactETHForTokens
method = routerContract.swapExactETHForTokens
args = [slippageAdjustedAmountsRaw[Field.OUTPUT].toString(), path, account, deadline]
value = hex(slippageAdjustedAmountsRaw[Field.INPUT])
args = [slippageAdjustedAmounts[Field.OUTPUT].raw.toString(), path, account, deadlineFromNow]
value = hex(slippageAdjustedAmounts[Field.INPUT].raw)
break
case SWAP_TYPE.TOKENS_FOR_EXACT_ETH:
estimate = routerContract.estimate.swapTokensForExactETH
method = routerContract.swapTokensForExactETH
args = [
slippageAdjustedAmountsRaw[Field.OUTPUT].toString(),
slippageAdjustedAmountsRaw[Field.INPUT].toString(),
slippageAdjustedAmounts[Field.OUTPUT].raw.toString(),
slippageAdjustedAmounts[Field.INPUT].raw.toString(),
path,
account,
deadline
deadlineFromNow
]
value = ethers.constants.Zero
break
......@@ -404,24 +472,22 @@ export default function ExchangePage() {
estimate = routerContract.estimate.swapExactTokensForETH
method = routerContract.swapExactTokensForETH
args = [
slippageAdjustedAmountsRaw[Field.INPUT].toString(),
slippageAdjustedAmountsRaw[Field.OUTPUT].toString(),
slippageAdjustedAmounts[Field.INPUT].raw.toString(),
slippageAdjustedAmounts[Field.OUTPUT].raw.toString(),
path,
account,
deadline
deadlineFromNow
]
value = ethers.constants.Zero
break
case SWAP_TYPE.ETH_FOR_EXACT_TOKENS:
estimate = routerContract.estimate.swapETHForExactTokens
method = routerContract.swapETHForExactTokens
args = [slippageAdjustedAmountsRaw[Field.OUTPUT].toString(), path, account, deadline]
value = hex(slippageAdjustedAmountsRaw[Field.INPUT])
args = [slippageAdjustedAmounts[Field.OUTPUT].raw.toString(), path, account, deadlineFromNow]
value = hex(slippageAdjustedAmounts[Field.INPUT].raw)
break
}
const GAS_MARGIN = ethers.utils.bigNumberify(1000)
const estimatedGasLimit = await estimate(...args, { value }).catch(e => {
console.log('error getting gas limit')
})
......@@ -433,71 +499,267 @@ export default function ExchangePage() {
.then(response => {
setTxHash(response)
addTransaction(response)
toggelPendingConfirmation(false)
setPendingConfirmation(false)
})
.catch(e => {
console.log('error when trying transaction')
console.log(e)
addPopup(
<AutoColumn gap="10px">
<Text>Transaction Failed: try again.</Text>
</AutoColumn>
)
resetModal()
setShowConfirm(false)
})
}
// errors
const [inputError, setInputError] = useState()
const [outputError, setOutputError] = useState()
const [errorText, setErrorText] = useState(' ')
const [isError, setIsError] = useState(false)
const [inputError, setInputError] = useState('')
const [outputError, setOutputError] = useState('')
const [isValid, setIsValid] = useState(false)
// update errors live
useEffect(() => {
// reset errors
setInputError(null)
setOutputError(null)
setIsError(false)
setTradeError(null)
setIsValid(true)
if (
parsedAmounts[Field.INPUT] &&
exchange &&
JSBI.greaterThan(parsedAmounts[Field.INPUT].raw, exchange.reserveOf(tokens[Field.INPUT]).raw)
) {
setTradeError('Low Liquidity Error')
setIsValid(false)
}
if (
parsedAmounts[Field.OUTPUT] &&
exchange &&
JSBI.greaterThan(parsedAmounts[Field.OUTPUT].raw, exchange.reserveOf(tokens[Field.OUTPUT]).raw)
) {
setTradeError('Low Liquidity Error')
setIsValid(false)
}
if (showInputUnlock) {
setInputError('Need to approve amount on input.')
setInputError('Approval Needed')
setIsValid(false)
}
if (showOutputUnlock) {
setOutputError('Approval Needed')
setIsValid(false)
}
if (
userBalances[Field.INPUT] &&
parsedAmounts[Field.INPUT] &&
JSBI.lessThan(userBalances[Field.INPUT].raw, parsedAmounts[Field.INPUT]?.raw)
) {
setInputError('Insufficient balance.')
setIsError(true)
setIsValid(false)
}
}, [parsedAmounts, showInputUnlock, userBalances])
// set error text based on all errors
useEffect(() => {
setErrorText(null)
if (!parsedAmounts[Field.INPUT]) {
setErrorText('Enter an amount to continue')
} else if (outputError) {
setErrorText(outputError)
} else if (inputError) {
setErrorText(inputError)
return
if (
userBalances[Field.OUTPUT] &&
parsedAmounts[Field.OUTPUT] &&
JSBI.lessThan(userBalances[Field.OUTPUT].raw, parsedAmounts[Field.OUTPUT]?.raw)
) {
setOutputError('Insufficient balance.')
setIsValid(false)
}
}, [inputError, outputError, parsedAmounts])
}, [exchange, parsedAmounts, showInputUnlock, showOutputUnlock, tokens, userBalances])
// error state for button
const isValid = !errorText
const warningMedium = slippageFromTrade && parseFloat(slippageFromTrade.toFixed(4)) > ALLOWED_IMPACT_MEDIUM / 100
const warningHigh = slippageFromTrade && parseFloat(slippageFromTrade.toFixed(4)) > ALLOWED_IMPACT_HIGH / 100
function resetModal() {
setPendingConfirmation(true)
setAttemptingTxn(false)
setShowAdvanced(false)
}
function modalHeader() {
return (
<AutoColumn gap={'20px'} style={{ marginTop: '40px' }}>
<RowBetween align="flex-end">
<Text fontSize={36} fontWeight={500}>
{!!slippageAdjustedAmounts[Field.INPUT] && slippageAdjustedAmounts[Field.INPUT].toSignificant(6)}
</Text>
<RowFixed gap="10px">
<TokenLogo address={tokens[Field.INPUT]?.address} size={'24px'} />
<Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}>
{tokens[Field.INPUT]?.symbol || ''}
</Text>
</RowFixed>
</RowBetween>
<RowFixed>
<ArrowDown size="16" color="#888D9B" />
</RowFixed>
<RowBetween align="flex-end">
<Text fontSize={36} fontWeight={500} color={warningHigh ? '#FF6871' : '#2172E5'}>
{!!slippageAdjustedAmounts[Field.OUTPUT] && slippageAdjustedAmounts[Field.OUTPUT].toSignificant(6)}
</Text>
<RowFixed gap="10px">
<TokenLogo address={tokens[Field.OUTPUT]?.address} size={'24px'} />
<Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}>
{tokens[Field.OUTPUT]?.symbol || ''}
</Text>
</RowFixed>
</RowBetween>
</AutoColumn>
)
}
function modalBottom() {
return showAdvanced ? (
<AutoColumn gap="20px">
<Link
onClick={() => {
setShowAdvanced(false)
}}
>
back
</Link>
<RowBetween>
<TYPE.main>Limit additional price slippage</TYPE.main>
<QuestionHelper text="" />
</RowBetween>
<Row>
<ButtonRadio
active={SLIPPAGE_INDEX[1] === activeIndex}
padding="4px 6px"
borderRadius="8px"
style={{ marginRight: '16px' }}
width={'60px'}
onClick={() => {
setActiveIndex(SLIPPAGE_INDEX[1])
setAllowedSlippage(10)
}}
>
0.1%
</ButtonRadio>
<ButtonRadio
active={SLIPPAGE_INDEX[2] === activeIndex}
padding="4px 6px"
borderRadius="8px"
style={{ marginRight: '16px' }}
width={'60px'}
onClick={() => {
setActiveIndex(SLIPPAGE_INDEX[2])
setAllowedSlippage(100)
}}
>
1%
</ButtonRadio>
<ButtonRadio
active={SLIPPAGE_INDEX[3] === activeIndex}
padding="4px"
borderRadius="8px"
width={'140px'}
onClick={() => {
setActiveIndex(SLIPPAGE_INDEX[3])
setAllowedSlippage(200)
}}
>
2% (suggested)
</ButtonRadio>
</Row>
<RowFixed>
<InputWrapper active={SLIPPAGE_INDEX[4] === activeIndex}>
<NumericalInput
align={customSlippage ? 'right' : 'left'}
value={customSlippage || ''}
onUserInput={val => {
parseCustomInput(val)
setActiveIndex(SLIPPAGE_INDEX[4])
}}
placeHolder="Custom"
onClick={() => {
setActiveIndex(SLIPPAGE_INDEX[4])
if (customSlippage) {
parseCustomInput(customSlippage)
}
}}
/>
%
</InputWrapper>
</RowFixed>
<RowBetween>
<TYPE.main>Adjust deadline (minutes from now)</TYPE.main>
</RowBetween>
<RowFixed>
<NumericalInput
value={customDeadline}
onUserInput={val => {
parseCustomDeadline(val)
}}
/>
</RowFixed>
</AutoColumn>
) : (
<>
<RowBetween>
<Text color="#565A69" fontWeight={500} fontSize={16}>
Price
</Text>
<Text fontWeight={500} fontSize={16}>
{`1 ${tokens[Field.INPUT]?.symbol} = ${route && route.midPrice && route.midPrice.adjusted.toFixed(8)} ${
tokens[Field.OUTPUT]?.symbol
}`}
</Text>
</RowBetween>
<RowBetween>
<Text color="#565A69" fontWeight={500} fontSize={16}>
Slippage <Link onClick={() => setShowAdvanced(true)}>(edit limits)</Link>
</Text>
<ErrorText warningHigh={warningHigh} fontWeight={500}>
{slippageFromTrade && slippageFromTrade.toFixed(4)}%
</ErrorText>
</RowBetween>
<ButtonError onClick={onSwap} error={!!warningHigh} style={{ margin: '10px 0' }}>
<Text fontSize={20} fontWeight={500}>
{warningHigh ? 'Swap Anyway' : 'Swap'}
</Text>
</ButtonError>
<Text fontSize={12} color="#565A69" textAlign="center">
{`Output is estimated. You will receive at least ${slippageAdjustedAmounts[Field.OUTPUT]?.toSignificant(6)} ${
tokens[Field.OUTPUT]?.symbol
} or the transaction will revert.`}
</Text>
<AutoColumn justify="center">
<Link
onClick={() => {
setShowAdvanced(true)
}}
>
Advanced Options
</Link>
</AutoColumn>
</>
)
}
const pendingText = ` Swapped ${parsedAmounts[Field.INPUT]?.toSignificant(6)} ${
tokens[Field.INPUT]?.symbol
} for ${parsedAmounts[Field.OUTPUT]?.toSignificant(6)} ${tokens[Field.OUTPUT]?.symbol}`
return (
<Wrapper>
<ConfirmationModal
isOpen={showConfirm}
onDismiss={() => {
setTxHash(null)
resetModal()
setShowConfirm(false)
}}
amount0={parsedAmounts[Field.INPUT]}
amount1={parsedAmounts[Field.OUTPUT]}
price={route?.midPrice}
transactionType={TRANSACTION_TYPE.SWAP}
contractCall={onSwap}
attemptingTxn={attemptingTxn}
pendingConfirmation={pendingConfirmation}
hash={txHash ? txHash.hash : ''}
hash={txHash ? txHash : ''}
topContent={() => modalHeader()}
bottomContent={modalBottom}
pendingText={pendingText}
title="Confirm Swap"
/>
<AutoColumn gap={'20px'}>
<CurrencyInputPanel
......@@ -509,7 +771,7 @@ export default function ExchangePage() {
}}
atMax={atMaxAmountInput}
token={tokens[Field.INPUT]}
onTokenSelection={onTokenSelection}
onTokenSelection={address => onTokenSelection(Field.INPUT, address)}
title={'Input'}
error={inputError}
exchange={exchange}
......@@ -530,36 +792,69 @@ export default function ExchangePage() {
}}
atMax={atMaxAmountOutput}
token={tokens[Field.OUTPUT]}
onTokenSelection={onTokenSelection}
onTokenSelection={address => onTokenSelection(Field.OUTPUT, address)}
title={'Output'}
error={outputError}
exchange={exchange}
disableUnlock
showUnlock={showOutputUnlock}
/>
<RowBetween>
Rate:
<div>
<Text fontWeight={500} color="#565A69">
Price
</Text>
<Text fontWeight={500} color="#565A69">
{exchange
? `1 ${tokens[Field.INPUT].symbol} = ${route?.midPrice.toSignificant(6)} ${tokens[Field.OUTPUT].symbol}`
: '-'}
</div>
</Text>
</RowBetween>
<ColumnCenter style={{ height: '20px' }}>
<ErrorText fontSize={12} error={isError}>
{errorText && errorText}
{warningMedium && (
<RowBetween>
<Text fontWeight={500} color="#565A69">
Slippage
</Text>
<ErrorText fontWeight={500} warningMedium={warningMedium} warningHigh={warningHigh}>
{slippageFromTrade.toFixed(4)}%
</ErrorText>
</ColumnCenter>
<ButtonPrimary
</RowBetween>
)}
<ButtonError
onClick={() => {
setShowConfirm(true)
}}
disabled={!isValid}
error={!!warningHigh}
>
<Text fontSize={20} fontWeight={500}>
Swap
{inputError
? inputError
: outputError
? outputError
: tradeError
? tradeError
: warningHigh
? 'Swap Anyway'
: 'Swap'}
</Text>
</ButtonPrimary>
</ButtonError>
</AutoColumn>
</>
{warningHigh && (
<FixedBottom>
<GreyCard>
<AutoColumn gap="12px">
<RowBetween>
<Text fontWeight={500}>Slippage Warning</Text>
<QuestionHelper text="" />
</RowBetween>
<Text color="#565A69" lineHeight="145.23%;">
This trade will move the price by {slippageFromTrade.toFixed(2)}%. This pool probably doesn’t have
enough liquidity. Are you sure you want to continue this trade?
</Text>
</AutoColumn>
</GreyCard>
</FixedBottom>
)}
</Wrapper>
)
}
......@@ -17,6 +17,7 @@ import { isMobile } from 'react-device-detect'
import { useWeb3React } from '../../hooks'
import { useAddressBalance } from '../../contexts/Balances'
import { useWalletModalToggle, usePopups } from '../../contexts/Application'
import { AutoColumn } from '../Column'
const HeaderFrame = styled.div`
display: flex;
......@@ -68,7 +69,7 @@ const AccountElement = styled.div`
/* width: 100%; */
`
const FixedPopupColumn = styled.div`
const FixedPopupColumn = styled(AutoColumn)`
position: absolute;
top: 80px;
right: 20px
......@@ -84,6 +85,15 @@ const StyledClose = styled(X)`
}
`
const Popup = styled(Card)`
box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.04), 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 16px 24px rgba(0, 0, 0, 0.04),
0px 24px 32px rgba(0, 0, 0, 0.04);
z-index: 9999;
border-radius: 8px;
padding: 1rem;
background: ${theme => theme.white};
`
export default function Header() {
const { account, chainId } = useWeb3React()
......@@ -117,13 +127,13 @@ export default function Header() {
</AccountElement>
<Menu />
</HeaderElement>
<FixedPopupColumn>
<FixedPopupColumn gap="20px">
{activePopups.map(item => {
return (
<Card bg="white" padding={'16px'} key={item.key} borderRadius={'8px'}>
<Popup key={item.key}>
<StyledClose color="#888D9B" onClick={() => removePopup(item.key)} />
{item.content}
</Card>
</Popup>
)
})}
</FixedPopupColumn>
......
......@@ -6,7 +6,6 @@ import { ReactComponent as MenuIcon } from '../../assets/images/menu.svg'
import { Link } from '../../theme'
import { darken, transparentize } from 'polished'
import { useAdvancedManager } from '../../contexts/LocalStorage'
import Toggle from 'react-switch'
......@@ -140,7 +139,7 @@ const EmojiToggle = styled.span`
export default function Menu() {
const [isDark, toggleDarkMode] = useDarkModeManager()
const [isAdvanced, toggleAdvanced] = useState()
const [isAdvanced, toggleAdvanced] = useState(false)
const node = useRef()
const [open, toggle] = useToggle(false)
......
......@@ -11,6 +11,8 @@ const StyledInput = styled.input`
flex: 1 1 auto;
width: 0;
background-color: ${({ theme }) => theme.inputBackground};
font-size: ${({ fontSize }) => fontSize && fontSize};
text-align: ${({ align }) => align && align};
[type='number'] {
-moz-appearance: textfield;
......@@ -32,10 +34,10 @@ function escapeRegExp(string: string): string {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string
}
export const Input = React.memo(({ field, value, onUserInput, ...rest }: any) => {
export const Input = React.memo(({ value, onUserInput, placeHolder = null, ...rest }: any) => {
function enforcer(nextUserInput: string) {
if (nextUserInput === '' || inputRegex.test(escapeRegExp(nextUserInput))) {
onUserInput(field, nextUserInput)
onUserInput(nextUserInput)
}
}
......@@ -53,7 +55,7 @@ export const Input = React.memo(({ field, value, onUserInput, ...rest }: any) =>
autoCorrect="off"
// text-specific options
type="text"
placeholder="0.0"
placeholder={placeHolder || '0.0'}
minLength={1}
maxLength={79}
spellCheck="false"
......
......@@ -16,7 +16,7 @@ import { Link } from '../../theme'
import { Text } from 'rebass'
import { Plus } from 'react-feather'
import { LightCard } from '../Card'
import { AutoColumn, ColumnCenter } from '../Column'
import Column, { AutoColumn, ColumnCenter } from '../Column'
import { ButtonPrimary, ButtonDropwdown, ButtonDropwdownLight } from '../Button'
import DoubleTokenLogo from '../DoubleLogo'
......@@ -112,6 +112,13 @@ function PoolFinder({ history }) {
</Row>
</ButtonDropwdownLight>
)}
{allowImport && (
<ColumnCenter justify="center" style={{ backgroundColor: '#EBF4FF', padding: '8px', borderRadius: '12px' }}>
<Text textAlign="center" fontWeight={500} color="#2172E5">
Liquidity Found!
</Text>
</ColumnCenter>
)}
{position ? (
!JSBI.equal(position.raw, JSBI.BigInt(0)) ? (
<PositionCard
......@@ -155,11 +162,7 @@ function PoolFinder({ history }) {
</Text>
</LightCard>
)}
{allowImport && (
<Text textAlign="center" fontWeight={500}>
Liquidity Found!
</Text>
)}
<ButtonPrimary disabled={!allowImport} onClick={endSearch}>
<Text fontWeight={500} fontSize={20}>
Import
......
......@@ -61,11 +61,6 @@ function PositionCard({ exchangeAddress, token0, token1, history, minimal = fals
return (
<DynamicCard {...rest}>
<AutoColumn gap="20px">
<FixedHeightRow>
<Text fontWeight={500} fontSize={16}>
Current Position
</Text>
</FixedHeightRow>
<FixedHeightRow>
<RowFixed>
<DoubleLogo a0={token0?.address || ''} a1={token1?.address || ''} margin={true} size={24} />
......
......@@ -4,6 +4,7 @@ const Row = styled.div`
width: 100%;
display: flex;
align-items: center;
align-items: ${({ align }) => align && align};
`
export const RowBetween = styled(Row)`
......
......@@ -411,6 +411,7 @@ function SearchModal({ history, isOpen, onDismiss, onTokenSelect, urlAddedTokens
/>
<RowBetween>
<div>
{filterType !== 'tokens' && (
<Text>
Don't see a pool?{' '}
<StyledLink
......@@ -421,6 +422,7 @@ function SearchModal({ history, isOpen, onDismiss, onTokenSelect, urlAddedTokens
Import it.
</StyledLink>
</Text>
)}
</div>
<div />
<Filter title="Your Balances" filter={FILTERS.BALANCES} />
......
......@@ -19,13 +19,6 @@ export const SUPPORTED_THEMES = {
LIGHT: 'LIGHT'
}
export enum TRANSACTION_TYPE {
SWAP,
SEND,
ADD,
REMOVE
}
const MAINNET_WALLETS = {
INJECTED: {
connector: injected,
......
......@@ -9,14 +9,14 @@ import { ChainId, WETH, Token, TokenAmount, Exchange, JSBI } from '@uniswap/sdk'
const UPDATE = 'UPDATE'
const ALL_EXCHANGES: [Token, Token][] = [
// [
// INITIAL_TOKENS_CONTEXT[ChainId.RINKEBY][WETH[ChainId.RINKEBY].address],
// INITIAL_TOKENS_CONTEXT[ChainId.RINKEBY]['0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735']
// ]
// [
// INITIAL_TOKENS_CONTEXT[ChainId.RINKEBY]['0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735'],
// INITIAL_TOKENS_CONTEXT[ChainId.RINKEBY]['0x8ab15C890E5C03B5F240f2D146e3DF54bEf3Df44']
// ]
[
INITIAL_TOKENS_CONTEXT[ChainId.RINKEBY][WETH[ChainId.RINKEBY].address],
INITIAL_TOKENS_CONTEXT[ChainId.RINKEBY]['0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735']
],
[
INITIAL_TOKENS_CONTEXT[ChainId.RINKEBY]['0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735'],
INITIAL_TOKENS_CONTEXT[ChainId.RINKEBY]['0x8ab15C890E5C03B5F240f2D146e3DF54bEf3Df44']
]
]
const EXCHANGE_MAP: {
......
......@@ -27,11 +27,6 @@ const HeaderWrapper = styled.div`
width: 100%;
justify-content: space-between;
`
const FooterWrapper = styled.div`
width: 100%;
min-height: 30px;
align-self: flex-end;
`
const BodyWrapper = styled.div`
display: flex;
......@@ -50,7 +45,7 @@ const Body = styled.div`
box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.01), 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 16px 24px rgba(0, 0, 0, 0.04),
0px 24px 32px rgba(0, 0, 0, 0.01);
border-radius: 20px;
padding: 2rem 2rem;
padding: 2rem 2rem 1rem 2rem;
`
export default function App() {
......
......@@ -5,26 +5,27 @@ import { parseUnits, parseEther } from '@ethersproject/units'
import { WETH, TokenAmount, JSBI, Percent, Route } from '@uniswap/sdk'
import DoubleLogo from '../../components/DoubleLogo'
import TokenLogo from '../../components/TokenLogo'
import SearchModal from '../../components/SearchModal'
import PositionCard from '../../components/PositionCard'
import ConfirmationModal from '../../components/ConfirmationModal'
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
import { Text } from 'rebass'
import { Plus } from 'react-feather'
import { RowBetween } from '../../components/Row'
import { ChevronDown } from 'react-feather'
import { ButtonPrimary } from '../../components/Button'
import { AutoColumn, ColumnCenter } from '../../components/Column'
import { ButtonPrimary, ButtonEmpty } from '../../components/Button'
import Row, { RowBetween, RowFlat, RowFixed } from '../../components/Row'
import { useToken } from '../../contexts/Tokens'
import { useWeb3React } from '../../hooks'
import { usePopups } from '../../contexts/Application'
import { useAddressBalance } from '../../contexts/Balances'
import { useAddressAllowance } from '../../contexts/Allowances'
import { useTransactionAdder } from '../../contexts/Transactions'
import { useExchange, useTotalSupply } from '../../contexts/Exchanges'
import { BigNumber } from 'ethers/utils'
import { TRANSACTION_TYPE, ROUTER_ADDRESSES } from '../../constants'
import { ROUTER_ADDRESSES } from '../../constants'
import { getRouterContract, calculateGasMargin } from '../../utils'
// denominated in bips
......@@ -41,7 +42,7 @@ const Wrapper = styled.div`
const FixedBottom = styled.div`
position: absolute;
bottom: -240px;
bottom: -200px;
width: 100%;
`
......@@ -145,6 +146,7 @@ export default function AddLiquidity({ token0, token1 }) {
// modal states
const [showSearch, setShowSearch] = useState<boolean>(false)
const [showConfirm, setShowConfirm] = useState<boolean>(false)
const [attemptingTxn, setAttemptingTxn] = useState<boolean>(false) // clicke confirm
const [pendingConfirmation, setPendingConfirmation] = useState<boolean>(true)
// input state
......@@ -369,7 +371,10 @@ export default function AddLiquidity({ token0, token1 }) {
return null
}
const [, addPopup] = usePopups()
async function onAdd() {
setAttemptingTxn(true)
const router = getRouterContract(chainId, library, account)
const minTokenInput = calculateSlippageAmount(parsedAmounts[Field.INPUT])[0]
......@@ -428,26 +433,112 @@ export default function AddLiquidity({ token0, token1 }) {
})
.catch((e: Error) => {
console.log(e)
addPopup(
<AutoColumn gap="10px">
<Text>Transaction Failed: try again.</Text>
</AutoColumn>
)
setPendingConfirmation(true)
setAttemptingTxn(false)
setShowConfirm(false)
})
}
const modalHeader = () => {
return (
<AutoColumn gap="20px">
<RowFlat style={{ marginTop: '60px' }}>
<Text fontSize="48px" fontWeight={500} lineHeight="32px" marginRight={10}>
{liquidityMinted?.toFixed(6)}
</Text>
<DoubleLogo a0={tokens[Field.INPUT]?.symbol || ''} a1={tokens[Field.OUTPUT]?.symbol || ''} size={30} />
</RowFlat>
<Row>
<Text fontSize="24px">
{tokens[Field.INPUT]?.symbol + ':' + tokens[Field.OUTPUT]?.symbol + ' Pool Tokens'}
</Text>
</Row>
</AutoColumn>
)
}
const modalBottom = () => {
return (
<>
<RowBetween>
<Text color="#565A69" fontWeight={500} fontSize={16}>
{tokens[Field.INPUT]?.symbol} Deposited
</Text>
<RowFixed>
<TokenLogo address={tokens[Field.INPUT]?.address || ''} style={{ marginRight: '8px' }} />
<Text fontWeight={500} fontSize={16}>
{!!parsedAmounts[Field.INPUT] && parsedAmounts[Field.INPUT].toSignificant(6)}
</Text>
</RowFixed>
</RowBetween>
<RowBetween>
<Text color="#565A69" fontWeight={500} fontSize={16}>
{tokens[Field.OUTPUT]?.symbol} Deposited
</Text>
<RowFixed>
<TokenLogo address={tokens[Field.OUTPUT]?.address || ''} style={{ marginRight: '8px' }} />
<Text fontWeight={500} fontSize={16}>
{!!parsedAmounts[Field.OUTPUT] && parsedAmounts[Field.OUTPUT].toSignificant(6)}
</Text>
</RowFixed>
</RowBetween>
<RowBetween>
<Text color="#565A69" fontWeight={500} fontSize={16}>
Rate
</Text>
<Text fontWeight={500} fontSize={16}>
{`1 ${tokens[Field.INPUT]?.symbol} = ${route?.midPrice &&
route?.midPrice?.raw?.denominator &&
route.midPrice.adjusted.toFixed(8)} ${tokens[Field.OUTPUT]?.symbol}`}
</Text>
</RowBetween>
<RowBetween>
<Text color="#565A69" fontWeight={500} fontSize={16}>
Minted Pool Share:
</Text>
<Text fontWeight={500} fontSize={16}>
{poolTokenPercentage?.toFixed(6) + '%'}
</Text>
</RowBetween>
<ButtonPrimary style={{ margin: '20px 0' }} onClick={onAdd}>
<Text fontWeight={500} fontSize={20}>
Confirm Supply
</Text>
</ButtonPrimary>
<Text fontSize={12} color="#565A69" textAlign="center">
{`Output is estimated. You will receive at least ${liquidityMinted?.toFixed(6)} UNI ${
tokens[Field.INPUT]?.symbol
}/${tokens[Field.OUTPUT]?.symbol} or the transaction will revert.`}
</Text>
</>
)
}
const pendingText = `Supplied ${parsedAmounts[Field.INPUT]?.toSignificant(6)} ${
tokens[Field.INPUT]?.symbol
} ${'and'} ${parsedAmounts[Field.OUTPUT]?.toSignificant(6)} ${tokens[Field.OUTPUT]?.symbol}`
return (
<Wrapper>
<ConfirmationModal
isOpen={showConfirm}
onDismiss={() => {
setPendingConfirmation(true)
setAttemptingTxn(false)
setShowConfirm(false)
}}
liquidityAmount={liquidityMinted}
amount0={parsedAmounts[Field.INPUT]}
amount1={parsedAmounts[Field.OUTPUT]}
poolTokenPercentage={poolTokenPercentage}
price={route?.midPrice && route?.midPrice?.raw?.denominator}
transactionType={TRANSACTION_TYPE.ADD}
contractCall={onAdd}
attemptingTxn={attemptingTxn}
pendingConfirmation={pendingConfirmation}
hash={txHash ? txHash : ''}
topContent={() => modalHeader()}
bottomContent={modalBottom}
pendingText={pendingText}
title="You will receive"
/>
<SearchModal
isOpen={showSearch}
......@@ -456,22 +547,6 @@ export default function AddLiquidity({ token0, token1 }) {
}}
/>
<AutoColumn gap="20px">
<ButtonEmpty
padding={'1rem'}
onClick={() => {
setShowSearch(true)
}}
>
<RowBetween>
<DoubleLogo a0={exchange?.token0?.address || ''} a1={exchange?.token1?.address || ''} size={24} />
<Text fontSize={20}>
{exchange?.token0 && exchange?.token1
? exchange.token0.symbol + ' / ' + exchange.token1.symbol + ' Pool'
: ''}
</Text>
<ChevronDown size={24} />
</RowBetween>
</ButtonEmpty>
{noLiquidity && (
<ColumnCenter>
<Text fontWeight={500} style={{ textAlign: 'center' }}>
......
......@@ -6,15 +6,17 @@ import { TokenAmount, JSBI, Route, WETH, Percent } from '@uniswap/sdk'
import Slider from '../../components/Slider'
import TokenLogo from '../../components/TokenLogo'
import DoubleLogo from '../../components/DoubleLogo'
import PositionCard from '../../components/PositionCard'
import ConfirmationModal from '../../components/ConfirmationModal'
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
import { Text } from 'rebass'
import { LightCard } from '../../components/Card'
import { ButtonPrimary } from '../../components/Button'
import { ButtonConfirmed } from '../../components/Button'
import { ArrowDown, Plus } from 'react-feather'
import { RowBetween, RowFixed } from '../../components/Row'
import { AutoColumn, ColumnCenter } from '../../components/Column'
import Row, { RowBetween, RowFixed } from '../../components/Row'
import { useToken } from '../../contexts/Tokens'
import { useWeb3React } from '../../hooks'
......@@ -25,7 +27,6 @@ import { useExchange, useTotalSupply } from '../../contexts/Exchanges'
import { BigNumber } from 'ethers/utils'
import { splitSignature } from '@ethersproject/bytes'
import { TRANSACTION_TYPE } from '../../constants'
import { ROUTER_ADDRESSES } from '../../constants'
import { getRouterContract, calculateGasMargin } from '../../utils'
......@@ -138,6 +139,10 @@ function reducer(
}
}
const ConfirmedText = styled(Text)`
color: ${({ theme, confirmed }) => (confirmed ? theme.connectedGreen : theme.white)};
`
export default function RemoveLiquidity({ token0, token1 }) {
const { account, chainId, library } = useWeb3React()
const routerAddress = ROUTER_ADDRESSES[chainId]
......@@ -302,19 +307,19 @@ export default function RemoveLiquidity({ token0, token1 }) {
: false
// errors
const [generalError, setGeneralError] = useState()
const [inputError, setInputError] = useState()
const [outputError, setOutputError] = useState()
const [poolTokenError, setPoolTokenError] = useState()
const [generalError, setGeneralError] = useState('')
const [inputError, setInputError] = useState('')
const [outputError, setOutputError] = useState('')
const [poolTokenError, setPoolTokenError] = useState('')
const [isValid, setIsValid] = useState(false)
// update errors live
useEffect(() => {
// reset errors
setGeneralError(null)
setInputError(null)
setOutputError(null)
setPoolTokenError(null)
setGeneralError('')
setInputError('')
setOutputError('')
setPoolTokenError('')
setIsValid(true)
if (formattedAmounts[Field.TOKEN0] === '') {
......@@ -480,28 +485,103 @@ export default function RemoveLiquidity({ token0, token1 }) {
setPendingConfirmation(true)
}
function modalHeader() {
return (
<AutoColumn gap="16px">
<Row style={{ marginTop: '40px' }}>
<TokenLogo address={tokens[Field.TOKEN0]?.symbol} size={'30px'} />
<Text fontSize="24px" marginLeft={10}>
{tokens[Field.TOKEN0]?.symbol}{' '}
{!!parsedAmounts[Field.TOKEN0] && parsedAmounts[Field.TOKEN0].toSignificant(8)}
</Text>
</Row>
<Row>
<TokenLogo address={tokens[Field.TOKEN1]?.symbol} size={'30px'} />
<Text fontSize="24px" marginLeft={10}>
{tokens[Field.TOKEN1]?.symbol}{' '}
{!!parsedAmounts[Field.TOKEN1] && parsedAmounts[Field.TOKEN1].toSignificant(8)}
</Text>
</Row>
</AutoColumn>
)
}
function modalBottom() {
return (
<>
<RowBetween>
<Text color="#565A69" fontWeight={500} fontSize={16}>
{'UNI ' + tokens[Field.TOKEN0]?.symbol + ':' + tokens[Field.TOKEN1]?.symbol} Burned
</Text>
<RowFixed>
<DoubleLogo
a0={tokens[Field.TOKEN0]?.address || ''}
a1={tokens[Field.TOKEN1]?.address || ''}
margin={true}
/>
<Text fontWeight={500} fontSize={16}>
{parsedAmounts[Field.LIQUIDITY]?.toSignificant(6)}
</Text>
</RowFixed>
</RowBetween>
<RowBetween>
<Text color="#565A69" fontWeight={500} fontSize={16}>
Rate
</Text>
<Text fontWeight={500} fontSize={16}>
{`1 ${tokens[Field.TOKEN0]?.symbol} = ${route?.midPrice && route.midPrice.adjusted.toFixed(8)} ${
tokens[Field.TOKEN1]?.symbol
}`}
</Text>
</RowBetween>
<RowBetween gap="20px">
<ButtonConfirmed
style={{ margin: '20px 0' }}
width="48%"
onClick={onSign}
confirmed={signed}
disabled={signed}
>
<ConfirmedText fontWeight={500} fontSize={20} confirmed={signed}>
{signed ? 'Signed' : 'Sign'}
</ConfirmedText>
</ButtonConfirmed>
<ButtonPrimary width="48%" disabled={!signed} style={{ margin: '20px 0' }} onClick={onRemove}>
<Text fontWeight={500} fontSize={20}>
Confirm Remove
</Text>
</ButtonPrimary>
</RowBetween>
<Text fontSize={12} color="#565A69" textAlign="center">
{`Output is estimated. You will receive at least ${parsedAmounts[Field.TOKEN0]?.toFixed(6)} ${
tokens[Field.TOKEN0]?.symbol
} and at least ${parsedAmounts[Field.TOKEN1]?.toFixed(6)} ${
tokens[Field.TOKEN1]?.symbol
} or the transaction will revert.`}
</Text>
</>
)
}
const pendingText = `Removed ${parsedAmounts[Field.TOKEN0]?.toSignificant(6)} ${
tokens[Field.TOKEN0]?.symbol
} and ${parsedAmounts[Field.TOKEN1]?.toSignificant(6)} ${tokens[Field.TOKEN1]?.symbol}`
return (
<Wrapper>
{!!parsedAmounts[Field.TOKEN0] && !!parsedAmounts[Field.TOKEN1] && !!parsedAmounts[Field.LIQUIDITY] && (
<ConfirmationModal
isOpen={showConfirm}
onDismiss={() => {
resetModalState()
setShowConfirm(false)
}}
amount0={parsedAmounts[Field.TOKEN0]}
amount1={parsedAmounts[Field.TOKEN1]}
price={route?.midPrice}
liquidityAmount={parsedAmounts[Field.LIQUIDITY]}
transactionType={TRANSACTION_TYPE.REMOVE}
contractCall={onRemove}
extraCall={onSign}
signed={signed}
attemptedRemoval={attemptedRemoval}
attemptingTxn={attemptedRemoval}
pendingConfirmation={pendingConfirmation}
hash={txHash ? txHash : ''}
topContent={modalHeader}
bottomContent={modalBottom}
pendingText={pendingText}
title="You will remove"
/>
)}
<AutoColumn gap="20px">
<LightCard>
<AutoColumn gap="20px">
......
......@@ -3,22 +3,22 @@ import styled from 'styled-components'
import { JSBI } from '@uniswap/sdk'
import { withRouter } from 'react-router-dom'
import { useWeb3React } from '@web3-react/core'
import { useAllTokens } from '../../contexts/Tokens'
import { useAllExchanges } from '../../contexts/Exchanges'
import { useAllBalances, useAccountLPBalances } from '../../contexts/Balances'
import Card from '../../components/Card'
import Question from '../../components/Question'
import SearchModal from '../../components/SearchModal'
import PositionCard from '../../components/PositionCard'
import Row, { RowBetween } from '../../components/Row'
import Card, { LightCard } from '../../components/Card'
import { Link } from '../../theme'
import { Text } from 'rebass'
import { AutoColumn } from '../../components/Column'
import { ArrowRight } from 'react-feather'
import { ButtonPrimary } from '../../components/Button'
import { useWeb3React } from '@web3-react/core'
import { useAllTokens } from '../../contexts/Tokens'
import { useAllExchanges } from '../../contexts/Exchanges'
import { useAllBalances, useAccountLPBalances } from '../../contexts/Balances'
const Positions = styled.div`
position: relative;
margin-top: 38px;
......@@ -71,19 +71,16 @@ function Supply({ history }) {
</ButtonPrimary>
<Positions>
<AutoColumn gap="20px">
{filteredExchangeList?.length !== 0 && (
<RowBetween>
<Text fontWeight={500}>Your Pooled Liquidity</Text>
<Question text="filler text" />
</RowBetween>
{filteredExchangeList}
{filteredExchangeList?.length === 0 && (
<LightCard bg="rgba(255, 255, 255, 0.6)" padding={'45px'}>
<Text color="#C3C5CB">Add liquidity to see your positions</Text>
</LightCard>
)}
{filteredExchangeList}
<AutoColumn justify="center">
<Text color="#AEAEAE">
Already have liquidity?{' '}
{filteredExchangeList?.length !== 0 ? `Don't see your ` : 'Already have '} liquidity?{' '}
<Link
onClick={() => {
history.push('/find')
......
......@@ -45,6 +45,7 @@ export const Link = styled.a.attrs({
text-decoration: none;
cursor: pointer;
color: ${({ theme }) => theme.royalBlue};
font-weight: 500;
:focus {
outline: none;
......
......@@ -3,6 +3,7 @@ import { ThemeProvider as StyledComponentsThemeProvider, createGlobalStyle, css
import { getQueryParam, checkSupportedTheme } from '../utils'
import { SUPPORTED_THEMES } from '../constants'
import { useDarkModeManager } from '../contexts/LocalStorage'
import { Text } from 'rebass'
export * from './components'
......@@ -117,6 +118,19 @@ const theme = darkMode => ({
`
})
export const TYPE = {
main: ({ children, ...rest }) => (
<Text fontWeight={500} color={theme().mineshaftGray} {...rest}>
{children}
</Text>
),
blue: ({ children, ...rest }) => (
<Text fontWeight={500} color={theme().royalBlue} {...rest}>
{children}
</Text>
)
}
export const GlobalStyle = createGlobalStyle`
@import url('https://rsms.me/inter/inter.css');
html { font-family: 'Inter', sans-serif; }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment