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', () => {
it('loads the two correct tokens', () => {
cy.visit('/remove/0xc778417E063141139Fce010982780140Aa0cD5Ab-0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85')
cy.get('#remove-liquidity-token0-symbol').should('contain.text', 'ETH')
cy.get('#remove-liquidity-token1-symbol').should('contain.text', 'MKR')
cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'ETH')
cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'MKR')
})
it('does not crash if ETH is duplicated', () => {
cy.visit('/remove/0xc778417E063141139Fce010982780140Aa0cD5Ab-0xc778417E063141139Fce010982780140Aa0cD5Ab')
cy.get('#remove-liquidity-token0-symbol').should('contain.text', 'ETH')
cy.get('#remove-liquidity-token1-symbol').should('not.contain.text', 'ETH')
cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'ETH')
cy.get('#remove-liquidity-tokenb-symbol').should('not.contain.text', 'ETH')
})
it('token not in storage is loaded', () => {
cy.visit('/remove/0xb290b2f9f8f108d03ff2af3ac5c8de6de31cdf6d-0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85')
cy.get('#remove-liquidity-token0-symbol').should('contain.text', 'SKL')
cy.get('#remove-liquidity-token1-symbol').should('contain.text', 'MKR')
cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'SKL')
cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'MKR')
})
})
......@@ -110,8 +110,8 @@ function NavigationTabs({ location: { pathname }, history }: RouteComponentProps
<QuestionHelper
text={
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.'
: '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.'
? '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.'
: '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>
......
import React, { useState, useEffect, useCallback } from 'react'
import React from 'react'
import Slider from '@material-ui/core/Slider'
import { withStyles } from '@material-ui/core/styles'
import { useDebounce } from '../../hooks'
const StyledSlider = withStyles({
root: {
......@@ -51,36 +50,12 @@ const StyledSlider = withStyles({
interface InputSliderProps {
value: number
onChange: (val: number) => void
override?: boolean
onChange: (value: number) => void
}
export default function InputSlider({ value, onChange, override }: InputSliderProps) {
const [internalVal, setInternalVal] = useState<number>(value)
const debouncedInternalValue = useDebounce(internalVal, 100)
const handleChange = useCallback(
(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}
/>
)
export default function InputSlider({ value, onChange }: InputSliderProps) {
function wrappedOnChange(_, value) {
onChange(value)
}
return <StyledSlider value={value} onChange={wrappedOnChange} aria-labelledby="input-slider" step={1} />
}
......@@ -151,7 +151,7 @@ export default function SlippageTabs({ rawSlippage, setRawSlippage, deadline, se
<TYPE.black fontWeight={400} fontSize={14} color={theme.text2}>
Set slippage tolerance
</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>
<SlippageSelector>
......@@ -227,7 +227,7 @@ export default function SlippageTabs({ rawSlippage, setRawSlippage, deadline, se
<TYPE.black fontSize={14} color={theme.text2}>
Deadline
</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 padding={'0 20px'}>
<OptionCustom style={{ width: '80px' }} tabIndex={-1}>
......
......@@ -29,13 +29,7 @@ function TradeSummary({ trade, allowedSlippage }: { trade: Trade; allowedSlippag
<TYPE.black fontSize={14} fontWeight={400} color={theme.text2}>
{isExactIn ? 'Minimum received' : 'Maximum sold'}
</TYPE.black>
<QuestionHelper
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.'
}
/>
<QuestionHelper text="Your transaction will revert if there is a large, unfavorable price movement before it is confirmed." />
</RowFixed>
<RowFixed>
<TYPE.black color={theme.text1} fontSize={14}>
......
......@@ -66,9 +66,9 @@ export default function SwapModalFooter({
<RowBetween>
<RowFixed>
<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>
<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>
<TYPE.black fontSize={14}>
......
......@@ -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`
padding-top: calc(10px + 2rem);
padding-bottom: 10px;
......
......@@ -4,12 +4,7 @@ import styled from 'styled-components'
export const Wrapper = styled.div`
position: relative;
`
export const FixedBottom = styled.div`
position: absolute;
top: 100px;
width: 100%;
margin-bottom: 80px;
`
export const ClickableText = styled(Text)`
:hover {
cursor: pointer;
......
import { splitSignature } from '@ethersproject/bytes'
import { Contract } from '@ethersproject/contracts'
import { parseUnits } from '@ethersproject/units'
import { JSBI, Percent, Route, Token, TokenAmount, WETH } from '@uniswap/sdk'
import React, { useCallback, useContext, useEffect, useReducer, useState } from 'react'
import { Percent, WETH } from '@uniswap/sdk'
import React, { useContext, useState } from 'react'
import { ArrowDown, Plus } from 'react-feather'
import ReactGA from 'react-ga'
import { RouteComponentProps } from 'react-router'
import { Text } from 'rebass'
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 { AutoColumn, ColumnCenter } from '../../components/Column'
import ConfirmationModal from '../../components/ConfirmationModal'
......@@ -19,353 +18,75 @@ import Row, { RowBetween, RowFixed } from '../../components/Row'
import Slider from '../../components/Slider'
import TokenLogo from '../../components/TokenLogo'
import { ROUTER_ADDRESS, DEFAULT_DEADLINE_FROM_NOW } from '../../constants'
import { usePair } from '../../data/Reserves'
import { useTotalSupply } from '../../data/TotalSupply'
import { ROUTER_ADDRESS, DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE } from '../../constants'
import { usePairContract, useActiveWeb3React } from '../../hooks'
import { useTokenByAddressAndAutomaticallyAdd } from '../../hooks/Tokens'
import { useTransactionAdder } from '../../state/transactions/hooks'
import { useTokenBalance } from '../../state/wallet/hooks'
import { TYPE } from '../../theme'
import { calculateGasMargin, calculateSlippageAmount, getRouterContract } from '../../utils'
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 { Dots } from '../../components/swap/styleds'
// denominated in bips
const ALLOWED_SLIPPAGE = 50
enum Field {
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
}
}
}
import { useDefaultsFromURLMatchParams, useBurnActionHandlers } from '../../state/burn/hooks'
import { useDerivedBurnInfo, useBurnState } from '../../state/burn/hooks'
import AdvancedSwapDetailsDropdown from '../../components/swap/AdvancedSwapDetailsDropdown'
import { Field } from '../../state/burn/actions'
import { useWalletModalToggle } from '../../state/application/hooks'
export default function RemoveLiquidity({ match: { params } }: RouteComponentProps<{ tokens: string }>) {
const [token0, token1] = params.tokens.split('-')
useDefaultsFromURLMatchParams(params)
const { account, chainId, library } = useActiveWeb3React()
const theme = useContext(ThemeContext)
const [showConfirm, setShowConfirm] = useState<boolean>(false)
const [showAdvanced, setShowAdvanced] = useState<boolean>(false)
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
}
// toggle wallet when disconnected
const toggleWalletModal = useWalletModalToggle()
const route: Route = pair
? new Route([pair], independentField !== Field.LIQUIDITY ? tokens[independentField] : tokens[Field.TOKEN1])
: undefined
// update input value when user types
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()
)
}
// burn state
const { independentField, typedValue } = useBurnState()
const { tokens, pair, route, parsedAmounts, error } = useDerivedBurnInfo()
const { onUserInput } = useBurnActionHandlers()
const isValid = !error
// check if the user has approved router to withdraw their LP tokens
const [approval, approveCallback] = useApproveCallback(parsedAmounts[Field.LIQUIDITY], ROUTER_ADDRESS)
// modal and loading
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
const slippageAdjustedAmounts = {
[Field.TOKEN0]:
tokens[Field.TOKEN0] && parsedAmounts[Field.TOKEN0]
? 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
}
// txn values
const [txHash, setTxHash] = useState<string>('')
const [deadline, setDeadline] = useState<number>(DEFAULT_DEADLINE_FROM_NOW)
const [allowedSlippage, setAllowedSlippage] = useState<number>(INITIAL_ALLOWED_SLIPPAGE)
// get formatted amounts
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]:
independentField === Field.LIQUIDITY
? typedValue
: parsedAmounts[Field.LIQUIDITY]
? parsedAmounts[Field.LIQUIDITY].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)
: ''
independentField === Field.LIQUIDITY ? typedValue : parsedAmounts[Field.LIQUIDITY]?.toSignificant(6) ?? '',
[Field.TOKEN_A]:
independentField === Field.TOKEN_A ? typedValue : parsedAmounts[Field.TOKEN_A]?.toSignificant(6) ?? '',
[Field.TOKEN_B]:
independentField === Field.TOKEN_B ? typedValue : parsedAmounts[Field.TOKEN_B]?.toSignificant(6) ?? ''
}
const onMax = () => {
onUserInput(Field.LIQUIDITY, userLiquidity.toExact())
}
const atMaxAmount = parsedAmounts[Field.LIQUIDITY_PERCENT]?.equalTo(new Percent('1'))
const atMaxAmount =
!!userLiquidity && !!parsedAmounts[Field.LIQUIDITY]
? 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)
}
// pair contract
const pairContract: Contract = usePairContract(pair?.liquidityToken?.address)
if (formattedAmounts[Field.TOKEN1] === '') {
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()
// allowance handling
const [signatureData, setSignatureData] = useState<{ v: number; r: string; s: string; deadline: number }>(null)
const [attemptedRemoval, setAttemptedRemoval] = useState(false) // clicked confirm
const [pendingConfirmation, setPendingConfirmation] = useState(true) // waiting for
const [approval, approveCallback] = useApproveCallback(parsedAmounts[Field.LIQUIDITY], ROUTER_ADDRESS)
async function onAttemptToApprove() {
// try to gather a signature for permission
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 = [
{ name: 'name', type: 'string' },
......@@ -424,19 +145,28 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
function resetModalState() {
setSignatureData(null)
setAttemptedRemoval(false)
setAttemptingTxn(false)
setPendingConfirmation(true)
}
// tx sending
const addTransaction = useTransactionAdder()
async function onRemove() {
setAttemptedRemoval(true)
setAttemptingTxn(true)
const router = getRouterContract(chainId, library, account)
const token0IsETH = tokens[Field.TOKEN0].equals(WETH[chainId])
const oneTokenIsETH = token0IsETH || tokens[Field.TOKEN1].equals(WETH[chainId])
const amountsMin = {
[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
if (approval === ApprovalState.APPROVED) {
// removeLiquidityETH
......@@ -444,12 +174,12 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
estimate = router.estimateGas.removeLiquidityETH
method = router.removeLiquidityETH
args = [
tokens[token0IsETH ? Field.TOKEN1 : Field.TOKEN0].address,
tokens[tokenBIsETH ? Field.TOKEN_A : Field.TOKEN_B].address,
parsedAmounts[Field.LIQUIDITY].raw.toString(),
slippageAdjustedAmounts[token0IsETH ? Field.TOKEN1 : Field.TOKEN0].raw.toString(),
slippageAdjustedAmounts[token0IsETH ? Field.TOKEN0 : Field.TOKEN1].raw.toString(),
amountsMin[tokenBIsETH ? Field.TOKEN_A : Field.TOKEN_B].toString(),
amountsMin[tokenBIsETH ? Field.TOKEN_B : Field.TOKEN_A].toString(),
account,
Math.ceil(Date.now() / 1000) + DEFAULT_DEADLINE_FROM_NOW
deadlineFromNow
]
}
// removeLiquidity
......@@ -457,13 +187,13 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
estimate = router.estimateGas.removeLiquidity
method = router.removeLiquidity
args = [
tokens[Field.TOKEN0].address,
tokens[Field.TOKEN1].address,
tokens[Field.TOKEN_A].address,
tokens[Field.TOKEN_B].address,
parsedAmounts[Field.LIQUIDITY].raw.toString(),
slippageAdjustedAmounts[Field.TOKEN0].raw.toString(),
slippageAdjustedAmounts[Field.TOKEN1].raw.toString(),
amountsMin[Field.TOKEN_A].toString(),
amountsMin[Field.TOKEN_B].toString(),
account,
Math.ceil(Date.now() / 1000) + DEFAULT_DEADLINE_FROM_NOW
deadlineFromNow
]
}
}
......@@ -474,10 +204,10 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
estimate = router.estimateGas.removeLiquidityETHWithPermit
method = router.removeLiquidityETHWithPermit
args = [
tokens[token0IsETH ? Field.TOKEN1 : Field.TOKEN0].address,
tokens[tokenBIsETH ? Field.TOKEN_A : Field.TOKEN_B].address,
parsedAmounts[Field.LIQUIDITY].raw.toString(),
slippageAdjustedAmounts[token0IsETH ? Field.TOKEN1 : Field.TOKEN0].raw.toString(),
slippageAdjustedAmounts[token0IsETH ? Field.TOKEN0 : Field.TOKEN1].raw.toString(),
amountsMin[tokenBIsETH ? Field.TOKEN_A : Field.TOKEN_B].toString(),
amountsMin[tokenBIsETH ? Field.TOKEN_B : Field.TOKEN_A].toString(),
account,
signatureData.deadline,
false,
......@@ -491,11 +221,11 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
estimate = router.estimateGas.removeLiquidityWithPermit
method = router.removeLiquidityWithPermit
args = [
tokens[Field.TOKEN0].address,
tokens[Field.TOKEN1].address,
tokens[Field.TOKEN_A].address,
tokens[Field.TOKEN_B].address,
parsedAmounts[Field.LIQUIDITY].raw.toString(),
slippageAdjustedAmounts[Field.TOKEN0].raw.toString(),
slippageAdjustedAmounts[Field.TOKEN1].raw.toString(),
amountsMin[Field.TOKEN_A].toString(),
amountsMin[Field.TOKEN_B].toString(),
account,
signatureData.deadline,
false,
......@@ -513,23 +243,25 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
method(...args, {
gasLimit: calculateGasMargin(estimatedGasLimit)
}).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, {
summary:
'Remove ' +
parsedAmounts[Field.TOKEN0]?.toSignificant(3) +
parsedAmounts[Field.TOKEN_A]?.toSignificant(3) +
' ' +
tokens[Field.TOKEN0]?.symbol +
tokens[Field.TOKEN_A]?.symbol +
' 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
console.error(e)
resetModalState()
setShowConfirm(false)
setShowAdvanced(false)
})
}
......@@ -545,12 +278,12 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
<AutoColumn gap={'md'} style={{ marginTop: '20px' }}>
<RowBetween align="flex-end">
<Text fontSize={24} fontWeight={500}>
{!!parsedAmounts[Field.TOKEN0] && parsedAmounts[Field.TOKEN0].toSignificant(6)}
{parsedAmounts[Field.TOKEN_A]?.toSignificant(6)}
</Text>
<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' }}>
{tokens[Field.TOKEN0]?.symbol || ''}
{tokens[Field.TOKEN_A]?.symbol}
</Text>
</RowFixed>
</RowBetween>
......@@ -558,23 +291,20 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
<Plus size="16" color={theme.text2} />
</RowFixed>
<RowBetween align="flex-end">
<Text fontSize={24} fontWeight={600}>
{!!parsedAmounts[Field.TOKEN1] && parsedAmounts[Field.TOKEN1].toSignificant(6)}
<Text fontSize={24} fontWeight={500}>
{parsedAmounts[Field.TOKEN_B]?.toSignificant(6)}
</Text>
<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' }}>
{tokens[Field.TOKEN1]?.symbol || ''}
{tokens[Field.TOKEN_B]?.symbol}
</Text>
</RowFixed>
</RowBetween>
<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)} ${
tokens[Field.TOKEN0]?.symbol
} and ${slippageAdjustedAmounts[Field.TOKEN1]?.toSignificant(6)} ${
tokens[Field.TOKEN1]?.symbol
} or the transaction will revert.`}
{`Output is estimated. If the price changes by more than ${allowedSlippage /
100}% your transaction will revert.`}
</TYPE.italic>
</AutoColumn>
)
......@@ -585,12 +315,12 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
<>
<RowBetween>
<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>
<RowFixed>
<DoubleLogo
a0={tokens[Field.TOKEN0]?.address || ''}
a1={tokens[Field.TOKEN1]?.address || ''}
a0={tokens[Field.TOKEN_A]?.address || ''}
a1={tokens[Field.TOKEN_B]?.address || ''}
margin={true}
/>
<Text fontWeight={500} fontSize={16}>
......@@ -598,16 +328,25 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
</Text>
</RowFixed>
</RowBetween>
<RowBetween>
<Text color={theme.text2} fontWeight={500} fontSize={16}>
Price
</Text>
<Text fontWeight={500} fontSize={16} color={theme.text1}>
{`1 ${tokens[Field.TOKEN1]?.symbol} = ${route?.midPrice && route.midPrice.adjusted.toSignificant(6)} ${
tokens[Field.TOKEN0]?.symbol
}`}
</Text>
</RowBetween>
{route && (
<>
<RowBetween>
<Text color={theme.text2} fontWeight={500} fontSize={16}>
Price
</Text>
<Text fontWeight={500} fontSize={16} color={theme.text1}>
1 {tokens[Field.TOKEN_A]?.symbol} = {route.midPrice.toSignificant(6)} {tokens[Field.TOKEN_B]?.symbol}
</Text>
</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">
<ButtonConfirmed
onClick={onAttemptToApprove}
......@@ -640,178 +379,211 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
)
}
const pendingText = `Removing ${parsedAmounts[Field.TOKEN0]?.toSignificant(6)} ${
tokens[Field.TOKEN0]?.symbol
} and ${parsedAmounts[Field.TOKEN1]?.toSignificant(6)} ${tokens[Field.TOKEN1]?.symbol}`
const pendingText = `Removing ${parsedAmounts[Field.TOKEN_A]?.toSignificant(6)} ${
tokens[Field.TOKEN_A]?.symbol
} and ${parsedAmounts[Field.TOKEN_B]?.toSignificant(6)} ${tokens[Field.TOKEN_B]?.symbol}`
return (
<AppBody>
<Wrapper>
<ConfirmationModal
isOpen={showConfirm}
onDismiss={() => {
resetModalState()
setShowConfirm(false)
}}
attemptingTxn={attemptedRemoval}
pendingConfirmation={pendingConfirmation}
hash={txHash ? txHash : ''}
topContent={modalHeader}
bottomContent={modalBottom}
pendingText={pendingText}
title="You will receive"
/>
<AutoColumn gap="md">
<LightCard>
<AutoColumn gap="20px">
<RowBetween>
<Text fontWeight={500}>Amount</Text>
<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 && (
<>
<AppBody>
<Wrapper>
<ConfirmationModal
isOpen={showConfirm}
onDismiss={() => {
resetModalState()
setShowConfirm(false)
setShowAdvanced(false)
}}
attemptingTxn={attemptingTxn}
pendingConfirmation={pendingConfirmation}
hash={txHash ? txHash : ''}
topContent={modalHeader}
bottomContent={modalBottom}
pendingText={pendingText}
title="You will receive"
/>
<AutoColumn gap="md">
<LightCard>
<AutoColumn gap="20px">
<RowBetween>
<MaxButton onClick={() => handlePresetPercentage(25)} width="20%">
25%
</MaxButton>
<MaxButton onClick={() => handlePresetPercentage(50)} width="20%">
50%
</MaxButton>
<MaxButton onClick={() => handlePresetPercentage(75)} width="20%">
75%
</MaxButton>
<MaxButton onClick={() => handlePresetPercentage(100)} width="20%">
Max
</MaxButton>
<Text fontWeight={500}>Amount</Text>
<ClickableText
fontWeight={500}
onClick={() => {
setShowDetailed(!showDetailed)
}}
>
{showDetailed ? 'Simple' : 'Detailed'}
</ClickableText>
</RowBetween>
)}
</AutoColumn>
</LightCard>
{!showAdvanced && (
<>
<ColumnCenter>
<ArrowDown size="16" color={theme.text2} />
</ColumnCenter>{' '}
<LightCard>
<AutoColumn gap="10px">
<RowBetween>
<Text fontSize={24} fontWeight={500}>
{formattedAmounts[Field.TOKEN0] ? formattedAmounts[Field.TOKEN0] : '-'}
</Text>
<RowFixed>
<TokenLogo address={tokens[Field.TOKEN0]?.address} style={{ marginRight: '12px' }} />
<Text fontSize={24} fontWeight={500} id="remove-liquidity-token0-symbol">
{tokens[Field.TOKEN0]?.symbol}
<Row style={{ alignItems: 'flex-end' }}>
<Text fontSize={72} fontWeight={500}>
{formattedAmounts[Field.LIQUIDITY_PERCENT]}%
</Text>
</Row>
{!showDetailed && (
<>
<Slider
value={Number.parseInt(parsedAmounts[Field.LIQUIDITY_PERCENT].toFixed(0))}
onChange={value => {
onUserInput(Field.LIQUIDITY_PERCENT, value.toString())
}}
/>
<RowBetween>
<MaxButton onClick={() => onUserInput(Field.LIQUIDITY_PERCENT, '25')} width="20%">
25%
</MaxButton>
<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>
</RowFixed>
</RowBetween>
<RowBetween>
<Text fontSize={24} fontWeight={500}>
{formattedAmounts[Field.TOKEN1] ? formattedAmounts[Field.TOKEN1] : '-'}
</Text>
<RowFixed>
<TokenLogo address={tokens[Field.TOKEN1]?.address} style={{ marginRight: '12px' }} />
<Text fontSize={24} fontWeight={500} id="remove-liquidity-token1-symbol">
{tokens[Field.TOKEN1]?.symbol}
<RowFixed>
<TokenLogo address={tokens[Field.TOKEN_A]?.address} style={{ marginRight: '12px' }} />
<Text fontSize={24} fontWeight={500} id="remove-liquidity-tokena-symbol">
{tokens[Field.TOKEN_A]?.symbol}
</Text>
</RowFixed>
</RowBetween>
<RowBetween>
<Text fontSize={24} fontWeight={500}>
{formattedAmounts[Field.TOKEN_B] || '-'}
</Text>
</RowFixed>
</RowBetween>
</AutoColumn>
</LightCard>
</>
)}
{showAdvanced && (
<>
<CurrencyInputPanel
field={Field.LIQUIDITY}
value={formattedAmounts[Field.LIQUIDITY]}
onUserInput={onUserInput}
onMax={onMax}
showMaxButton={!atMaxAmount}
disableTokenSelect
token={pair?.liquidityToken}
isExchange={true}
pair={pair}
id="liquidity-amount"
/>
<ColumnCenter>
<ArrowDown size="16" color={theme.text2} />
</ColumnCenter>
<CurrencyInputPanel
field={Field.TOKEN0}
value={formattedAmounts[Field.TOKEN0]}
onUserInput={onUserInput}
onMax={onMax}
showMaxButton={!atMaxAmount}
token={tokens[Field.TOKEN0]}
label={'Output'}
disableTokenSelect
id="remove-liquidity-token0"
/>
<ColumnCenter>
<Plus size="16" color={theme.text2} />
</ColumnCenter>
<CurrencyInputPanel
field={Field.TOKEN1}
value={formattedAmounts[Field.TOKEN1]}
onUserInput={onUserInput}
onMax={onMax}
showMaxButton={!atMaxAmount}
token={tokens[Field.TOKEN1]}
label={'Output'}
disableTokenSelect
id="remove-liquidity-token1"
/>
</>
)}
<div style={{ padding: '10px 20px' }}>
<RowBetween>
Price:
<div>
1 {pair?.token0.symbol} ={' '}
{independentField === Field.TOKEN0 || independentField === Field.LIQUIDITY
? route?.midPrice.toSignificant(6)
: route?.midPrice.invert().toSignificant(6)}{' '}
{pair?.token1.symbol}
<RowFixed>
<TokenLogo address={tokens[Field.TOKEN_B]?.address} style={{ marginRight: '12px' }} />
<Text fontSize={24} fontWeight={500} id="remove-liquidity-tokenb-symbol">
{tokens[Field.TOKEN_B]?.symbol}
</Text>
</RowFixed>
</RowBetween>
</AutoColumn>
</LightCard>
</>
)}
{showDetailed && (
<>
<CurrencyInputPanel
field={Field.LIQUIDITY}
value={formattedAmounts[Field.LIQUIDITY]}
onUserInput={onUserInput}
onMax={() => {
onUserInput(Field.LIQUIDITY_PERCENT, '100')
}}
showMaxButton={!atMaxAmount}
disableTokenSelect
token={pair?.liquidityToken}
isExchange={true}
pair={pair}
id="liquidity-amount"
/>
<ColumnCenter>
<ArrowDown size="16" color={theme.text2} />
</ColumnCenter>
<CurrencyInputPanel
hideBalance={true}
field={Field.TOKEN_A}
value={formattedAmounts[Field.TOKEN_A]}
onUserInput={onUserInput}
onMax={() => onUserInput(Field.LIQUIDITY_PERCENT, '100')}
showMaxButton={!atMaxAmount}
token={tokens[Field.TOKEN_A]}
label={'Output'}
disableTokenSelect
id="remove-liquidity-tokena"
/>
<ColumnCenter>
<Plus size="16" color={theme.text2} />
</ColumnCenter>
<CurrencyInputPanel
hideBalance={true}
field={Field.TOKEN_B}
value={formattedAmounts[Field.TOKEN_B]}
onUserInput={onUserInput}
onMax={() => onUserInput(Field.LIQUIDITY_PERCENT, '100')}
showMaxButton={!atMaxAmount}
token={tokens[Field.TOKEN_B]}
label={'Output'}
disableTokenSelect
id="remove-liquidity-tokenb"
/>
</>
)}
{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>
</RowBetween>
</div>
<div style={{ position: 'relative' }}>
<ButtonPrimary
onClick={() => {
setShowConfirm(true)
}}
disabled={!isValid}
>
<Text fontSize={20} fontWeight={500}>
{inputError || outputError || poolTokenError || generalError || 'Remove'}
</Text>
</ButtonPrimary>
<FixedBottom>
<PositionCard pair={pair} minimal={true} />
</FixedBottom>
</div>
)}
<div style={{ position: 'relative' }}>
{!account ? (
<ButtonLight onClick={toggleWalletModal}>Connect Wallet</ButtonLight>
) : (
<ButtonError
onClick={() => {
setShowConfirm(true)
}}
disabled={!isValid}
error={!isValid && !!parsedAmounts[Field.TOKEN_A] && !!parsedAmounts[Field.TOKEN_B]}
>
<Text fontSize={20} fontWeight={500}>
{error || 'Remove'}
</Text>
</ButtonError>
)}
</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>
</Wrapper>
</AppBody>
) : null}
</>
)
}
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'
import wallet from './wallet/reducer'
import swap from './swap/reducer'
import mint from './mint/reducer'
import burn from './burn/reducer'
import { updateVersion } from './user/actions'
......@@ -19,7 +20,8 @@ const store = configureStore({
transactions,
wallet,
swap,
mint
mint,
burn
},
middleware: [...getDefaultMiddleware(), save({ states: PERSISTED_KEYS })],
preloadedState: load({ states: PERSISTED_KEYS })
......
......@@ -28,7 +28,7 @@ const initialState: MintState = {
}
}
function parseTokens(chainId: number, tokens: string): string[] {
export function parseTokens(chainId: number, tokens: string): string[] {
return (
tokens
// 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