Commit 28c916ff authored by Noah Zinsmeister's avatar Noah Zinsmeister Committed by GitHub

Remove liquidity callback (#837)

* give add liquidity the reducer treatment

rename setDefaultsFromURL to setDefaultsFromURLSearch

* fix tests and crash

* rework DOM structure to make flow more natural

* allow slippage + deadline setting in add liquidity

* migrate burn

* disable token selection in mint

clear input between pairs

* reset fields between pairs

* tweak helper text

* address review comments
parent 7adb4b6b
describe('Remove Liquidity', () => { describe('Remove Liquidity', () => {
it('loads the two correct tokens', () => { it('loads the two correct tokens', () => {
cy.visit('/remove/0xc778417E063141139Fce010982780140Aa0cD5Ab-0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85') cy.visit('/remove/0xc778417E063141139Fce010982780140Aa0cD5Ab-0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85')
cy.get('#remove-liquidity-token0-symbol').should('contain.text', 'ETH') cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'ETH')
cy.get('#remove-liquidity-token1-symbol').should('contain.text', 'MKR') cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'MKR')
}) })
it('does not crash if ETH is duplicated', () => { it('does not crash if ETH is duplicated', () => {
cy.visit('/remove/0xc778417E063141139Fce010982780140Aa0cD5Ab-0xc778417E063141139Fce010982780140Aa0cD5Ab') cy.visit('/remove/0xc778417E063141139Fce010982780140Aa0cD5Ab-0xc778417E063141139Fce010982780140Aa0cD5Ab')
cy.get('#remove-liquidity-token0-symbol').should('contain.text', 'ETH') cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'ETH')
cy.get('#remove-liquidity-token1-symbol').should('not.contain.text', 'ETH') cy.get('#remove-liquidity-tokenb-symbol').should('not.contain.text', 'ETH')
}) })
it('token not in storage is loaded', () => { it('token not in storage is loaded', () => {
cy.visit('/remove/0xb290b2f9f8f108d03ff2af3ac5c8de6de31cdf6d-0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85') cy.visit('/remove/0xb290b2f9f8f108d03ff2af3ac5c8de6de31cdf6d-0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85')
cy.get('#remove-liquidity-token0-symbol').should('contain.text', 'SKL') cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'SKL')
cy.get('#remove-liquidity-token1-symbol').should('contain.text', 'MKR') cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'MKR')
}) })
}) })
...@@ -110,8 +110,8 @@ function NavigationTabs({ location: { pathname }, history }: RouteComponentProps ...@@ -110,8 +110,8 @@ function NavigationTabs({ location: { pathname }, history }: RouteComponentProps
<QuestionHelper <QuestionHelper
text={ text={
adding adding
? 'When you add liquidity, you are given pool tokens that represent your position in this pool. These tokens automatically earn fees proportional to your pool share and can be redeemed at any time.' ? 'When you add liquidity, you are given pool tokens representing your position. These tokens automatically earn fees proportional to your share of the pool, and can be redeemed at any time.'
: 'Your liquidity is represented by a pool token (ERC20). Removing will convert your position back into tokens at the current rate and proportional to the amount of each token in the pool. Any fees you accrued are included in the token amounts you receive.' : 'Removing pool tokens converts your position back into underlying tokens at the current rate, proportional to your share of the pool. Accrued fees are included in the amounts you receive.'
} }
/> />
</RowBetween> </RowBetween>
......
import React, { useState, useEffect, useCallback } from 'react' import React from 'react'
import Slider from '@material-ui/core/Slider' import Slider from '@material-ui/core/Slider'
import { withStyles } from '@material-ui/core/styles' import { withStyles } from '@material-ui/core/styles'
import { useDebounce } from '../../hooks'
const StyledSlider = withStyles({ const StyledSlider = withStyles({
root: { root: {
...@@ -51,36 +50,12 @@ const StyledSlider = withStyles({ ...@@ -51,36 +50,12 @@ const StyledSlider = withStyles({
interface InputSliderProps { interface InputSliderProps {
value: number value: number
onChange: (val: number) => void onChange: (value: number) => void
override?: boolean
} }
export default function InputSlider({ value, onChange, override }: InputSliderProps) { export default function InputSlider({ value, onChange }: InputSliderProps) {
const [internalVal, setInternalVal] = useState<number>(value) function wrappedOnChange(_, value) {
const debouncedInternalValue = useDebounce(internalVal, 100) onChange(value)
}
const handleChange = useCallback( return <StyledSlider value={value} onChange={wrappedOnChange} aria-labelledby="input-slider" step={1} />
(e, val) => {
setInternalVal(val)
if (val !== debouncedInternalValue) {
onChange(val)
}
},
[setInternalVal, onChange, debouncedInternalValue]
)
useEffect(() => {
if (override) {
setInternalVal(value)
}
}, [override, value])
return (
<StyledSlider
value={typeof internalVal === 'number' ? internalVal : 0}
onChange={handleChange}
aria-labelledby="input-slider"
step={1}
/>
)
} }
...@@ -151,7 +151,7 @@ export default function SlippageTabs({ rawSlippage, setRawSlippage, deadline, se ...@@ -151,7 +151,7 @@ export default function SlippageTabs({ rawSlippage, setRawSlippage, deadline, se
<TYPE.black fontWeight={400} fontSize={14} color={theme.text2}> <TYPE.black fontWeight={400} fontSize={14} color={theme.text2}>
Set slippage tolerance Set slippage tolerance
</TYPE.black> </TYPE.black>
<QuestionHelper text="Your transaction will revert if the execution price changes by more than this amount after you submit your trade." /> <QuestionHelper text="Your transaction will revert if the price changes unfavorably by more than this percentage." />
</RowFixed> </RowFixed>
<SlippageSelector> <SlippageSelector>
...@@ -227,7 +227,7 @@ export default function SlippageTabs({ rawSlippage, setRawSlippage, deadline, se ...@@ -227,7 +227,7 @@ export default function SlippageTabs({ rawSlippage, setRawSlippage, deadline, se
<TYPE.black fontSize={14} color={theme.text2}> <TYPE.black fontSize={14} color={theme.text2}>
Deadline Deadline
</TYPE.black> </TYPE.black>
<QuestionHelper text="Deadline in minutes. If your transaction takes longer than this it will revert." /> <QuestionHelper text="Your transaction will revert if it is pending for more than this long." />
</RowFixed> </RowFixed>
<RowFixed padding={'0 20px'}> <RowFixed padding={'0 20px'}>
<OptionCustom style={{ width: '80px' }} tabIndex={-1}> <OptionCustom style={{ width: '80px' }} tabIndex={-1}>
......
...@@ -29,13 +29,7 @@ function TradeSummary({ trade, allowedSlippage }: { trade: Trade; allowedSlippag ...@@ -29,13 +29,7 @@ function TradeSummary({ trade, allowedSlippage }: { trade: Trade; allowedSlippag
<TYPE.black fontSize={14} fontWeight={400} color={theme.text2}> <TYPE.black fontSize={14} fontWeight={400} color={theme.text2}>
{isExactIn ? 'Minimum received' : 'Maximum sold'} {isExactIn ? 'Minimum received' : 'Maximum sold'}
</TYPE.black> </TYPE.black>
<QuestionHelper <QuestionHelper text="Your transaction will revert if there is a large, unfavorable price movement before it is confirmed." />
text={
isExactIn
? 'Price can change between when a transaction is submitted and when it is executed. This is the minimum amount you will receive. A worse rate will cause your transaction to revert.'
: 'Price can change between when a transaction is submitted and when it is executed. This is the maximum amount you will pay. A worse rate will cause your transaction to revert.'
}
/>
</RowFixed> </RowFixed>
<RowFixed> <RowFixed>
<TYPE.black color={theme.text1} fontSize={14}> <TYPE.black color={theme.text1} fontSize={14}>
......
...@@ -66,9 +66,9 @@ export default function SwapModalFooter({ ...@@ -66,9 +66,9 @@ export default function SwapModalFooter({
<RowBetween> <RowBetween>
<RowFixed> <RowFixed>
<TYPE.black fontSize={14} fontWeight={400} color={theme.text2}> <TYPE.black fontSize={14} fontWeight={400} color={theme.text2}>
{trade?.tradeType === TradeType.EXACT_INPUT ? 'Min sent' : 'Maximum sold'} {trade?.tradeType === TradeType.EXACT_INPUT ? 'Minimum sent' : 'Maximum sold'}
</TYPE.black> </TYPE.black>
<QuestionHelper text="A boundary is set so you are protected from large price movements after you submit your trade." /> <QuestionHelper text="Your transaction will revert if there is a large, unfavorable price movement before it is confirmed." />
</RowFixed> </RowFixed>
<RowFixed> <RowFixed>
<TYPE.black fontSize={14}> <TYPE.black fontSize={14}>
......
...@@ -21,13 +21,6 @@ export const ArrowWrapper = styled.div` ...@@ -21,13 +21,6 @@ export const ArrowWrapper = styled.div`
} }
` `
export const FixedBottom = styled.div`
position: absolute;
margin-top: 1.5rem;
width: 100%;
margin-bottom: 40px;
`
export const AdvancedDropdown = styled.div` export const AdvancedDropdown = styled.div`
padding-top: calc(10px + 2rem); padding-top: calc(10px + 2rem);
padding-bottom: 10px; padding-bottom: 10px;
......
...@@ -4,12 +4,7 @@ import styled from 'styled-components' ...@@ -4,12 +4,7 @@ import styled from 'styled-components'
export const Wrapper = styled.div` export const Wrapper = styled.div`
position: relative; position: relative;
` `
export const FixedBottom = styled.div`
position: absolute;
top: 100px;
width: 100%;
margin-bottom: 80px;
`
export const ClickableText = styled(Text)` export const ClickableText = styled(Text)`
:hover { :hover {
cursor: pointer; cursor: pointer;
......
import { splitSignature } from '@ethersproject/bytes' import { splitSignature } from '@ethersproject/bytes'
import { Contract } from '@ethersproject/contracts' import { Contract } from '@ethersproject/contracts'
import { parseUnits } from '@ethersproject/units' import { Percent, WETH } from '@uniswap/sdk'
import { JSBI, Percent, Route, Token, TokenAmount, WETH } from '@uniswap/sdk' import React, { useContext, useState } from 'react'
import React, { useCallback, useContext, useEffect, useReducer, useState } from 'react'
import { ArrowDown, Plus } from 'react-feather' import { ArrowDown, Plus } from 'react-feather'
import ReactGA from 'react-ga' import ReactGA from 'react-ga'
import { RouteComponentProps } from 'react-router' import { RouteComponentProps } from 'react-router'
import { Text } from 'rebass' import { Text } from 'rebass'
import { ThemeContext } from 'styled-components' import { ThemeContext } from 'styled-components'
import { ButtonConfirmed, ButtonPrimary } from '../../components/Button' import { ButtonConfirmed, ButtonPrimary, ButtonLight, ButtonError } from '../../components/Button'
import { LightCard } from '../../components/Card' import { LightCard } from '../../components/Card'
import { AutoColumn, ColumnCenter } from '../../components/Column' import { AutoColumn, ColumnCenter } from '../../components/Column'
import ConfirmationModal from '../../components/ConfirmationModal' import ConfirmationModal from '../../components/ConfirmationModal'
...@@ -19,353 +18,75 @@ import Row, { RowBetween, RowFixed } from '../../components/Row' ...@@ -19,353 +18,75 @@ import Row, { RowBetween, RowFixed } from '../../components/Row'
import Slider from '../../components/Slider' import Slider from '../../components/Slider'
import TokenLogo from '../../components/TokenLogo' import TokenLogo from '../../components/TokenLogo'
import { ROUTER_ADDRESS, DEFAULT_DEADLINE_FROM_NOW } from '../../constants' import { ROUTER_ADDRESS, DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE } from '../../constants'
import { usePair } from '../../data/Reserves'
import { useTotalSupply } from '../../data/TotalSupply'
import { usePairContract, useActiveWeb3React } from '../../hooks' import { usePairContract, useActiveWeb3React } from '../../hooks'
import { useTokenByAddressAndAutomaticallyAdd } from '../../hooks/Tokens'
import { useTransactionAdder } from '../../state/transactions/hooks' import { useTransactionAdder } from '../../state/transactions/hooks'
import { useTokenBalance } from '../../state/wallet/hooks'
import { TYPE } from '../../theme' import { TYPE } from '../../theme'
import { calculateGasMargin, calculateSlippageAmount, getRouterContract } from '../../utils' import { calculateGasMargin, calculateSlippageAmount, getRouterContract } from '../../utils'
import AppBody from '../AppBody' import AppBody from '../AppBody'
import { ClickableText, FixedBottom, MaxButton, Wrapper } from '../Pool/styleds' import { ClickableText, MaxButton, Wrapper } from '../Pool/styleds'
import { useApproveCallback, ApprovalState } from '../../hooks/useApproveCallback' import { useApproveCallback, ApprovalState } from '../../hooks/useApproveCallback'
import { Dots } from '../../components/swap/styleds' import { Dots } from '../../components/swap/styleds'
import { useDefaultsFromURLMatchParams, useBurnActionHandlers } from '../../state/burn/hooks'
// denominated in bips import { useDerivedBurnInfo, useBurnState } from '../../state/burn/hooks'
const ALLOWED_SLIPPAGE = 50 import AdvancedSwapDetailsDropdown from '../../components/swap/AdvancedSwapDetailsDropdown'
import { Field } from '../../state/burn/actions'
enum Field { import { useWalletModalToggle } from '../../state/application/hooks'
LIQUIDITY = 'LIQUIDITY',
TOKEN0 = 'TOKEN0',
TOKEN1 = 'TOKEN1'
}
interface RemoveState {
independentField: Field
typedValue: string
[Field.LIQUIDITY]: {
address: string | undefined
}
[Field.TOKEN0]: {
address: string | undefined
}
[Field.TOKEN1]: {
address: string | undefined
}
}
function initializeRemoveState(liquidity, inputAddress?: string, outputAddress?: string): RemoveState {
return {
independentField: Field.LIQUIDITY,
typedValue: liquidity || '',
[Field.LIQUIDITY]: {
address: ''
},
[Field.TOKEN0]: {
address: inputAddress
},
[Field.TOKEN1]: {
address: outputAddress
}
}
}
enum RemoveAction {
TYPE
}
interface Payload {
[RemoveAction.TYPE]: {
field: Field
typedValue: string
}
}
function reducer(
state: RemoveState,
action: {
type: RemoveAction
payload: Payload[RemoveAction]
}
): RemoveState {
switch (action.type) {
case RemoveAction.TYPE: {
const { field, typedValue } = action.payload as Payload[RemoveAction.TYPE]
return {
...state,
independentField: field,
typedValue
}
}
default: {
throw Error
}
}
}
export default function RemoveLiquidity({ match: { params } }: RouteComponentProps<{ tokens: string }>) { export default function RemoveLiquidity({ match: { params } }: RouteComponentProps<{ tokens: string }>) {
const [token0, token1] = params.tokens.split('-') useDefaultsFromURLMatchParams(params)
const { account, chainId, library } = useActiveWeb3React() const { account, chainId, library } = useActiveWeb3React()
const theme = useContext(ThemeContext) const theme = useContext(ThemeContext)
const [showConfirm, setShowConfirm] = useState<boolean>(false) // toggle wallet when disconnected
const [showAdvanced, setShowAdvanced] = useState<boolean>(false) const toggleWalletModal = useWalletModalToggle()
const inputToken: Token = useTokenByAddressAndAutomaticallyAdd(token0)
const outputToken: Token = useTokenByAddressAndAutomaticallyAdd(token1)
// get basic SDK entities
const tokens: { [field in Field]?: Token } = {
[Field.TOKEN0]: inputToken,
[Field.TOKEN1]: outputToken && inputToken && outputToken.equals(inputToken) ? undefined : outputToken
}
const pair = usePair(inputToken, outputToken)
const pairContract: Contract = usePairContract(pair?.liquidityToken.address)
// pool token data
const userLiquidity = useTokenBalance(account, pair?.liquidityToken)
const totalPoolTokens = useTotalSupply(pair?.liquidityToken)
// input state
const [state, dispatch] = useReducer(reducer, initializeRemoveState(userLiquidity?.toExact(), token0, token1))
const { independentField, typedValue } = state
const tokensDeposited: { [field in Field]?: TokenAmount } = {
[Field.TOKEN0]:
pair &&
totalPoolTokens &&
userLiquidity &&
// this condition is a short-circuit in the case where useTokenBalance updates sooner than useTotalSupply
JSBI.greaterThanOrEqual(totalPoolTokens.raw, userLiquidity.raw)
? pair.getLiquidityValue(tokens[Field.TOKEN0], totalPoolTokens, userLiquidity, false)
: undefined,
[Field.TOKEN1]:
pair &&
totalPoolTokens &&
userLiquidity &&
// this condition is a short-circuit in the case where useTokenBalance updates sooner than useTotalSupply
JSBI.greaterThanOrEqual(totalPoolTokens.raw, userLiquidity.raw)
? pair.getLiquidityValue(tokens[Field.TOKEN1], totalPoolTokens, userLiquidity, false)
: undefined
}
const route: Route = pair // burn state
? new Route([pair], independentField !== Field.LIQUIDITY ? tokens[independentField] : tokens[Field.TOKEN1]) const { independentField, typedValue } = useBurnState()
: undefined const { tokens, pair, route, parsedAmounts, error } = useDerivedBurnInfo()
const { onUserInput } = useBurnActionHandlers()
// update input value when user types const isValid = !error
const onUserInput = useCallback((field: Field, typedValue: string) => {
dispatch({ type: RemoveAction.TYPE, payload: { field, typedValue } })
}, [])
const parsedAmounts: { [field in Field]?: TokenAmount } = {}
let poolTokenAmount
try {
if (typedValue !== '' && typedValue !== '.' && tokens[Field.TOKEN0] && tokens[Field.TOKEN1] && userLiquidity) {
if (independentField === Field.TOKEN0) {
const typedValueParsed = parseUnits(typedValue, tokens[Field.TOKEN0].decimals).toString()
if (typedValueParsed !== '0') {
const tokenAmount = new TokenAmount(tokens[Field.TOKEN0], typedValueParsed)
if (
tokensDeposited[Field.TOKEN0] &&
JSBI.lessThanOrEqual(tokenAmount.raw, tokensDeposited[Field.TOKEN0].raw)
) {
poolTokenAmount = JSBI.divide(
JSBI.multiply(tokenAmount.raw, userLiquidity.raw),
tokensDeposited[Field.TOKEN0].raw
)
}
}
}
if (independentField === Field.TOKEN1) {
const typedValueParsed = parseUnits(typedValue, tokens[Field.TOKEN1].decimals).toString()
if (typedValueParsed !== '0') {
const tokenAmount = new TokenAmount(tokens[Field.TOKEN1], typedValueParsed)
if (
tokensDeposited[Field.TOKEN1] &&
JSBI.lessThanOrEqual(tokenAmount.raw, tokensDeposited[Field.TOKEN1].raw)
) {
poolTokenAmount = JSBI.divide(
JSBI.multiply(tokenAmount.raw, userLiquidity.raw),
tokensDeposited[Field.TOKEN1].raw
)
}
}
}
if (independentField === Field.LIQUIDITY) {
const typedValueParsed = parseUnits(typedValue, pair?.liquidityToken.decimals).toString()
const formattedAmount = new TokenAmount(pair?.liquidityToken, typedValueParsed)
if (typedValueParsed !== '0') {
if (JSBI.lessThanOrEqual(formattedAmount.raw, userLiquidity?.raw)) {
poolTokenAmount = typedValueParsed
}
}
}
}
} catch (error) {
// should only fail if the user specifies too many decimal places of precision (or maybe exceed max uint?)
console.error(error)
}
// set parsed amounts based on live amount of liquidity
parsedAmounts[Field.LIQUIDITY] =
!!pair && !!poolTokenAmount ? new TokenAmount(pair.liquidityToken, poolTokenAmount) : undefined
parsedAmounts[Field.TOKEN0] =
!!pair &&
!!totalPoolTokens &&
!!parsedAmounts[Field.LIQUIDITY] &&
// this condition is a short-circuit in the case where useTokenBalance updates sooner than useTotalSupply
JSBI.greaterThanOrEqual(totalPoolTokens.raw, userLiquidity.raw)
? pair.getLiquidityValue(tokens[Field.TOKEN0], totalPoolTokens, parsedAmounts[Field.LIQUIDITY], false)
: undefined
parsedAmounts[Field.TOKEN1] =
!!pair &&
!!totalPoolTokens &&
!!parsedAmounts[Field.LIQUIDITY] &&
// this condition is a short-circuit in the case where useTokenBalance updates sooner than useTotalSupply
JSBI.greaterThanOrEqual(totalPoolTokens.raw, userLiquidity.raw)
? pair.getLiquidityValue(tokens[Field.TOKEN1], totalPoolTokens, parsedAmounts[Field.LIQUIDITY], false)
: undefined
// derived percent for advanced mode
const derivedPercent =
!!parsedAmounts[Field.LIQUIDITY] && !!userLiquidity
? new Percent(parsedAmounts[Field.LIQUIDITY].raw, userLiquidity.raw)
: undefined
const [override, setSliderOverride] = useState(false) // override slider internal value
const handlePresetPercentage = newPercent => {
setSliderOverride(true)
onUserInput(
Field.LIQUIDITY,
new TokenAmount(
pair?.liquidityToken,
JSBI.divide(JSBI.multiply(userLiquidity.raw, JSBI.BigInt(newPercent)), JSBI.BigInt(100))
).toExact()
)
}
const handleSliderChange = newPercent => {
onUserInput(
Field.LIQUIDITY,
new TokenAmount(
pair?.liquidityToken,
JSBI.divide(JSBI.multiply(userLiquidity.raw, JSBI.BigInt(newPercent)), JSBI.BigInt(100))
).toExact()
)
}
// check if the user has approved router to withdraw their LP tokens // modal and loading
const [approval, approveCallback] = useApproveCallback(parsedAmounts[Field.LIQUIDITY], ROUTER_ADDRESS) const [showConfirm, setShowConfirm] = useState<boolean>(false)
const [showAdvanced, setShowAdvanced] = useState<boolean>(false)
const [showDetailed, setShowDetailed] = useState<boolean>(false)
const [attemptingTxn, setAttemptingTxn] = useState(false) // clicked confirm
const [pendingConfirmation, setPendingConfirmation] = useState(true) // waiting for
// adjust amounts for slippage // txn values
const slippageAdjustedAmounts = { const [txHash, setTxHash] = useState<string>('')
[Field.TOKEN0]: const [deadline, setDeadline] = useState<number>(DEFAULT_DEADLINE_FROM_NOW)
tokens[Field.TOKEN0] && parsedAmounts[Field.TOKEN0] const [allowedSlippage, setAllowedSlippage] = useState<number>(INITIAL_ALLOWED_SLIPPAGE)
? new TokenAmount(
tokens[Field.TOKEN0],
calculateSlippageAmount(parsedAmounts[Field.TOKEN0], ALLOWED_SLIPPAGE)[0]
)
: undefined,
[Field.TOKEN1]:
tokens[Field.TOKEN1] && parsedAmounts[Field.TOKEN1]
? new TokenAmount(
tokens[Field.TOKEN1],
calculateSlippageAmount(parsedAmounts[Field.TOKEN1], ALLOWED_SLIPPAGE)[0]
)
: undefined
}
// get formatted amounts
const formattedAmounts = { const formattedAmounts = {
[Field.LIQUIDITY_PERCENT]: parsedAmounts[Field.LIQUIDITY_PERCENT].equalTo('0')
? '0'
: parsedAmounts[Field.LIQUIDITY_PERCENT].lessThan(new Percent('1', '100'))
? '<1'
: parsedAmounts[Field.LIQUIDITY_PERCENT].toFixed(0),
[Field.LIQUIDITY]: [Field.LIQUIDITY]:
independentField === Field.LIQUIDITY independentField === Field.LIQUIDITY ? typedValue : parsedAmounts[Field.LIQUIDITY]?.toSignificant(6) ?? '',
? typedValue [Field.TOKEN_A]:
: parsedAmounts[Field.LIQUIDITY] independentField === Field.TOKEN_A ? typedValue : parsedAmounts[Field.TOKEN_A]?.toSignificant(6) ?? '',
? parsedAmounts[Field.LIQUIDITY].toSignificant(6) [Field.TOKEN_B]:
: '', independentField === Field.TOKEN_B ? typedValue : parsedAmounts[Field.TOKEN_B]?.toSignificant(6) ?? ''
[Field.TOKEN0]:
independentField === Field.TOKEN0
? typedValue
: parsedAmounts[Field.TOKEN0]
? parsedAmounts[Field.TOKEN0].toSignificant(6)
: '',
[Field.TOKEN1]:
independentField === Field.TOKEN1
? typedValue
: parsedAmounts[Field.TOKEN1]
? parsedAmounts[Field.TOKEN1].toSignificant(6)
: ''
} }
const onMax = () => { const atMaxAmount = parsedAmounts[Field.LIQUIDITY_PERCENT]?.equalTo(new Percent('1'))
onUserInput(Field.LIQUIDITY, userLiquidity.toExact())
}
const atMaxAmount = // pair contract
!!userLiquidity && !!parsedAmounts[Field.LIQUIDITY] const pairContract: Contract = usePairContract(pair?.liquidityToken?.address)
? JSBI.equal(userLiquidity.raw, parsedAmounts[Field.LIQUIDITY].raw)
: false
// errors
const [generalError, setGeneralError] = useState<string>('')
const [inputError, setInputError] = useState<string>('')
const [outputError, setOutputError] = useState<string>('')
const [poolTokenError, setPoolTokenError] = useState<string>('')
const [isValid, setIsValid] = useState<boolean>(false)
// update errors live
useEffect(() => {
// reset errors
setGeneralError('')
setInputError('')
setOutputError('')
setPoolTokenError('')
setIsValid(true)
if (formattedAmounts[Field.TOKEN0] === '') {
setGeneralError('Enter an amount')
setIsValid(false)
} else if (!parsedAmounts[Field.TOKEN0]) {
setInputError('Invalid amount')
setIsValid(false)
}
if (formattedAmounts[Field.TOKEN1] === '') { // allowance handling
setGeneralError('Enter an amount')
setIsValid(false)
} else if (!parsedAmounts[Field.TOKEN1]) {
setOutputError('Invalid amount')
setIsValid(false)
}
if (formattedAmounts[Field.LIQUIDITY] === '') {
setGeneralError('Enter an amount')
setIsValid(false)
} else if (!parsedAmounts[Field.LIQUIDITY]) {
setPoolTokenError('Invalid Amount')
setIsValid(false)
}
}, [formattedAmounts, parsedAmounts, totalPoolTokens, userLiquidity])
// state for txn
const addTransaction = useTransactionAdder()
const [txHash, setTxHash] = useState()
const [signatureData, setSignatureData] = useState<{ v: number; r: string; s: string; deadline: number }>(null) const [signatureData, setSignatureData] = useState<{ v: number; r: string; s: string; deadline: number }>(null)
const [attemptedRemoval, setAttemptedRemoval] = useState(false) // clicked confirm const [approval, approveCallback] = useApproveCallback(parsedAmounts[Field.LIQUIDITY], ROUTER_ADDRESS)
const [pendingConfirmation, setPendingConfirmation] = useState(true) // waiting for
async function onAttemptToApprove() { async function onAttemptToApprove() {
// try to gather a signature for permission // try to gather a signature for permission
const nonce = await pairContract.nonces(account) const nonce = await pairContract.nonces(account)
const deadlineForSignature: number = Math.ceil(Date.now() / 1000) + DEFAULT_DEADLINE_FROM_NOW const deadlineForSignature: number = Math.ceil(Date.now() / 1000) + deadline
const EIP712Domain = [ const EIP712Domain = [
{ name: 'name', type: 'string' }, { name: 'name', type: 'string' },
...@@ -424,19 +145,28 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro ...@@ -424,19 +145,28 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
function resetModalState() { function resetModalState() {
setSignatureData(null) setSignatureData(null)
setAttemptedRemoval(false) setAttemptingTxn(false)
setPendingConfirmation(true) setPendingConfirmation(true)
} }
// tx sending
const addTransaction = useTransactionAdder()
async function onRemove() { async function onRemove() {
setAttemptedRemoval(true) setAttemptingTxn(true)
const router = getRouterContract(chainId, library, account) const router = getRouterContract(chainId, library, account)
const token0IsETH = tokens[Field.TOKEN0].equals(WETH[chainId]) const amountsMin = {
const oneTokenIsETH = token0IsETH || tokens[Field.TOKEN1].equals(WETH[chainId]) [Field.TOKEN_A]: calculateSlippageAmount(parsedAmounts[Field.TOKEN_A], allowedSlippage)[0],
[Field.TOKEN_B]: calculateSlippageAmount(parsedAmounts[Field.TOKEN_B], allowedSlippage)[0]
}
const tokenBIsETH = tokens[Field.TOKEN_B].equals(WETH[chainId])
const oneTokenIsETH = tokens[Field.TOKEN_A].equals(WETH[chainId]) || tokenBIsETH
const deadlineFromNow = Math.ceil(Date.now() / 1000) + deadline
let estimate, method, args let estimate, method: Function, args: Array<string | string[] | number | boolean>
// we have approval, use normal remove liquidity // we have approval, use normal remove liquidity
if (approval === ApprovalState.APPROVED) { if (approval === ApprovalState.APPROVED) {
// removeLiquidityETH // removeLiquidityETH
...@@ -444,12 +174,12 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro ...@@ -444,12 +174,12 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
estimate = router.estimateGas.removeLiquidityETH estimate = router.estimateGas.removeLiquidityETH
method = router.removeLiquidityETH method = router.removeLiquidityETH
args = [ args = [
tokens[token0IsETH ? Field.TOKEN1 : Field.TOKEN0].address, tokens[tokenBIsETH ? Field.TOKEN_A : Field.TOKEN_B].address,
parsedAmounts[Field.LIQUIDITY].raw.toString(), parsedAmounts[Field.LIQUIDITY].raw.toString(),
slippageAdjustedAmounts[token0IsETH ? Field.TOKEN1 : Field.TOKEN0].raw.toString(), amountsMin[tokenBIsETH ? Field.TOKEN_A : Field.TOKEN_B].toString(),
slippageAdjustedAmounts[token0IsETH ? Field.TOKEN0 : Field.TOKEN1].raw.toString(), amountsMin[tokenBIsETH ? Field.TOKEN_B : Field.TOKEN_A].toString(),
account, account,
Math.ceil(Date.now() / 1000) + DEFAULT_DEADLINE_FROM_NOW deadlineFromNow
] ]
} }
// removeLiquidity // removeLiquidity
...@@ -457,13 +187,13 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro ...@@ -457,13 +187,13 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
estimate = router.estimateGas.removeLiquidity estimate = router.estimateGas.removeLiquidity
method = router.removeLiquidity method = router.removeLiquidity
args = [ args = [
tokens[Field.TOKEN0].address, tokens[Field.TOKEN_A].address,
tokens[Field.TOKEN1].address, tokens[Field.TOKEN_B].address,
parsedAmounts[Field.LIQUIDITY].raw.toString(), parsedAmounts[Field.LIQUIDITY].raw.toString(),
slippageAdjustedAmounts[Field.TOKEN0].raw.toString(), amountsMin[Field.TOKEN_A].toString(),
slippageAdjustedAmounts[Field.TOKEN1].raw.toString(), amountsMin[Field.TOKEN_B].toString(),
account, account,
Math.ceil(Date.now() / 1000) + DEFAULT_DEADLINE_FROM_NOW deadlineFromNow
] ]
} }
} }
...@@ -474,10 +204,10 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro ...@@ -474,10 +204,10 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
estimate = router.estimateGas.removeLiquidityETHWithPermit estimate = router.estimateGas.removeLiquidityETHWithPermit
method = router.removeLiquidityETHWithPermit method = router.removeLiquidityETHWithPermit
args = [ args = [
tokens[token0IsETH ? Field.TOKEN1 : Field.TOKEN0].address, tokens[tokenBIsETH ? Field.TOKEN_A : Field.TOKEN_B].address,
parsedAmounts[Field.LIQUIDITY].raw.toString(), parsedAmounts[Field.LIQUIDITY].raw.toString(),
slippageAdjustedAmounts[token0IsETH ? Field.TOKEN1 : Field.TOKEN0].raw.toString(), amountsMin[tokenBIsETH ? Field.TOKEN_A : Field.TOKEN_B].toString(),
slippageAdjustedAmounts[token0IsETH ? Field.TOKEN0 : Field.TOKEN1].raw.toString(), amountsMin[tokenBIsETH ? Field.TOKEN_B : Field.TOKEN_A].toString(),
account, account,
signatureData.deadline, signatureData.deadline,
false, false,
...@@ -491,11 +221,11 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro ...@@ -491,11 +221,11 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
estimate = router.estimateGas.removeLiquidityWithPermit estimate = router.estimateGas.removeLiquidityWithPermit
method = router.removeLiquidityWithPermit method = router.removeLiquidityWithPermit
args = [ args = [
tokens[Field.TOKEN0].address, tokens[Field.TOKEN_A].address,
tokens[Field.TOKEN1].address, tokens[Field.TOKEN_B].address,
parsedAmounts[Field.LIQUIDITY].raw.toString(), parsedAmounts[Field.LIQUIDITY].raw.toString(),
slippageAdjustedAmounts[Field.TOKEN0].raw.toString(), amountsMin[Field.TOKEN_A].toString(),
slippageAdjustedAmounts[Field.TOKEN1].raw.toString(), amountsMin[Field.TOKEN_B].toString(),
account, account,
signatureData.deadline, signatureData.deadline,
false, false,
...@@ -513,23 +243,25 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro ...@@ -513,23 +243,25 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
method(...args, { method(...args, {
gasLimit: calculateGasMargin(estimatedGasLimit) gasLimit: calculateGasMargin(estimatedGasLimit)
}).then(response => { }).then(response => {
ReactGA.event({
category: 'Liquidity',
action: 'Remove',
label: [tokens[Field.TOKEN0]?.symbol, tokens[Field.TOKEN1]?.symbol].join('/')
})
setPendingConfirmation(false)
setTxHash(response.hash)
addTransaction(response, { addTransaction(response, {
summary: summary:
'Remove ' + 'Remove ' +
parsedAmounts[Field.TOKEN0]?.toSignificant(3) + parsedAmounts[Field.TOKEN_A]?.toSignificant(3) +
' ' + ' ' +
tokens[Field.TOKEN0]?.symbol + tokens[Field.TOKEN_A]?.symbol +
' and ' + ' and ' +
parsedAmounts[Field.TOKEN1]?.toSignificant(3) + parsedAmounts[Field.TOKEN_B]?.toSignificant(3) +
' ' + ' ' +
tokens[Field.TOKEN1]?.symbol tokens[Field.TOKEN_B]?.symbol
})
setTxHash(response.hash)
setPendingConfirmation(false)
ReactGA.event({
category: 'Liquidity',
action: 'Remove',
label: [tokens[Field.TOKEN_A]?.symbol, tokens[Field.TOKEN_B]?.symbol].join('/')
}) })
}) })
) )
...@@ -537,6 +269,7 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro ...@@ -537,6 +269,7 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
console.error(e) console.error(e)
resetModalState() resetModalState()
setShowConfirm(false) setShowConfirm(false)
setShowAdvanced(false)
}) })
} }
...@@ -545,12 +278,12 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro ...@@ -545,12 +278,12 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
<AutoColumn gap={'md'} style={{ marginTop: '20px' }}> <AutoColumn gap={'md'} style={{ marginTop: '20px' }}>
<RowBetween align="flex-end"> <RowBetween align="flex-end">
<Text fontSize={24} fontWeight={500}> <Text fontSize={24} fontWeight={500}>
{!!parsedAmounts[Field.TOKEN0] && parsedAmounts[Field.TOKEN0].toSignificant(6)} {parsedAmounts[Field.TOKEN_A]?.toSignificant(6)}
</Text> </Text>
<RowFixed gap="4px"> <RowFixed gap="4px">
<TokenLogo address={tokens[Field.TOKEN0]?.address} size={'24px'} /> <TokenLogo address={tokens[Field.TOKEN_A]?.address} size={'24px'} />
<Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}> <Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}>
{tokens[Field.TOKEN0]?.symbol || ''} {tokens[Field.TOKEN_A]?.symbol}
</Text> </Text>
</RowFixed> </RowFixed>
</RowBetween> </RowBetween>
...@@ -558,23 +291,20 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro ...@@ -558,23 +291,20 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
<Plus size="16" color={theme.text2} /> <Plus size="16" color={theme.text2} />
</RowFixed> </RowFixed>
<RowBetween align="flex-end"> <RowBetween align="flex-end">
<Text fontSize={24} fontWeight={600}> <Text fontSize={24} fontWeight={500}>
{!!parsedAmounts[Field.TOKEN1] && parsedAmounts[Field.TOKEN1].toSignificant(6)} {parsedAmounts[Field.TOKEN_B]?.toSignificant(6)}
</Text> </Text>
<RowFixed gap="4px"> <RowFixed gap="4px">
<TokenLogo address={tokens[Field.TOKEN1]?.address} size={'24px'} /> <TokenLogo address={tokens[Field.TOKEN_B]?.address} size={'24px'} />
<Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}> <Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}>
{tokens[Field.TOKEN1]?.symbol || ''} {tokens[Field.TOKEN_B]?.symbol}
</Text> </Text>
</RowFixed> </RowFixed>
</RowBetween> </RowBetween>
<TYPE.italic fontSize={12} color={theme.text2} textAlign="left" padding={'12px 0 0 0'}> <TYPE.italic fontSize={12} color={theme.text2} textAlign="left" padding={'12px 0 0 0'}>
{`Output is estimated. You will receive at least ${slippageAdjustedAmounts[Field.TOKEN0]?.toSignificant(6)} ${ {`Output is estimated. If the price changes by more than ${allowedSlippage /
tokens[Field.TOKEN0]?.symbol 100}% your transaction will revert.`}
} and ${slippageAdjustedAmounts[Field.TOKEN1]?.toSignificant(6)} ${
tokens[Field.TOKEN1]?.symbol
} or the transaction will revert.`}
</TYPE.italic> </TYPE.italic>
</AutoColumn> </AutoColumn>
) )
...@@ -585,12 +315,12 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro ...@@ -585,12 +315,12 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
<> <>
<RowBetween> <RowBetween>
<Text color={theme.text2} fontWeight={500} fontSize={16}> <Text color={theme.text2} fontWeight={500} fontSize={16}>
{'UNI ' + tokens[Field.TOKEN0]?.symbol + '/' + tokens[Field.TOKEN1]?.symbol} Burned {'UNI ' + tokens[Field.TOKEN_A]?.symbol + '/' + tokens[Field.TOKEN_B]?.symbol} Burned
</Text> </Text>
<RowFixed> <RowFixed>
<DoubleLogo <DoubleLogo
a0={tokens[Field.TOKEN0]?.address || ''} a0={tokens[Field.TOKEN_A]?.address || ''}
a1={tokens[Field.TOKEN1]?.address || ''} a1={tokens[Field.TOKEN_B]?.address || ''}
margin={true} margin={true}
/> />
<Text fontWeight={500} fontSize={16}> <Text fontWeight={500} fontSize={16}>
...@@ -598,16 +328,25 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro ...@@ -598,16 +328,25 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
</Text> </Text>
</RowFixed> </RowFixed>
</RowBetween> </RowBetween>
<RowBetween> {route && (
<Text color={theme.text2} fontWeight={500} fontSize={16}> <>
Price <RowBetween>
</Text> <Text color={theme.text2} fontWeight={500} fontSize={16}>
<Text fontWeight={500} fontSize={16} color={theme.text1}> Price
{`1 ${tokens[Field.TOKEN1]?.symbol} = ${route?.midPrice && route.midPrice.adjusted.toSignificant(6)} ${ </Text>
tokens[Field.TOKEN0]?.symbol <Text fontWeight={500} fontSize={16} color={theme.text1}>
}`} 1 {tokens[Field.TOKEN_A]?.symbol} = {route.midPrice.toSignificant(6)} {tokens[Field.TOKEN_B]?.symbol}
</Text> </Text>
</RowBetween> </RowBetween>
<RowBetween>
<div />
<Text fontWeight={500} fontSize={16} color={theme.text1}>
1 {tokens[Field.TOKEN_B]?.symbol} = {route.midPrice.invert().toSignificant(6)}{' '}
{tokens[Field.TOKEN_A]?.symbol}
</Text>
</RowBetween>
</>
)}
<RowBetween mt="1rem"> <RowBetween mt="1rem">
<ButtonConfirmed <ButtonConfirmed
onClick={onAttemptToApprove} onClick={onAttemptToApprove}
...@@ -640,178 +379,211 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro ...@@ -640,178 +379,211 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
) )
} }
const pendingText = `Removing ${parsedAmounts[Field.TOKEN0]?.toSignificant(6)} ${ const pendingText = `Removing ${parsedAmounts[Field.TOKEN_A]?.toSignificant(6)} ${
tokens[Field.TOKEN0]?.symbol tokens[Field.TOKEN_A]?.symbol
} and ${parsedAmounts[Field.TOKEN1]?.toSignificant(6)} ${tokens[Field.TOKEN1]?.symbol}` } and ${parsedAmounts[Field.TOKEN_B]?.toSignificant(6)} ${tokens[Field.TOKEN_B]?.symbol}`
return ( return (
<AppBody> <>
<Wrapper> <AppBody>
<ConfirmationModal <Wrapper>
isOpen={showConfirm} <ConfirmationModal
onDismiss={() => { isOpen={showConfirm}
resetModalState() onDismiss={() => {
setShowConfirm(false) resetModalState()
}} setShowConfirm(false)
attemptingTxn={attemptedRemoval} setShowAdvanced(false)
pendingConfirmation={pendingConfirmation} }}
hash={txHash ? txHash : ''} attemptingTxn={attemptingTxn}
topContent={modalHeader} pendingConfirmation={pendingConfirmation}
bottomContent={modalBottom} hash={txHash ? txHash : ''}
pendingText={pendingText} topContent={modalHeader}
title="You will receive" bottomContent={modalBottom}
/> pendingText={pendingText}
<AutoColumn gap="md"> title="You will receive"
<LightCard> />
<AutoColumn gap="20px"> <AutoColumn gap="md">
<RowBetween> <LightCard>
<Text fontWeight={500}>Amount</Text> <AutoColumn gap="20px">
<ClickableText
fontWeight={500}
onClick={() => {
setShowAdvanced(!showAdvanced)
}}
>
{showAdvanced ? 'Hide Advanced' : 'Show Advanced'}
</ClickableText>
</RowBetween>
<Row style={{ alignItems: 'flex-end' }}>
<Text fontSize={72} fontWeight={500}>
{derivedPercent?.toFixed(0) === '0' ? '<1' : derivedPercent?.toFixed(0) ?? '0'}%
</Text>
</Row>
{!showAdvanced && (
<Slider
value={parseInt(derivedPercent?.toFixed(0) ?? '0')}
onChange={handleSliderChange}
override={override}
/>
)}
{!showAdvanced && (
<RowBetween> <RowBetween>
<MaxButton onClick={() => handlePresetPercentage(25)} width="20%"> <Text fontWeight={500}>Amount</Text>
25% <ClickableText
</MaxButton> fontWeight={500}
<MaxButton onClick={() => handlePresetPercentage(50)} width="20%"> onClick={() => {
50% setShowDetailed(!showDetailed)
</MaxButton> }}
<MaxButton onClick={() => handlePresetPercentage(75)} width="20%"> >
75% {showDetailed ? 'Simple' : 'Detailed'}
</MaxButton> </ClickableText>
<MaxButton onClick={() => handlePresetPercentage(100)} width="20%">
Max
</MaxButton>
</RowBetween> </RowBetween>
)} <Row style={{ alignItems: 'flex-end' }}>
</AutoColumn> <Text fontSize={72} fontWeight={500}>
</LightCard> {formattedAmounts[Field.LIQUIDITY_PERCENT]}%
{!showAdvanced && ( </Text>
<> </Row>
<ColumnCenter> {!showDetailed && (
<ArrowDown size="16" color={theme.text2} /> <>
</ColumnCenter>{' '} <Slider
<LightCard> value={Number.parseInt(parsedAmounts[Field.LIQUIDITY_PERCENT].toFixed(0))}
<AutoColumn gap="10px"> onChange={value => {
<RowBetween> onUserInput(Field.LIQUIDITY_PERCENT, value.toString())
<Text fontSize={24} fontWeight={500}> }}
{formattedAmounts[Field.TOKEN0] ? formattedAmounts[Field.TOKEN0] : '-'} />
</Text> <RowBetween>
<RowFixed> <MaxButton onClick={() => onUserInput(Field.LIQUIDITY_PERCENT, '25')} width="20%">
<TokenLogo address={tokens[Field.TOKEN0]?.address} style={{ marginRight: '12px' }} /> 25%
<Text fontSize={24} fontWeight={500} id="remove-liquidity-token0-symbol"> </MaxButton>
{tokens[Field.TOKEN0]?.symbol} <MaxButton onClick={() => onUserInput(Field.LIQUIDITY_PERCENT, '50')} width="20%">
50%
</MaxButton>
<MaxButton onClick={() => onUserInput(Field.LIQUIDITY_PERCENT, '75')} width="20%">
75%
</MaxButton>
<MaxButton onClick={() => onUserInput(Field.LIQUIDITY_PERCENT, '100')} width="20%">
Max
</MaxButton>
</RowBetween>
</>
)}
</AutoColumn>
</LightCard>
{!showDetailed && (
<>
<ColumnCenter>
<ArrowDown size="16" color={theme.text2} />
</ColumnCenter>
<LightCard>
<AutoColumn gap="10px">
<RowBetween>
<Text fontSize={24} fontWeight={500}>
{formattedAmounts[Field.TOKEN_A] || '-'}
</Text> </Text>
</RowFixed> <RowFixed>
</RowBetween> <TokenLogo address={tokens[Field.TOKEN_A]?.address} style={{ marginRight: '12px' }} />
<RowBetween> <Text fontSize={24} fontWeight={500} id="remove-liquidity-tokena-symbol">
<Text fontSize={24} fontWeight={500}> {tokens[Field.TOKEN_A]?.symbol}
{formattedAmounts[Field.TOKEN1] ? formattedAmounts[Field.TOKEN1] : '-'} </Text>
</Text> </RowFixed>
<RowFixed> </RowBetween>
<TokenLogo address={tokens[Field.TOKEN1]?.address} style={{ marginRight: '12px' }} /> <RowBetween>
<Text fontSize={24} fontWeight={500} id="remove-liquidity-token1-symbol"> <Text fontSize={24} fontWeight={500}>
{tokens[Field.TOKEN1]?.symbol} {formattedAmounts[Field.TOKEN_B] || '-'}
</Text> </Text>
</RowFixed> <RowFixed>
</RowBetween> <TokenLogo address={tokens[Field.TOKEN_B]?.address} style={{ marginRight: '12px' }} />
</AutoColumn> <Text fontSize={24} fontWeight={500} id="remove-liquidity-tokenb-symbol">
</LightCard> {tokens[Field.TOKEN_B]?.symbol}
</> </Text>
)} </RowFixed>
</RowBetween>
{showAdvanced && ( </AutoColumn>
<> </LightCard>
<CurrencyInputPanel </>
field={Field.LIQUIDITY} )}
value={formattedAmounts[Field.LIQUIDITY]}
onUserInput={onUserInput} {showDetailed && (
onMax={onMax} <>
showMaxButton={!atMaxAmount} <CurrencyInputPanel
disableTokenSelect field={Field.LIQUIDITY}
token={pair?.liquidityToken} value={formattedAmounts[Field.LIQUIDITY]}
isExchange={true} onUserInput={onUserInput}
pair={pair} onMax={() => {
id="liquidity-amount" onUserInput(Field.LIQUIDITY_PERCENT, '100')
/> }}
<ColumnCenter> showMaxButton={!atMaxAmount}
<ArrowDown size="16" color={theme.text2} /> disableTokenSelect
</ColumnCenter> token={pair?.liquidityToken}
<CurrencyInputPanel isExchange={true}
field={Field.TOKEN0} pair={pair}
value={formattedAmounts[Field.TOKEN0]} id="liquidity-amount"
onUserInput={onUserInput} />
onMax={onMax} <ColumnCenter>
showMaxButton={!atMaxAmount} <ArrowDown size="16" color={theme.text2} />
token={tokens[Field.TOKEN0]} </ColumnCenter>
label={'Output'} <CurrencyInputPanel
disableTokenSelect hideBalance={true}
id="remove-liquidity-token0" field={Field.TOKEN_A}
/> value={formattedAmounts[Field.TOKEN_A]}
<ColumnCenter> onUserInput={onUserInput}
<Plus size="16" color={theme.text2} /> onMax={() => onUserInput(Field.LIQUIDITY_PERCENT, '100')}
</ColumnCenter> showMaxButton={!atMaxAmount}
<CurrencyInputPanel token={tokens[Field.TOKEN_A]}
field={Field.TOKEN1} label={'Output'}
value={formattedAmounts[Field.TOKEN1]} disableTokenSelect
onUserInput={onUserInput} id="remove-liquidity-tokena"
onMax={onMax} />
showMaxButton={!atMaxAmount} <ColumnCenter>
token={tokens[Field.TOKEN1]} <Plus size="16" color={theme.text2} />
label={'Output'} </ColumnCenter>
disableTokenSelect <CurrencyInputPanel
id="remove-liquidity-token1" hideBalance={true}
/> field={Field.TOKEN_B}
</> value={formattedAmounts[Field.TOKEN_B]}
)} onUserInput={onUserInput}
<div style={{ padding: '10px 20px' }}> onMax={() => onUserInput(Field.LIQUIDITY_PERCENT, '100')}
<RowBetween> showMaxButton={!atMaxAmount}
Price: token={tokens[Field.TOKEN_B]}
<div> label={'Output'}
1 {pair?.token0.symbol} ={' '} disableTokenSelect
{independentField === Field.TOKEN0 || independentField === Field.LIQUIDITY id="remove-liquidity-tokenb"
? route?.midPrice.toSignificant(6) />
: route?.midPrice.invert().toSignificant(6)}{' '} </>
{pair?.token1.symbol} )}
{route && (
<div style={{ padding: '10px 20px' }}>
<RowBetween>
Price:
<div>
1 {tokens[Field.TOKEN_A]?.symbol} = {route.midPrice.toSignificant(6)}{' '}
{tokens[Field.TOKEN_B]?.symbol}
</div>
</RowBetween>
<RowBetween>
<div />
<div>
1 {tokens[Field.TOKEN_B]?.symbol} = {route.midPrice.invert().toSignificant(6)}{' '}
{tokens[Field.TOKEN_A]?.symbol}
</div>
</RowBetween>
</div> </div>
</RowBetween> )}
</div> <div style={{ position: 'relative' }}>
<div style={{ position: 'relative' }}> {!account ? (
<ButtonPrimary <ButtonLight onClick={toggleWalletModal}>Connect Wallet</ButtonLight>
onClick={() => { ) : (
setShowConfirm(true) <ButtonError
}} onClick={() => {
disabled={!isValid} setShowConfirm(true)
> }}
<Text fontSize={20} fontWeight={500}> disabled={!isValid}
{inputError || outputError || poolTokenError || generalError || 'Remove'} error={!isValid && !!parsedAmounts[Field.TOKEN_A] && !!parsedAmounts[Field.TOKEN_B]}
</Text> >
</ButtonPrimary> <Text fontSize={20} fontWeight={500}>
<FixedBottom> {error || 'Remove'}
<PositionCard pair={pair} minimal={true} /> </Text>
</FixedBottom> </ButtonError>
</div> )}
</div>
</AutoColumn>
</Wrapper>
</AppBody>
{isValid ? (
<AdvancedSwapDetailsDropdown
rawSlippage={allowedSlippage}
deadline={deadline}
showAdvanced={showAdvanced}
setShowAdvanced={setShowAdvanced}
setDeadline={setDeadline}
setRawSlippage={setAllowedSlippage}
/>
) : null}
{pair ? (
<AutoColumn style={{ minWidth: '20rem', marginTop: '1rem' }}>
<PositionCard pair={pair} minimal={true} />
</AutoColumn> </AutoColumn>
</Wrapper> ) : null}
</AppBody> </>
) )
} }
import { createAction } from '@reduxjs/toolkit'
export enum Field {
LIQUIDITY_PERCENT = 'LIQUIDITY_PERCENT',
LIQUIDITY = 'LIQUIDITY',
TOKEN_A = 'TOKEN_A',
TOKEN_B = 'TOKEN_B'
}
export const typeInput = createAction<{ field: Field; typedValue: string }>('typeInputBurn')
import { useEffect, useCallback, useMemo } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { useActiveWeb3React } from '../../hooks'
import { AppDispatch, AppState } from '../index'
import { Field, typeInput } from './actions'
import { setDefaultsFromURLMatchParams } from '../mint/actions'
import { useTokenByAddressAndAutomaticallyAdd } from '../../hooks/Tokens'
import { Token, Pair, TokenAmount, Percent, JSBI, Route } from '@uniswap/sdk'
import { usePair } from '../../data/Reserves'
import { useTokenBalances } from '../wallet/hooks'
import { tryParseAmount } from '../swap/hooks'
import { useTotalSupply } from '../../data/TotalSupply'
const ZERO = JSBI.BigInt(0)
export function useBurnState(): AppState['burn'] {
return useSelector<AppState, AppState['burn']>(state => state.burn)
}
export function useDerivedBurnInfo(): {
tokens: { [field in Extract<Field, Field.TOKEN_A | Field.TOKEN_B>]?: Token }
pair?: Pair | null
route?: Route
parsedAmounts: {
[Field.LIQUIDITY_PERCENT]: Percent
[Field.LIQUIDITY]?: TokenAmount
[Field.TOKEN_A]?: TokenAmount
[Field.TOKEN_B]?: TokenAmount
}
error?: string
} {
const { account } = useActiveWeb3React()
const {
independentField,
typedValue,
[Field.TOKEN_A]: { address: tokenAAddress },
[Field.TOKEN_B]: { address: tokenBAddress }
} = useBurnState()
// tokens
const tokenA = useTokenByAddressAndAutomaticallyAdd(tokenAAddress)
const tokenB = useTokenByAddressAndAutomaticallyAdd(tokenBAddress)
const tokens: { [field in Extract<Field, Field.TOKEN_A | Field.TOKEN_B>]?: Token } = useMemo(
() => ({
[Field.TOKEN_A]: tokenA,
[Field.TOKEN_B]: tokenB
}),
[tokenA, tokenB]
)
// pair + totalsupply
const pair = usePair(tokens[Field.TOKEN_A], tokens[Field.TOKEN_B])
const noLiquidity =
pair === null || (!!pair && JSBI.equal(pair.reserve0.raw, ZERO) && JSBI.equal(pair.reserve1.raw, ZERO))
// route
const route =
!noLiquidity && pair && tokens[Field.TOKEN_A] ? new Route([pair], tokens[Field.TOKEN_A] as Token) : undefined
// balances
const relevantTokenBalances = useTokenBalances(account ?? undefined, [pair?.liquidityToken])
const userLiquidity: undefined | TokenAmount = relevantTokenBalances?.[pair?.liquidityToken?.address ?? '']
// liquidity values
const totalSupply = useTotalSupply(pair?.liquidityToken)
const liquidityValues: { [field in Extract<Field, Field.TOKEN_A | Field.TOKEN_B>]?: TokenAmount } = {
[Field.TOKEN_A]:
pair &&
tokens[Field.TOKEN_A] &&
totalSupply &&
userLiquidity &&
// this condition is a short-circuit in the case where useTokenBalance updates sooner than useTotalSupply
JSBI.greaterThanOrEqual(totalSupply.raw, userLiquidity.raw)
? new TokenAmount(
tokens[Field.TOKEN_A] as Token,
pair.getLiquidityValue(tokens[Field.TOKEN_A] as Token, totalSupply, userLiquidity, false).raw
)
: undefined,
[Field.TOKEN_B]:
pair &&
tokens[Field.TOKEN_B] &&
totalSupply &&
userLiquidity &&
// this condition is a short-circuit in the case where useTokenBalance updates sooner than useTotalSupply
JSBI.greaterThanOrEqual(totalSupply.raw, userLiquidity.raw)
? new TokenAmount(
tokens[Field.TOKEN_B] as Token,
pair.getLiquidityValue(tokens[Field.TOKEN_B] as Token, totalSupply, userLiquidity, false).raw
)
: undefined
}
let percentToRemove: Percent = new Percent('0', '100')
// user specified a %
if (independentField === Field.LIQUIDITY_PERCENT) {
percentToRemove = new Percent(typedValue, '100')
}
// user specified a specific amount of liquidity tokens
else if (independentField === Field.LIQUIDITY) {
if (pair?.liquidityToken) {
const independentAmount = tryParseAmount(typedValue, pair.liquidityToken)
if (independentAmount && userLiquidity && !independentAmount.greaterThan(userLiquidity)) {
percentToRemove = new Percent(independentAmount.raw, userLiquidity.raw)
}
}
}
// user specified a specific amount of token a or b
else {
if (tokens[independentField]) {
const independentAmount = tryParseAmount(typedValue, tokens[independentField])
if (
independentAmount &&
liquidityValues[independentField] &&
!independentAmount.greaterThan(liquidityValues[independentField] as TokenAmount)
) {
percentToRemove = new Percent(independentAmount.raw, (liquidityValues[independentField] as TokenAmount).raw)
}
}
}
const parsedAmounts: {
[Field.LIQUIDITY_PERCENT]: Percent
[Field.LIQUIDITY]?: TokenAmount
[Field.TOKEN_A]?: TokenAmount
[Field.TOKEN_B]?: TokenAmount
} = {
[Field.LIQUIDITY_PERCENT]: percentToRemove,
[Field.LIQUIDITY]:
userLiquidity && percentToRemove && percentToRemove.greaterThan('0')
? new TokenAmount(userLiquidity.token, percentToRemove.multiply(userLiquidity.raw).quotient)
: undefined,
[Field.TOKEN_A]:
tokens[Field.TOKEN_A] && percentToRemove && percentToRemove.greaterThan('0') && liquidityValues[Field.TOKEN_A]
? new TokenAmount(
tokens[Field.TOKEN_A] as Token,
percentToRemove.multiply((liquidityValues[Field.TOKEN_A] as TokenAmount).raw).quotient
)
: undefined,
[Field.TOKEN_B]:
tokens[Field.TOKEN_B] && percentToRemove && percentToRemove.greaterThan('0') && liquidityValues[Field.TOKEN_B]
? new TokenAmount(
tokens[Field.TOKEN_B] as Token,
percentToRemove.multiply((liquidityValues[Field.TOKEN_B] as TokenAmount).raw).quotient
)
: undefined
}
let error: string | undefined
if (!account) {
error = 'Connect Wallet'
}
if (!parsedAmounts[Field.LIQUIDITY] || !parsedAmounts[Field.TOKEN_A] || !parsedAmounts[Field.TOKEN_B]) {
error = error ?? 'Enter an amount'
}
return { tokens, pair, route, parsedAmounts, error }
}
export function useBurnActionHandlers(): {
onUserInput: (field: Field, typedValue: string) => void
} {
const dispatch = useDispatch<AppDispatch>()
const onUserInput = useCallback(
(field: Field, typedValue: string) => {
dispatch(typeInput({ field, typedValue }))
},
[dispatch]
)
return {
onUserInput
}
}
// updates the burn state to use the appropriate tokens, given the route
export function useDefaultsFromURLMatchParams(params: { [k: string]: string }) {
const { chainId } = useActiveWeb3React()
const dispatch = useDispatch<AppDispatch>()
useEffect(() => {
if (!chainId) return
dispatch(setDefaultsFromURLMatchParams({ chainId, params }))
}, [dispatch, chainId, params])
}
import { createReducer } from '@reduxjs/toolkit'
import { Field, typeInput } from './actions'
import { setDefaultsFromURLMatchParams } from '../mint/actions'
import { parseTokens } from '../mint/reducer'
export interface MintState {
readonly independentField: Field
readonly typedValue: string
readonly [Field.TOKEN_A]: {
readonly address: string
}
readonly [Field.TOKEN_B]: {
readonly address: string
}
}
const initialState: MintState = {
independentField: Field.LIQUIDITY_PERCENT,
typedValue: '0',
[Field.TOKEN_A]: {
address: ''
},
[Field.TOKEN_B]: {
address: ''
}
}
export default createReducer<MintState>(initialState, builder =>
builder
.addCase(setDefaultsFromURLMatchParams, (state, { payload: { chainId, params } }) => {
const tokens = parseTokens(chainId, params?.tokens ?? '')
return {
independentField: Field.LIQUIDITY_PERCENT,
typedValue: '0',
[Field.TOKEN_A]: {
address: tokens[0]
},
[Field.TOKEN_B]: {
address: tokens[1]
}
}
})
.addCase(typeInput, (state, { payload: { field, typedValue } }) => {
return {
...state,
independentField: field,
typedValue
}
})
)
...@@ -7,6 +7,7 @@ import transactions from './transactions/reducer' ...@@ -7,6 +7,7 @@ import transactions from './transactions/reducer'
import wallet from './wallet/reducer' import wallet from './wallet/reducer'
import swap from './swap/reducer' import swap from './swap/reducer'
import mint from './mint/reducer' import mint from './mint/reducer'
import burn from './burn/reducer'
import { updateVersion } from './user/actions' import { updateVersion } from './user/actions'
...@@ -19,7 +20,8 @@ const store = configureStore({ ...@@ -19,7 +20,8 @@ const store = configureStore({
transactions, transactions,
wallet, wallet,
swap, swap,
mint mint,
burn
}, },
middleware: [...getDefaultMiddleware(), save({ states: PERSISTED_KEYS })], middleware: [...getDefaultMiddleware(), save({ states: PERSISTED_KEYS })],
preloadedState: load({ states: PERSISTED_KEYS }) preloadedState: load({ states: PERSISTED_KEYS })
......
...@@ -28,7 +28,7 @@ const initialState: MintState = { ...@@ -28,7 +28,7 @@ const initialState: MintState = {
} }
} }
function parseTokens(chainId: number, tokens: string): string[] { export function parseTokens(chainId: number, tokens: string): string[] {
return ( return (
tokens tokens
// split by '-' // split by '-'
......
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