Add liquidity callback (#830)
* 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 * disable token selection in mint clear input between pairs
This commit is contained in:
parent
4b57059353
commit
b2f0236ee8
@ -1,19 +1,19 @@
|
||||
describe('Add Liquidity', () => {
|
||||
it('loads the two correct tokens', () => {
|
||||
cy.visit('/add/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85-0xc778417E063141139Fce010982780140Aa0cD5Ab')
|
||||
cy.get('#add-liquidity-input-token0 .token-symbol-container').should('contain.text', 'MKR')
|
||||
cy.get('#add-liquidity-input-token1 .token-symbol-container').should('contain.text', 'ETH')
|
||||
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'MKR')
|
||||
cy.get('#add-liquidity-input-tokenb .token-symbol-container').should('contain.text', 'ETH')
|
||||
})
|
||||
|
||||
it('does not crash if ETH is duplicated', () => {
|
||||
cy.visit('/add/0xc778417E063141139Fce010982780140Aa0cD5Ab-0xc778417E063141139Fce010982780140Aa0cD5Ab')
|
||||
cy.get('#add-liquidity-input-token0 .token-symbol-container').should('contain.text', 'ETH')
|
||||
cy.get('#add-liquidity-input-token1 .token-symbol-container').should('not.contain.text', 'ETH')
|
||||
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'ETH')
|
||||
cy.get('#add-liquidity-input-tokenb .token-symbol-container').should('not.contain.text', 'ETH')
|
||||
})
|
||||
|
||||
it('token not in storage is loaded', () => {
|
||||
cy.visit('/add/0xb290b2f9f8f108d03ff2af3ac5c8de6de31cdf6d-0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85')
|
||||
cy.get('#add-liquidity-input-token0 .token-symbol-container').should('contain.text', 'SKL')
|
||||
cy.get('#add-liquidity-input-token1 .token-symbol-container').should('contain.text', 'MKR')
|
||||
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'SKL')
|
||||
cy.get('#add-liquidity-input-tokenb .token-symbol-container').should('contain.text', 'MKR')
|
||||
})
|
||||
})
|
||||
|
@ -1,22 +1,22 @@
|
||||
import React, { useState, useEffect, useRef, useCallback, useContext } from 'react'
|
||||
import React, { useState, useRef, useContext } from 'react'
|
||||
import styled, { ThemeContext } from 'styled-components'
|
||||
|
||||
import QuestionHelper from '../QuestionHelper'
|
||||
import { Text } from 'rebass'
|
||||
import { TYPE } from '../../theme'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { RowBetween, RowFixed } from '../Row'
|
||||
|
||||
import { darken } from 'polished'
|
||||
import { useDebounce } from '../../hooks'
|
||||
|
||||
const WARNING_TYPE = Object.freeze({
|
||||
none: 'none',
|
||||
emptyInput: 'emptyInput',
|
||||
invalidEntryBound: 'invalidEntryBound',
|
||||
riskyEntryHigh: 'riskyEntryHigh',
|
||||
riskyEntryLow: 'riskyEntryLow'
|
||||
})
|
||||
enum SlippageError {
|
||||
InvalidInput = 'InvalidInput',
|
||||
RiskyLow = 'RiskyLow',
|
||||
RiskyHigh = 'RiskyHigh'
|
||||
}
|
||||
|
||||
enum DeadlineError {
|
||||
InvalidInput = 'InvalidInput'
|
||||
}
|
||||
|
||||
const FancyButton = styled.button`
|
||||
color: ${({ theme }) => theme.text1};
|
||||
@ -46,7 +46,7 @@ const Option = styled(FancyButton)<{ active: boolean }>`
|
||||
color: ${({ active, theme }) => (active ? theme.white : theme.text1)};
|
||||
`
|
||||
|
||||
const Input = styled.input<{ active?: boolean }>`
|
||||
const Input = styled.input`
|
||||
background: ${({ theme }) => theme.bg1};
|
||||
flex-grow: 1;
|
||||
font-size: 12px;
|
||||
@ -56,15 +56,8 @@ const Input = styled.input<{ active?: boolean }>`
|
||||
&::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
color: ${({ active, theme, color }) => (color === 'red' ? theme.red1 : active ? 'initial' : theme.text1)};
|
||||
cursor: ${({ active }) => (active ? 'initial' : 'inherit')};
|
||||
text-align: ${({ active }) => (active ? 'right' : 'left')};
|
||||
`
|
||||
|
||||
const BottomError = styled(Text)<{ show?: boolean }>`
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
padding-top: ${({ show }) => (show ? '12px' : '')};
|
||||
color: ${({ theme, color }) => (color === 'red' ? theme.red1 : theme.text1)};
|
||||
text-align: right;
|
||||
`
|
||||
|
||||
const OptionCustom = styled(FancyButton)<{ active?: boolean; warning?: boolean }>`
|
||||
@ -89,12 +82,6 @@ const SlippageSelector = styled.div`
|
||||
padding: 0 20px;
|
||||
`
|
||||
|
||||
const Percent = styled.div`
|
||||
color: ${({ color, theme }) => (color === 'faded' ? theme.bg1 : color === 'red' ? theme.red1 : 'inherit')};
|
||||
font-size: 0, 8rem;
|
||||
flex-grow: 0;
|
||||
`
|
||||
|
||||
export interface SlippageTabsProps {
|
||||
rawSlippage: number
|
||||
setRawSlippage: (rawSlippage: number) => void
|
||||
@ -102,235 +89,139 @@ export interface SlippageTabsProps {
|
||||
setDeadline: (deadline: number) => void
|
||||
}
|
||||
|
||||
export default function SlippageTabs({ setRawSlippage, rawSlippage, deadline, setDeadline }: SlippageTabsProps) {
|
||||
export default function SlippageTabs({ rawSlippage, setRawSlippage, deadline, setDeadline }: SlippageTabsProps) {
|
||||
const theme = useContext(ThemeContext)
|
||||
const [activeIndex, setActiveIndex] = useState(2)
|
||||
|
||||
const [warningType, setWarningType] = useState(WARNING_TYPE.none)
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>()
|
||||
|
||||
const [userInput, setUserInput] = useState('')
|
||||
const debouncedInput = useDebounce(userInput, 150)
|
||||
const [slippageInput, setSlippageInput] = useState('')
|
||||
const [deadlineInput, setDeadlineInput] = useState('')
|
||||
|
||||
const [initialSlippage] = useState(rawSlippage)
|
||||
const slippageInputIsValid =
|
||||
slippageInput === '' || (rawSlippage / 100).toFixed(2) === Number.parseFloat(slippageInput).toFixed(2)
|
||||
const deadlineInputIsValid = deadlineInput === '' || (deadline / 60).toString() === deadlineInput
|
||||
|
||||
const [deadlineInput, setDeadlineInput] = useState(deadline / 60)
|
||||
|
||||
const updateSlippage = useCallback(
|
||||
newSlippage => {
|
||||
// round to 2 decimals to prevent ethers error
|
||||
const numParsed = newSlippage * 100
|
||||
|
||||
// set both slippage values in parents
|
||||
setRawSlippage(numParsed)
|
||||
},
|
||||
[setRawSlippage]
|
||||
)
|
||||
|
||||
const checkBounds = useCallback(
|
||||
slippageValue => {
|
||||
setWarningType(WARNING_TYPE.none)
|
||||
|
||||
if (slippageValue === '' || slippageValue === '.') {
|
||||
return setWarningType(WARNING_TYPE.emptyInput)
|
||||
let slippageError: SlippageError
|
||||
if (slippageInput !== '' && !slippageInputIsValid) {
|
||||
slippageError = SlippageError.InvalidInput
|
||||
} else if (slippageInputIsValid && rawSlippage < 50) {
|
||||
slippageError = SlippageError.RiskyLow
|
||||
} else if (slippageInputIsValid && rawSlippage > 500) {
|
||||
slippageError = SlippageError.RiskyHigh
|
||||
}
|
||||
|
||||
// check bounds and set errors
|
||||
if (Number(slippageValue) < 0 || Number(slippageValue) > 50) {
|
||||
return setWarningType(WARNING_TYPE.invalidEntryBound)
|
||||
}
|
||||
if (Number(slippageValue) >= 0 && Number(slippageValue) < 0.1) {
|
||||
setWarningType(WARNING_TYPE.riskyEntryLow)
|
||||
}
|
||||
if (Number(slippageValue) > 5) {
|
||||
setWarningType(WARNING_TYPE.riskyEntryHigh)
|
||||
}
|
||||
//update the actual slippage value in parent
|
||||
updateSlippage(Number(slippageValue))
|
||||
},
|
||||
[updateSlippage]
|
||||
)
|
||||
|
||||
function parseCustomDeadline(e) {
|
||||
const val = e.target.value
|
||||
const acceptableValues = [/^$/, /^\d+$/]
|
||||
if (acceptableValues.some(re => re.test(val))) {
|
||||
setDeadlineInput(val)
|
||||
setDeadline(val * 60)
|
||||
}
|
||||
}
|
||||
const setFromCustom = () => {
|
||||
setActiveIndex(4)
|
||||
inputRef.current.focus()
|
||||
// if there's a value, evaluate the bounds
|
||||
checkBounds(debouncedInput)
|
||||
let deadlineError: DeadlineError
|
||||
if (deadlineInput !== '' && !deadlineInputIsValid) {
|
||||
deadlineError = DeadlineError.InvalidInput
|
||||
}
|
||||
|
||||
// used for slippage presets
|
||||
const setFromFixed = useCallback(
|
||||
(index, slippage) => {
|
||||
// update slippage in parent, reset errors and input state
|
||||
updateSlippage(slippage)
|
||||
setWarningType(WARNING_TYPE.none)
|
||||
setActiveIndex(index)
|
||||
},
|
||||
[updateSlippage]
|
||||
)
|
||||
function parseCustomSlippage(event) {
|
||||
setSlippageInput(event.target.value)
|
||||
|
||||
useEffect(() => {
|
||||
switch (initialSlippage) {
|
||||
case 10:
|
||||
setFromFixed(1, 0.1)
|
||||
break
|
||||
case 50:
|
||||
setFromFixed(2, 0.5)
|
||||
break
|
||||
case 100:
|
||||
setFromFixed(3, 1)
|
||||
break
|
||||
default:
|
||||
// restrict to 2 decimal places
|
||||
const acceptableValues = [/^$/, /^\d{1,2}$/, /^\d{0,2}\.\d{0,2}$/]
|
||||
// if its within accepted decimal limit, update the input state
|
||||
if (acceptableValues.some(val => val.test('' + initialSlippage / 100))) {
|
||||
setUserInput('' + initialSlippage / 100)
|
||||
setActiveIndex(4)
|
||||
}
|
||||
}
|
||||
}, [initialSlippage, setFromFixed])
|
||||
let valueAsIntFromRoundedFloat: number
|
||||
try {
|
||||
valueAsIntFromRoundedFloat = Number.parseInt((Number.parseFloat(event.target.value) * 100).toString())
|
||||
} catch {}
|
||||
|
||||
// check that the theyve entered number and correct decimal
|
||||
const parseInput = e => {
|
||||
const input = e.target.value
|
||||
|
||||
// restrict to 2 decimal places
|
||||
const acceptableValues = [/^$/, /^\d{1,2}$/, /^\d{0,2}\.\d{0,2}$/]
|
||||
// if its within accepted decimal limit, update the input state
|
||||
if (acceptableValues.some(a => a.test(input))) {
|
||||
setUserInput(input)
|
||||
if (
|
||||
typeof valueAsIntFromRoundedFloat === 'number' &&
|
||||
!Number.isNaN(valueAsIntFromRoundedFloat) &&
|
||||
valueAsIntFromRoundedFloat < 5000
|
||||
) {
|
||||
setRawSlippage(valueAsIntFromRoundedFloat)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (activeIndex === 4) {
|
||||
checkBounds(debouncedInput)
|
||||
function parseCustomDeadline(event) {
|
||||
setDeadlineInput(event.target.value)
|
||||
|
||||
let valueAsInt: number
|
||||
try {
|
||||
valueAsInt = Number.parseInt(event.target.value) * 60
|
||||
} catch {}
|
||||
|
||||
if (typeof valueAsInt === 'number' && !Number.isNaN(valueAsInt) && valueAsInt > 0) {
|
||||
setDeadline(valueAsInt)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<RowFixed padding={'0 20px'}>
|
||||
<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." />
|
||||
</RowFixed>
|
||||
|
||||
<SlippageSelector>
|
||||
<RowBetween>
|
||||
<Option
|
||||
onClick={() => {
|
||||
setFromFixed(1, 0.1)
|
||||
setSlippageInput('')
|
||||
setRawSlippage(10)
|
||||
}}
|
||||
active={activeIndex === 1}
|
||||
active={rawSlippage === 10}
|
||||
>
|
||||
0.1%
|
||||
</Option>
|
||||
<Option
|
||||
onClick={() => {
|
||||
setFromFixed(2, 0.5)
|
||||
setSlippageInput('')
|
||||
setRawSlippage(50)
|
||||
}}
|
||||
active={activeIndex === 2}
|
||||
active={rawSlippage === 50}
|
||||
>
|
||||
0.5%
|
||||
</Option>
|
||||
<Option
|
||||
onClick={() => {
|
||||
setFromFixed(3, 1)
|
||||
setSlippageInput('')
|
||||
setRawSlippage(100)
|
||||
}}
|
||||
active={activeIndex === 3}
|
||||
active={rawSlippage === 100}
|
||||
>
|
||||
1%
|
||||
</Option>
|
||||
<OptionCustom
|
||||
active={activeIndex === 4}
|
||||
warning={
|
||||
warningType !== WARNING_TYPE.none &&
|
||||
warningType !== WARNING_TYPE.emptyInput &&
|
||||
warningType !== WARNING_TYPE.riskyEntryLow
|
||||
}
|
||||
onClick={() => {
|
||||
setFromCustom()
|
||||
}}
|
||||
>
|
||||
<OptionCustom active={![10, 50, 100].includes(rawSlippage)} warning={!slippageInputIsValid} tabIndex={-1}>
|
||||
<RowBetween>
|
||||
{!(warningType === WARNING_TYPE.none || warningType === WARNING_TYPE.emptyInput) && (
|
||||
<span
|
||||
role="img"
|
||||
aria-label="warning"
|
||||
style={{
|
||||
color:
|
||||
warningType !== WARNING_TYPE.none && warningType !== WARNING_TYPE.riskyEntryLow
|
||||
? 'red'
|
||||
: warningType === WARNING_TYPE.riskyEntryLow
|
||||
? '#F3841E'
|
||||
: ''
|
||||
}}
|
||||
>
|
||||
{!!slippageInput &&
|
||||
(slippageError === SlippageError.RiskyLow || slippageError === SlippageError.RiskyHigh) ? (
|
||||
<span role="img" aria-label="warning" style={{ color: '#F3841E' }}>
|
||||
⚠️
|
||||
</span>
|
||||
)}
|
||||
) : null}
|
||||
<Input
|
||||
tabIndex={-1}
|
||||
ref={inputRef}
|
||||
active={activeIndex === 4}
|
||||
placeholder={
|
||||
activeIndex === 4
|
||||
? !!userInput
|
||||
? ''
|
||||
: '0'
|
||||
: activeIndex !== 4 && userInput !== ''
|
||||
? userInput
|
||||
: 'Custom'
|
||||
}
|
||||
value={activeIndex === 4 ? userInput : ''}
|
||||
onChange={parseInput}
|
||||
color={
|
||||
warningType === WARNING_TYPE.emptyInput
|
||||
? ''
|
||||
: warningType !== WARNING_TYPE.none && warningType !== WARNING_TYPE.riskyEntryLow
|
||||
? 'red'
|
||||
: ''
|
||||
}
|
||||
placeholder={(rawSlippage / 100).toFixed(2)}
|
||||
value={slippageInput}
|
||||
onBlur={() => {
|
||||
parseCustomSlippage({ target: { value: (rawSlippage / 100).toFixed(2) } })
|
||||
}}
|
||||
onChange={parseCustomSlippage}
|
||||
color={!slippageInputIsValid ? 'red' : ''}
|
||||
/>
|
||||
<Percent
|
||||
color={
|
||||
activeIndex !== 4
|
||||
? 'faded'
|
||||
: warningType === WARNING_TYPE.riskyEntryHigh || warningType === WARNING_TYPE.invalidEntryBound
|
||||
? 'red'
|
||||
: ''
|
||||
}
|
||||
>
|
||||
%
|
||||
</Percent>
|
||||
</RowBetween>
|
||||
</OptionCustom>
|
||||
</RowBetween>
|
||||
<RowBetween>
|
||||
<BottomError
|
||||
show={activeIndex === 4}
|
||||
color={
|
||||
warningType === WARNING_TYPE.emptyInput
|
||||
? '#565A69'
|
||||
: warningType !== WARNING_TYPE.none && warningType !== WARNING_TYPE.riskyEntryLow
|
||||
? 'red'
|
||||
: warningType === WARNING_TYPE.riskyEntryLow
|
||||
? '#F3841E'
|
||||
: ''
|
||||
}
|
||||
{!!slippageError && (
|
||||
<RowBetween
|
||||
style={{
|
||||
fontSize: '14px',
|
||||
paddingTop: '7px',
|
||||
color: slippageError === SlippageError.InvalidInput ? 'red' : '#F3841E'
|
||||
}}
|
||||
>
|
||||
{warningType === WARNING_TYPE.emptyInput && 'Enter a slippage percentage'}
|
||||
{warningType === WARNING_TYPE.invalidEntryBound && 'Please select a value no greater than 50%'}
|
||||
{warningType === WARNING_TYPE.riskyEntryHigh && 'Your transaction may be frontrun'}
|
||||
{warningType === WARNING_TYPE.riskyEntryLow && 'Your transaction may fail'}
|
||||
</BottomError>
|
||||
{slippageError === SlippageError.InvalidInput
|
||||
? 'Enter a valid slippage percentage'
|
||||
: slippageError === SlippageError.RiskyLow
|
||||
? 'Your transaction may fail'
|
||||
: 'Your transaction may be frontrun'}
|
||||
</RowBetween>
|
||||
)}
|
||||
</SlippageSelector>
|
||||
|
||||
<AutoColumn gap="sm">
|
||||
<RowFixed padding={'0 20px'}>
|
||||
<TYPE.black fontSize={14} color={theme.text2}>
|
||||
@ -339,10 +230,13 @@ export default function SlippageTabs({ setRawSlippage, rawSlippage, deadline, se
|
||||
<QuestionHelper text="Deadline in minutes. If your transaction takes longer than this it will revert." />
|
||||
</RowFixed>
|
||||
<RowFixed padding={'0 20px'}>
|
||||
<OptionCustom style={{ width: '80px' }}>
|
||||
<OptionCustom style={{ width: '80px' }} tabIndex={-1}>
|
||||
<Input
|
||||
tabIndex={-1}
|
||||
placeholder={'' + deadlineInput}
|
||||
color={!!deadlineError ? 'red' : undefined}
|
||||
onBlur={() => {
|
||||
parseCustomDeadline({ target: { value: (deadline / 60).toString() } })
|
||||
}}
|
||||
placeholder={(deadline / 60).toString()}
|
||||
value={deadlineInput}
|
||||
onChange={parseCustomDeadline}
|
||||
/>
|
||||
|
@ -15,28 +15,14 @@ import FormattedPriceImpact from './FormattedPriceImpact'
|
||||
import TokenLogo from '../TokenLogo'
|
||||
import flatMap from 'lodash.flatmap'
|
||||
|
||||
export interface AdvancedSwapDetailsProps extends SlippageTabsProps {
|
||||
trade: Trade
|
||||
onDismiss: () => void
|
||||
}
|
||||
|
||||
export function AdvancedSwapDetails({ trade, onDismiss, ...slippageTabProps }: AdvancedSwapDetailsProps) {
|
||||
const { priceImpactWithoutFee, realizedLPFee } = computeTradePriceBreakdown(trade)
|
||||
function TradeSummary({ trade, allowedSlippage }: { trade: Trade; allowedSlippage: number }) {
|
||||
const theme = useContext(ThemeContext)
|
||||
const { priceImpactWithoutFee, realizedLPFee } = computeTradePriceBreakdown(trade)
|
||||
const isExactIn = trade.tradeType === TradeType.EXACT_INPUT
|
||||
const slippageAdjustedAmounts = computeSlippageAdjustedAmounts(trade, slippageTabProps.rawSlippage)
|
||||
const slippageAdjustedAmounts = computeSlippageAdjustedAmounts(trade, allowedSlippage)
|
||||
|
||||
return (
|
||||
<AutoColumn gap="md">
|
||||
<CursorPointer>
|
||||
<RowBetween onClick={onDismiss} padding={'8px 20px'}>
|
||||
<Text fontSize={16} color={theme.text2} fontWeight={500} style={{ userSelect: 'none' }}>
|
||||
Hide Advanced
|
||||
</Text>
|
||||
<ChevronUp color={theme.text2} />
|
||||
</RowBetween>
|
||||
</CursorPointer>
|
||||
<SectionBreak />
|
||||
<>
|
||||
<AutoColumn style={{ padding: '0 20px' }}>
|
||||
<RowBetween>
|
||||
<RowFixed>
|
||||
@ -83,16 +69,36 @@ export function AdvancedSwapDetails({ trade, onDismiss, ...slippageTabProps }: A
|
||||
</AutoColumn>
|
||||
|
||||
<SectionBreak />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export interface AdvancedSwapDetailsProps extends SlippageTabsProps {
|
||||
trade?: Trade
|
||||
onDismiss: () => void
|
||||
}
|
||||
|
||||
export function AdvancedSwapDetails({ trade, onDismiss, ...slippageTabProps }: AdvancedSwapDetailsProps) {
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
return (
|
||||
<AutoColumn gap="md">
|
||||
<CursorPointer>
|
||||
<RowBetween onClick={onDismiss} padding={'8px 20px'}>
|
||||
<Text fontSize={16} color={theme.text2} fontWeight={500} style={{ userSelect: 'none' }}>
|
||||
Hide Advanced
|
||||
</Text>
|
||||
<ChevronUp color={theme.text2} />
|
||||
</RowBetween>
|
||||
</CursorPointer>
|
||||
|
||||
<SectionBreak />
|
||||
|
||||
{trade && <TradeSummary trade={trade} allowedSlippage={slippageTabProps.rawSlippage} />}
|
||||
|
||||
<RowFixed padding={'0 20px'}>
|
||||
<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." />
|
||||
</RowFixed>
|
||||
<SlippageTabs {...slippageTabProps} />
|
||||
|
||||
{trade.route.path.length > 2 && (
|
||||
{trade?.route?.path?.length > 2 && (
|
||||
<AutoColumn style={{ padding: '0 20px' }}>
|
||||
<RowFixed>
|
||||
<TYPE.black fontSize={14} fontWeight={400} color={theme.text2}>
|
||||
|
@ -1,30 +1,23 @@
|
||||
import { Percent } from '@uniswap/sdk'
|
||||
import React, { useContext } from 'react'
|
||||
import { ChevronDown } from 'react-feather'
|
||||
import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import { CursorPointer } from '../../theme'
|
||||
import { warningServerity } from '../../utils/prices'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { RowBetween } from '../Row'
|
||||
import { AdvancedSwapDetails, AdvancedSwapDetailsProps } from './AdvancedSwapDetails'
|
||||
import { PriceSlippageWarningCard } from './PriceSlippageWarningCard'
|
||||
import { AdvancedDropwdown, FixedBottom } from './styleds'
|
||||
import { AdvancedDropdown } from './styleds'
|
||||
|
||||
export default function AdvancedSwapDetailsDropdown({
|
||||
priceImpactWithoutFee,
|
||||
showAdvanced,
|
||||
setShowAdvanced,
|
||||
...rest
|
||||
}: Omit<AdvancedSwapDetailsProps, 'onDismiss'> & {
|
||||
showAdvanced: boolean
|
||||
setShowAdvanced: (showAdvanced: boolean) => void
|
||||
priceImpactWithoutFee: Percent
|
||||
}) {
|
||||
const theme = useContext(ThemeContext)
|
||||
const severity = warningServerity(priceImpactWithoutFee)
|
||||
return (
|
||||
<AdvancedDropwdown>
|
||||
<AdvancedDropdown>
|
||||
{showAdvanced ? (
|
||||
<AdvancedSwapDetails {...rest} onDismiss={() => setShowAdvanced(false)} />
|
||||
) : (
|
||||
@ -37,11 +30,6 @@ export default function AdvancedSwapDetailsDropdown({
|
||||
</RowBetween>
|
||||
</CursorPointer>
|
||||
)}
|
||||
<FixedBottom>
|
||||
<AutoColumn gap="lg">
|
||||
{severity > 2 && <PriceSlippageWarningCard priceSlippage={priceImpactWithoutFee} />}
|
||||
</AutoColumn>
|
||||
</FixedBottom>
|
||||
</AdvancedDropwdown>
|
||||
</AdvancedDropdown>
|
||||
)
|
||||
}
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { Percent } from '@uniswap/sdk'
|
||||
import React from 'react'
|
||||
import { ONE_BIPS } from '../../constants'
|
||||
import { warningServerity } from '../../utils/prices'
|
||||
import { warningSeverity } from '../../utils/prices'
|
||||
import { ErrorText } from './styleds'
|
||||
|
||||
export default function FormattedPriceImpact({ priceImpact }: { priceImpact?: Percent }) {
|
||||
return (
|
||||
<ErrorText fontWeight={500} fontSize={14} severity={warningServerity(priceImpact)}>
|
||||
<ErrorText fontWeight={500} fontSize={14} severity={warningSeverity(priceImpact)}>
|
||||
{priceImpact?.lessThan(ONE_BIPS) ? '<0.01%' : `${priceImpact?.toFixed(2)}%` ?? '-'}
|
||||
</ErrorText>
|
||||
)
|
||||
|
@ -28,19 +28,16 @@ export const FixedBottom = styled.div`
|
||||
margin-bottom: 40px;
|
||||
`
|
||||
|
||||
export const AdvancedDropwdown = styled.div`
|
||||
position: absolute;
|
||||
margin-top: -12px;
|
||||
max-width: 455px;
|
||||
export const AdvancedDropdown = styled.div`
|
||||
padding-top: calc(10px + 2rem);
|
||||
padding-bottom: 10px;
|
||||
margin-top: -2rem;
|
||||
width: 100%;
|
||||
margin-bottom: 100px;
|
||||
padding: 10px 0;
|
||||
padding-top: 36px;
|
||||
max-width: 400px;
|
||||
border-bottom-left-radius: 20px;
|
||||
border-bottom-right-radius: 20px;
|
||||
color: ${({ theme }) => theme.text2};
|
||||
background-color: ${({ theme }) => theme.advancedBG};
|
||||
color: ${({ theme }) => theme.text2};
|
||||
z-index: -1;
|
||||
`
|
||||
|
||||
@ -57,7 +54,7 @@ export const BottomGrouping = styled.div`
|
||||
|
||||
export const ErrorText = styled(Text)<{ severity?: 0 | 1 | 2 | 3 }>`
|
||||
color: ${({ theme, severity }) =>
|
||||
severity === 3 ? theme.red1 : severity === 2 ? theme.yellow2 : severity === 1 ? theme.green1 : theme.text1};
|
||||
severity === 3 ? theme.red1 : severity === 2 ? theme.yellow2 : severity === 1 ? theme.text1 : theme.green1};
|
||||
`
|
||||
|
||||
export const InputGroup = styled(AutoColumn)`
|
||||
|
@ -1,15 +1,12 @@
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
import { MaxUint256 } from '@ethersproject/constants'
|
||||
import { Contract } from '@ethersproject/contracts'
|
||||
import { parseEther, parseUnits } from '@ethersproject/units'
|
||||
import { JSBI, Percent, Price, Route, Token, TokenAmount, WETH } from '@uniswap/sdk'
|
||||
import React, { useCallback, useContext, useEffect, useReducer, useState } from 'react'
|
||||
import { TokenAmount, WETH } from '@uniswap/sdk'
|
||||
import React, { useContext, useState } from 'react'
|
||||
import { Plus } from 'react-feather'
|
||||
import ReactGA from 'react-ga'
|
||||
import { RouteComponentProps } from 'react-router-dom'
|
||||
import { Text } from 'rebass'
|
||||
import styled, { ThemeContext } from 'styled-components'
|
||||
import { ButtonLight, ButtonPrimary } from '../../components/Button'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import { ButtonLight, ButtonPrimary, ButtonError } from '../../components/Button'
|
||||
import { BlueCard, GreyCard, LightCard } from '../../components/Card'
|
||||
import { AutoColumn, ColumnCenter } from '../../components/Column'
|
||||
import ConfirmationModal from '../../components/ConfirmationModal'
|
||||
@ -17,422 +14,143 @@ import CurrencyInputPanel from '../../components/CurrencyInputPanel'
|
||||
import DoubleLogo from '../../components/DoubleLogo'
|
||||
import PositionCard from '../../components/PositionCard'
|
||||
import Row, { AutoRow, RowBetween, RowFixed, RowFlat } from '../../components/Row'
|
||||
import SearchModal from '../../components/SearchModal'
|
||||
|
||||
import TokenLogo from '../../components/TokenLogo'
|
||||
|
||||
import { ROUTER_ADDRESS } from '../../constants'
|
||||
import { useTokenAllowance } from '../../data/Allowances'
|
||||
import { usePair } from '../../data/Reserves'
|
||||
import { useTotalSupply } from '../../data/TotalSupply'
|
||||
import { useTokenContract, useActiveWeb3React } from '../../hooks'
|
||||
import { ROUTER_ADDRESS, MIN_ETH, ONE_BIPS, DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE } from '../../constants'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
|
||||
import { useTokenByAddressAndAutomaticallyAdd } from '../../hooks/Tokens'
|
||||
import { useHasPendingApproval, useTransactionAdder } from '../../state/transactions/hooks'
|
||||
import { useTokenBalanceTreatingWETHasETH } from '../../state/wallet/hooks'
|
||||
import { useTransactionAdder } from '../../state/transactions/hooks'
|
||||
import { TYPE } from '../../theme'
|
||||
import { calculateGasMargin, calculateSlippageAmount, getRouterContract, isAddress } from '../../utils'
|
||||
import { calculateGasMargin, calculateSlippageAmount, getRouterContract } from '../../utils'
|
||||
import AppBody from '../AppBody'
|
||||
import { Dots, Wrapper } from '../Pool/styleds'
|
||||
|
||||
// denominated in bips
|
||||
const ALLOWED_SLIPPAGE = 50
|
||||
|
||||
// denominated in seconds
|
||||
const DEADLINE_FROM_NOW = 60 * 20
|
||||
|
||||
const FixedBottom = styled.div`
|
||||
position: absolute;
|
||||
margin-top: 2rem;
|
||||
width: 100%;
|
||||
`
|
||||
|
||||
enum Field {
|
||||
INPUT = 'INPUT',
|
||||
OUTPUT = 'OUTPUT'
|
||||
}
|
||||
|
||||
interface AddState {
|
||||
independentField: Field
|
||||
typedValue: string
|
||||
[Field.INPUT]: {
|
||||
address: string | undefined
|
||||
}
|
||||
[Field.OUTPUT]: {
|
||||
address: string | undefined
|
||||
}
|
||||
}
|
||||
|
||||
function initializeAddState(inputAddress?: string, outputAddress?: string): AddState {
|
||||
const validatedInput = isAddress(inputAddress)
|
||||
const validatedOutput = isAddress(outputAddress)
|
||||
return {
|
||||
independentField: Field.INPUT,
|
||||
typedValue: '',
|
||||
[Field.INPUT]: {
|
||||
address: validatedInput || ''
|
||||
},
|
||||
[Field.OUTPUT]: {
|
||||
address: validatedOutput && validatedOutput !== validatedInput ? validatedOutput : ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum AddAction {
|
||||
SELECT_TOKEN,
|
||||
SWITCH_TOKENS,
|
||||
TYPE
|
||||
}
|
||||
|
||||
interface Payload {
|
||||
[AddAction.SELECT_TOKEN]: {
|
||||
field: Field
|
||||
address: string
|
||||
}
|
||||
[AddAction.SWITCH_TOKENS]: undefined
|
||||
[AddAction.TYPE]: {
|
||||
field: Field
|
||||
typedValue: string
|
||||
}
|
||||
}
|
||||
|
||||
function reducer(
|
||||
state: AddState,
|
||||
action: {
|
||||
type: AddAction
|
||||
payload: Payload[AddAction]
|
||||
}
|
||||
): AddState {
|
||||
switch (action.type) {
|
||||
case AddAction.SELECT_TOKEN: {
|
||||
const { field, address } = action.payload as Payload[AddAction.SELECT_TOKEN]
|
||||
const otherField = field === Field.INPUT ? Field.OUTPUT : Field.INPUT
|
||||
if (address === state[otherField].address) {
|
||||
// the case where we have to swap the order
|
||||
return {
|
||||
...state,
|
||||
independentField: state.independentField === Field.INPUT ? Field.OUTPUT : Field.INPUT,
|
||||
[field]: { address },
|
||||
[otherField]: { address: state[field].address }
|
||||
}
|
||||
} else {
|
||||
// the normal case
|
||||
return {
|
||||
...state,
|
||||
[field]: { address }
|
||||
}
|
||||
}
|
||||
}
|
||||
case AddAction.TYPE: {
|
||||
const { field, typedValue } = action.payload as Payload[AddAction.TYPE]
|
||||
return {
|
||||
...state,
|
||||
independentField: field,
|
||||
typedValue
|
||||
}
|
||||
}
|
||||
default: {
|
||||
throw Error
|
||||
}
|
||||
}
|
||||
}
|
||||
import {
|
||||
useDefaultsFromURLMatchParams,
|
||||
useMintState,
|
||||
useDerivedMintInfo,
|
||||
useMintActionHandlers
|
||||
} from '../../state/mint/hooks'
|
||||
import { Field } from '../../state/mint/actions'
|
||||
import { useApproveCallback, ApprovalState } from '../../hooks/useApproveCallback'
|
||||
import { useWalletModalToggle } from '../../state/application/hooks'
|
||||
import AdvancedSwapDetailsDropdown from '../../components/swap/AdvancedSwapDetailsDropdown'
|
||||
|
||||
export default function AddLiquidity({ match: { params } }: RouteComponentProps<{ tokens: string }>) {
|
||||
const [token0, token1] = params.tokens.split('-')
|
||||
useDefaultsFromURLMatchParams(params)
|
||||
|
||||
const { account, chainId, library } = useActiveWeb3React()
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
// modal states
|
||||
const [showSearch, setShowSearch] = useState<boolean>(false)
|
||||
// toggle wallet when disconnected
|
||||
const toggleWalletModal = useWalletModalToggle()
|
||||
|
||||
// mint state
|
||||
const { independentField, typedValue, otherTypedValue } = useMintState()
|
||||
const {
|
||||
dependentField,
|
||||
tokens,
|
||||
pair,
|
||||
tokenBalances,
|
||||
parsedAmounts,
|
||||
price,
|
||||
noLiquidity,
|
||||
liquidityMinted,
|
||||
poolTokenPercentage,
|
||||
error
|
||||
} = useDerivedMintInfo()
|
||||
const { onUserInput } = useMintActionHandlers()
|
||||
|
||||
const isValid = !error
|
||||
|
||||
// modal and loading
|
||||
const [showConfirm, setShowConfirm] = useState<boolean>(false)
|
||||
const [attemptingTxn, setAttemptingTxn] = useState<boolean>(false) // clicke confirm
|
||||
const [pendingConfirmation, setPendingConfirmation] = useState<boolean>(true)
|
||||
const [showAdvanced, setShowAdvanced] = useState<boolean>(false)
|
||||
const [attemptingTxn, setAttemptingTxn] = useState<boolean>(false) // clicked confirm
|
||||
const [pendingConfirmation, setPendingConfirmation] = useState<boolean>(true) // waiting for user confirmation
|
||||
|
||||
// input state
|
||||
const [state, dispatch] = useReducer(reducer, initializeAddState(token0, token1))
|
||||
const { independentField, typedValue, ...fieldData } = state
|
||||
const dependentField: Field = independentField === Field.INPUT ? Field.OUTPUT : Field.INPUT
|
||||
|
||||
const inputToken = useTokenByAddressAndAutomaticallyAdd(fieldData[Field.INPUT].address)
|
||||
const outputToken = useTokenByAddressAndAutomaticallyAdd(fieldData[Field.OUTPUT].address)
|
||||
|
||||
// get basic SDK entities
|
||||
const tokens: { [field in Field]: Token } = {
|
||||
[Field.INPUT]: inputToken,
|
||||
[Field.OUTPUT]: outputToken
|
||||
}
|
||||
|
||||
// token contracts for approvals and direct sends
|
||||
const tokenContractInput: Contract = useTokenContract(tokens[Field.INPUT]?.address)
|
||||
const tokenContractOutput: Contract = useTokenContract(tokens[Field.OUTPUT]?.address)
|
||||
|
||||
// exchange data
|
||||
const pair = usePair(tokens[Field.INPUT], tokens[Field.OUTPUT])
|
||||
const route: Route = pair ? new Route([pair], tokens[independentField]) : undefined
|
||||
const totalSupply: TokenAmount = useTotalSupply(pair?.liquidityToken)
|
||||
const noLiquidity = // used to detect new exchange
|
||||
pair === null ||
|
||||
(!!pair && JSBI.equal(pair.reserve0.raw, JSBI.BigInt(0)) && JSBI.equal(pair.reserve1.raw, JSBI.BigInt(0)))
|
||||
|
||||
// get user-pecific and token-specific lookup data
|
||||
const userBalances: { [field in Field]: TokenAmount } = {
|
||||
[Field.INPUT]: useTokenBalanceTreatingWETHasETH(account, tokens[Field.INPUT]),
|
||||
[Field.OUTPUT]: useTokenBalanceTreatingWETHasETH(account, tokens[Field.OUTPUT])
|
||||
}
|
||||
|
||||
// track non relational amounts if first person to add liquidity
|
||||
const [nonrelationalAmounts, setNonrelationalAmounts] = useState({
|
||||
[Field.INPUT]: null,
|
||||
[Field.OUTPUT]: null
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (typedValue !== '.' && tokens[independentField] && noLiquidity) {
|
||||
const newNonRelationalAmounts = nonrelationalAmounts
|
||||
if (typedValue === '') {
|
||||
if (independentField === Field.OUTPUT) {
|
||||
newNonRelationalAmounts[Field.OUTPUT] = null
|
||||
} else {
|
||||
newNonRelationalAmounts[Field.INPUT] = null
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const typedValueParsed = parseUnits(typedValue, tokens[independentField].decimals).toString()
|
||||
if (independentField === Field.OUTPUT) {
|
||||
newNonRelationalAmounts[Field.OUTPUT] = new TokenAmount(tokens[independentField], typedValueParsed)
|
||||
} else {
|
||||
newNonRelationalAmounts[Field.INPUT] = new TokenAmount(tokens[independentField], typedValueParsed)
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
setNonrelationalAmounts(newNonRelationalAmounts)
|
||||
}
|
||||
}, [independentField, nonrelationalAmounts, tokens, typedValue, noLiquidity])
|
||||
|
||||
// caclulate the token amounts based on the input
|
||||
const parsedAmounts: { [field: number]: TokenAmount } = {}
|
||||
if (noLiquidity) {
|
||||
parsedAmounts[independentField] = nonrelationalAmounts[independentField]
|
||||
parsedAmounts[dependentField] = nonrelationalAmounts[dependentField]
|
||||
}
|
||||
if (typedValue !== '' && typedValue !== '.' && tokens[independentField]) {
|
||||
try {
|
||||
const typedValueParsed = parseUnits(typedValue, tokens[independentField].decimals).toString()
|
||||
if (typedValueParsed !== '0')
|
||||
parsedAmounts[independentField] = new TokenAmount(tokens[independentField], typedValueParsed)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
route &&
|
||||
!noLiquidity &&
|
||||
parsedAmounts[independentField] &&
|
||||
JSBI.greaterThan(parsedAmounts[independentField].raw, JSBI.BigInt(0))
|
||||
) {
|
||||
parsedAmounts[dependentField] = route.midPrice.quote(parsedAmounts[independentField])
|
||||
}
|
||||
// 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 = {
|
||||
[independentField]: typedValue,
|
||||
[dependentField]: parsedAmounts[dependentField] ? parsedAmounts[dependentField]?.toSignificant(6) : ''
|
||||
[dependentField]: noLiquidity ? otherTypedValue : parsedAmounts[dependentField]?.toSignificant(6) ?? ''
|
||||
}
|
||||
|
||||
// check whether the user has approved the router on both tokens
|
||||
const inputApproval: TokenAmount = useTokenAllowance(tokens[Field.INPUT], account, ROUTER_ADDRESS)
|
||||
const outputApproval: TokenAmount = useTokenAllowance(tokens[Field.OUTPUT], account, ROUTER_ADDRESS)
|
||||
const inputApproved =
|
||||
tokens[Field.INPUT]?.equals(WETH[chainId]) ||
|
||||
(!!inputApproval &&
|
||||
!!parsedAmounts[Field.INPUT] &&
|
||||
JSBI.greaterThanOrEqual(inputApproval.raw, parsedAmounts[Field.INPUT].raw))
|
||||
const outputApproved =
|
||||
tokens[Field.OUTPUT]?.equals(WETH[chainId]) ||
|
||||
(!!outputApproval &&
|
||||
!!parsedAmounts[Field.OUTPUT] &&
|
||||
JSBI.greaterThanOrEqual(outputApproval.raw, parsedAmounts[Field.OUTPUT].raw))
|
||||
// check on pending approvals for token amounts
|
||||
const pendingApprovalInput = useHasPendingApproval(tokens[Field.INPUT]?.address)
|
||||
const pendingApprovalOutput = useHasPendingApproval(tokens[Field.OUTPUT]?.address)
|
||||
|
||||
// used for displaying approximate starting price in UI
|
||||
const derivedPrice =
|
||||
parsedAmounts[Field.INPUT] &&
|
||||
parsedAmounts[Field.OUTPUT] &&
|
||||
nonrelationalAmounts[Field.INPUT] &&
|
||||
nonrelationalAmounts[Field.OUTPUT] &&
|
||||
typedValue !== ''
|
||||
? new Price(
|
||||
parsedAmounts[Field.INPUT].token,
|
||||
parsedAmounts[Field.OUTPUT].token,
|
||||
parsedAmounts[Field.INPUT].raw,
|
||||
parsedAmounts[Field.OUTPUT].raw
|
||||
)
|
||||
: null
|
||||
|
||||
// check for estimated liquidity minted
|
||||
const liquidityMinted: TokenAmount =
|
||||
!!pair &&
|
||||
!!totalSupply &&
|
||||
!!parsedAmounts[Field.INPUT] &&
|
||||
!!parsedAmounts[Field.OUTPUT] &&
|
||||
!JSBI.equal(parsedAmounts[Field.INPUT].raw, JSBI.BigInt(0)) &&
|
||||
!JSBI.equal(parsedAmounts[Field.OUTPUT].raw, JSBI.BigInt(0))
|
||||
? pair.getLiquidityMinted(totalSupply, parsedAmounts[Field.INPUT], parsedAmounts[Field.OUTPUT])
|
||||
: undefined
|
||||
|
||||
const poolTokenPercentage: Percent =
|
||||
!!liquidityMinted && !!totalSupply
|
||||
? new Percent(liquidityMinted.raw, totalSupply.add(liquidityMinted).raw)
|
||||
: undefined
|
||||
|
||||
const onTokenSelection = useCallback((field: Field, address: string) => {
|
||||
dispatch({
|
||||
type: AddAction.SELECT_TOKEN,
|
||||
payload: { field, address }
|
||||
})
|
||||
}, [])
|
||||
|
||||
const onUserInput = useCallback((field: Field, typedValue: string) => {
|
||||
dispatch({ type: AddAction.TYPE, payload: { field, typedValue } })
|
||||
}, [])
|
||||
|
||||
const onMax = useCallback((typedValue: string, field) => {
|
||||
dispatch({
|
||||
type: AddAction.TYPE,
|
||||
payload: {
|
||||
field: field,
|
||||
typedValue
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
const MIN_ETHER: TokenAmount = new TokenAmount(WETH[chainId], JSBI.BigInt(parseEther('.01')))
|
||||
|
||||
// get the max amounts user can add
|
||||
const [maxAmountInput, maxAmountOutput]: TokenAmount[] = [Field.INPUT, Field.OUTPUT].map(index => {
|
||||
const field = Field[index]
|
||||
return !!userBalances[Field[field]] &&
|
||||
JSBI.greaterThan(
|
||||
userBalances[Field[field]].raw,
|
||||
tokens[Field[field]]?.equals(WETH[chainId]) ? MIN_ETHER.raw : JSBI.BigInt(0)
|
||||
const maxAmounts: { [field in Field]?: TokenAmount } = [Field.TOKEN_A, Field.TOKEN_B].reduce((accumulator, field) => {
|
||||
return {
|
||||
...accumulator,
|
||||
[field]:
|
||||
!!tokenBalances[field] &&
|
||||
!!tokens[field] &&
|
||||
!!WETH[chainId] &&
|
||||
tokenBalances[field].greaterThan(
|
||||
new TokenAmount(tokens[field], tokens[field].equals(WETH[chainId]) ? MIN_ETH : '0')
|
||||
)
|
||||
? tokens[Field[field]]?.equals(WETH[chainId])
|
||||
? userBalances[Field[field]].subtract(MIN_ETHER)
|
||||
: userBalances[Field[field]]
|
||||
? tokens[field].equals(WETH[chainId])
|
||||
? tokenBalances[field].subtract(new TokenAmount(WETH[chainId], MIN_ETH))
|
||||
: tokenBalances[field]
|
||||
: undefined
|
||||
})
|
||||
|
||||
const [atMaxAmountInput, atMaxAmountOutput]: boolean[] = [Field.INPUT, Field.OUTPUT].map(index => {
|
||||
const field = Field[index]
|
||||
const maxAmount = index === Field.INPUT ? maxAmountInput : maxAmountOutput
|
||||
return !!maxAmount && !!parsedAmounts[Field[field]]
|
||||
? JSBI.equal(maxAmount.raw, parsedAmounts[Field[field]].raw)
|
||||
: undefined
|
||||
})
|
||||
|
||||
// errors
|
||||
const [generalError, setGeneralError] = useState('')
|
||||
const [inputError, setInputError] = useState('')
|
||||
const [outputError, setOutputError] = useState('')
|
||||
const [isValid, setIsValid] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
// reset errors
|
||||
setGeneralError(null)
|
||||
setInputError(null)
|
||||
setOutputError(null)
|
||||
setIsValid(true)
|
||||
|
||||
if (!account) {
|
||||
setGeneralError('Connect Wallet')
|
||||
setIsValid(false)
|
||||
}
|
||||
}, {})
|
||||
|
||||
if (noLiquidity && parsedAmounts[Field.INPUT] && JSBI.equal(parsedAmounts[Field.INPUT].raw, JSBI.BigInt(0))) {
|
||||
setGeneralError('Enter an amount')
|
||||
setIsValid(false)
|
||||
const atMaxAmounts: { [field in Field]?: TokenAmount } = [Field.TOKEN_A, Field.TOKEN_B].reduce(
|
||||
(accumulator, field) => {
|
||||
return {
|
||||
...accumulator,
|
||||
[field]: maxAmounts[field] && parsedAmounts[field] ? maxAmounts[field].equalTo(parsedAmounts[field]) : undefined
|
||||
}
|
||||
},
|
||||
{}
|
||||
)
|
||||
|
||||
if (noLiquidity && parsedAmounts[Field.OUTPUT] && JSBI.equal(parsedAmounts[Field.OUTPUT].raw, JSBI.BigInt(0))) {
|
||||
setGeneralError('Enter an amount')
|
||||
setIsValid(false)
|
||||
}
|
||||
// check whether the user has approved the router on the tokens
|
||||
const [approvalA, approveACallback] = useApproveCallback(parsedAmounts[Field.TOKEN_A], ROUTER_ADDRESS)
|
||||
const [approvalB, approveBCallback] = useApproveCallback(parsedAmounts[Field.TOKEN_B], ROUTER_ADDRESS)
|
||||
|
||||
if (!parsedAmounts[Field.INPUT]) {
|
||||
setGeneralError('Enter an amount')
|
||||
setIsValid(false)
|
||||
}
|
||||
if (!parsedAmounts[Field.OUTPUT]) {
|
||||
setGeneralError('Enter an amount')
|
||||
setIsValid(false)
|
||||
}
|
||||
if (
|
||||
parsedAmounts?.[Field.INPUT] &&
|
||||
userBalances?.[Field.INPUT] &&
|
||||
JSBI.greaterThan(parsedAmounts?.[Field.INPUT]?.raw, userBalances?.[Field.INPUT]?.raw)
|
||||
) {
|
||||
setInputError('Insufficient ' + tokens[Field.INPUT]?.symbol + ' balance')
|
||||
setIsValid(false)
|
||||
}
|
||||
if (
|
||||
parsedAmounts?.[Field.OUTPUT] &&
|
||||
userBalances?.[Field.OUTPUT] &&
|
||||
JSBI.greaterThan(parsedAmounts?.[Field.OUTPUT]?.raw, userBalances?.[Field.OUTPUT]?.raw)
|
||||
) {
|
||||
setOutputError('Insufficient ' + tokens[Field.OUTPUT]?.symbol + ' balance')
|
||||
setIsValid(false)
|
||||
}
|
||||
}, [noLiquidity, parsedAmounts, tokens, userBalances, account])
|
||||
|
||||
// state for txn
|
||||
const addTransaction = useTransactionAdder()
|
||||
const [txHash, setTxHash] = useState<string>('')
|
||||
|
||||
async function onAdd() {
|
||||
setAttemptingTxn(true)
|
||||
|
||||
const router = getRouterContract(chainId, library, account)
|
||||
|
||||
const minInput = calculateSlippageAmount(parsedAmounts[Field.INPUT], ALLOWED_SLIPPAGE)[0]
|
||||
const minOutput = calculateSlippageAmount(parsedAmounts[Field.OUTPUT], ALLOWED_SLIPPAGE)[0]
|
||||
const amountsMin = {
|
||||
[Field.TOKEN_A]: calculateSlippageAmount(parsedAmounts[Field.TOKEN_A], noLiquidity ? 0 : allowedSlippage)[0],
|
||||
[Field.TOKEN_B]: calculateSlippageAmount(parsedAmounts[Field.TOKEN_B], noLiquidity ? 0 : allowedSlippage)[0]
|
||||
}
|
||||
|
||||
const deadline = Math.ceil(Date.now() / 1000) + DEADLINE_FROM_NOW
|
||||
const deadlineFromNow = Math.ceil(Date.now() / 1000) + deadline
|
||||
|
||||
let method, estimate, args, value
|
||||
|
||||
// one of the tokens is ETH
|
||||
if (tokens[Field.INPUT].equals(WETH[chainId]) || tokens[Field.OUTPUT].equals(WETH[chainId])) {
|
||||
method = router.addLiquidityETH
|
||||
let estimate, method: Function, args: Array<string | string[] | number>, value: BigNumber | null
|
||||
if (tokens[Field.TOKEN_A].equals(WETH[chainId]) || tokens[Field.TOKEN_B].equals(WETH[chainId])) {
|
||||
const tokenBIsETH = tokens[Field.TOKEN_B].equals(WETH[chainId])
|
||||
estimate = router.estimateGas.addLiquidityETH
|
||||
|
||||
const outputIsETH = tokens[Field.OUTPUT].equals(WETH[chainId])
|
||||
|
||||
method = router.addLiquidityETH
|
||||
args = [
|
||||
tokens[outputIsETH ? Field.INPUT : Field.OUTPUT].address, // token
|
||||
parsedAmounts[outputIsETH ? Field.INPUT : Field.OUTPUT].raw.toString(), // token desired
|
||||
outputIsETH ? minInput.toString() : minOutput.toString(), // token min
|
||||
outputIsETH ? minOutput.toString() : minInput.toString(), // eth min
|
||||
tokens[tokenBIsETH ? Field.TOKEN_A : Field.TOKEN_B].address, // token
|
||||
parsedAmounts[tokenBIsETH ? Field.TOKEN_A : Field.TOKEN_B].raw.toString(), // token desired
|
||||
amountsMin[tokenBIsETH ? Field.TOKEN_A : Field.TOKEN_B].toString(), // token min
|
||||
amountsMin[tokenBIsETH ? Field.TOKEN_B : Field.TOKEN_A].toString(), // eth min
|
||||
account,
|
||||
deadline
|
||||
deadlineFromNow
|
||||
]
|
||||
value = BigNumber.from(parsedAmounts[outputIsETH ? Field.OUTPUT : Field.INPUT].raw.toString())
|
||||
value = BigNumber.from(parsedAmounts[tokenBIsETH ? Field.TOKEN_B : Field.TOKEN_A].raw.toString())
|
||||
} else {
|
||||
method = router.addLiquidity
|
||||
estimate = router.estimateGas.addLiquidity
|
||||
method = router.addLiquidity
|
||||
args = [
|
||||
tokens[Field.INPUT].address,
|
||||
tokens[Field.OUTPUT].address,
|
||||
parsedAmounts[Field.INPUT].raw.toString(),
|
||||
parsedAmounts[Field.OUTPUT].raw.toString(),
|
||||
noLiquidity ? parsedAmounts[Field.INPUT].raw.toString() : minInput.toString(),
|
||||
noLiquidity ? parsedAmounts[Field.OUTPUT].raw.toString() : minOutput.toString(),
|
||||
tokens[Field.TOKEN_A].address,
|
||||
tokens[Field.TOKEN_B].address,
|
||||
parsedAmounts[Field.TOKEN_A].raw.toString(),
|
||||
parsedAmounts[Field.TOKEN_B].raw.toString(),
|
||||
amountsMin[Field.TOKEN_A].toString(),
|
||||
amountsMin[Field.TOKEN_B].toString(),
|
||||
account,
|
||||
deadline
|
||||
deadlineFromNow
|
||||
]
|
||||
value = null
|
||||
}
|
||||
@ -443,24 +161,26 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps<
|
||||
...(value ? { value } : {}),
|
||||
gasLimit: calculateGasMargin(estimatedGasLimit)
|
||||
}).then(response => {
|
||||
ReactGA.event({
|
||||
category: 'Liquidity',
|
||||
action: 'Add',
|
||||
label: [tokens[Field.INPUT]?.symbol, tokens[Field.OUTPUT]?.symbol].join('/')
|
||||
})
|
||||
setTxHash(response.hash)
|
||||
addTransaction(response, {
|
||||
summary:
|
||||
'Add ' +
|
||||
parsedAmounts[Field.INPUT]?.toSignificant(3) +
|
||||
parsedAmounts[Field.TOKEN_A]?.toSignificant(3) +
|
||||
' ' +
|
||||
tokens[Field.INPUT]?.symbol +
|
||||
tokens[Field.TOKEN_A]?.symbol +
|
||||
' and ' +
|
||||
parsedAmounts[Field.OUTPUT]?.toSignificant(3) +
|
||||
parsedAmounts[Field.TOKEN_B]?.toSignificant(3) +
|
||||
' ' +
|
||||
tokens[Field.OUTPUT]?.symbol
|
||||
tokens[Field.TOKEN_B]?.symbol
|
||||
})
|
||||
|
||||
setTxHash(response.hash)
|
||||
setPendingConfirmation(false)
|
||||
|
||||
ReactGA.event({
|
||||
category: 'Liquidity',
|
||||
action: 'Add',
|
||||
label: [tokens[Field.TOKEN_A]?.symbol, tokens[Field.TOKEN_B]?.symbol].join('/')
|
||||
})
|
||||
})
|
||||
)
|
||||
.catch((e: Error) => {
|
||||
@ -468,64 +188,20 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps<
|
||||
setPendingConfirmation(true)
|
||||
setAttemptingTxn(false)
|
||||
setShowConfirm(false)
|
||||
})
|
||||
}
|
||||
|
||||
async function approveAmount(field) {
|
||||
let useUserBalance = false
|
||||
const tokenContract = field === Field.INPUT ? tokenContractInput : tokenContractOutput
|
||||
|
||||
const estimatedGas = await tokenContract.estimateGas.approve(ROUTER_ADDRESS, MaxUint256).catch(() => {
|
||||
// general fallback for tokens who restrict approval amounts
|
||||
useUserBalance = true
|
||||
return tokenContract.estimateGas.approve(ROUTER_ADDRESS, userBalances[field])
|
||||
})
|
||||
|
||||
tokenContract
|
||||
.approve(ROUTER_ADDRESS, useUserBalance ? userBalances[field] : MaxUint256, {
|
||||
gasLimit: calculateGasMargin(estimatedGas)
|
||||
})
|
||||
.then(response => {
|
||||
addTransaction(response, {
|
||||
summary: 'Approve ' + tokens[field]?.symbol,
|
||||
approvalOfToken: tokens[field].address
|
||||
})
|
||||
setShowAdvanced(false)
|
||||
})
|
||||
}
|
||||
|
||||
const modalHeader = () => {
|
||||
return noLiquidity ? (
|
||||
<AutoColumn gap="12px">
|
||||
<LightCard margin={'30px 0'} borderRadius="20px">
|
||||
<ColumnCenter>
|
||||
<RowFixed>
|
||||
<Text fontSize={36} fontWeight={500} marginRight={20}>
|
||||
{tokens[Field.INPUT]?.symbol + '-' + tokens[Field.OUTPUT]?.symbol}
|
||||
</Text>{' '}
|
||||
<DoubleLogo a0={tokens[Field.INPUT]?.address} a1={tokens[Field.OUTPUT]?.address} size={36} />
|
||||
</RowFixed>
|
||||
</ColumnCenter>
|
||||
</LightCard>
|
||||
<TYPE.body>Starting pool prices</TYPE.body>
|
||||
<LightCard borderRadius="20px">
|
||||
<TYPE.mediumHeader>
|
||||
{parsedAmounts[0] &&
|
||||
parsedAmounts[1] &&
|
||||
JSBI.greaterThan(parsedAmounts[0].raw, JSBI.BigInt(0)) &&
|
||||
JSBI.greaterThan(parsedAmounts[1].raw, JSBI.BigInt(0)) &&
|
||||
derivedPrice?.invert().toSignificant(6)}{' '}
|
||||
{tokens[Field.INPUT]?.symbol + '/' + tokens[Field.OUTPUT]?.symbol}
|
||||
</TYPE.mediumHeader>
|
||||
</LightCard>
|
||||
<LightCard borderRadius="20px">
|
||||
<TYPE.mediumHeader>
|
||||
{parsedAmounts[0] &&
|
||||
parsedAmounts[1] &&
|
||||
JSBI.greaterThan(parsedAmounts[0].raw, JSBI.BigInt(0)) &&
|
||||
JSBI.greaterThan(parsedAmounts[1].raw, JSBI.BigInt(0)) &&
|
||||
derivedPrice?.toSignificant(6)}{' '}
|
||||
{tokens[Field.OUTPUT]?.symbol + '/' + tokens[Field.INPUT]?.symbol}
|
||||
</TYPE.mediumHeader>
|
||||
<AutoColumn gap="20px">
|
||||
<LightCard mt="20px" borderRadius="20px">
|
||||
<RowFlat>
|
||||
<Text fontSize="48px" fontWeight={500} lineHeight="42px" marginRight={10}>
|
||||
{tokens[Field.TOKEN_A]?.symbol + '/' + tokens[Field.TOKEN_B]?.symbol}
|
||||
</Text>
|
||||
<DoubleLogo a0={tokens[Field.TOKEN_A]?.address} a1={tokens[Field.TOKEN_B]?.address} size={30} />
|
||||
</RowFlat>
|
||||
</LightCard>
|
||||
</AutoColumn>
|
||||
) : (
|
||||
@ -534,17 +210,16 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps<
|
||||
<Text fontSize="48px" fontWeight={500} lineHeight="42px" marginRight={10}>
|
||||
{liquidityMinted?.toSignificant(6)}
|
||||
</Text>
|
||||
<DoubleLogo a0={tokens[Field.INPUT]?.address} a1={tokens[Field.OUTPUT]?.address} size={30} />
|
||||
<DoubleLogo a0={tokens[Field.TOKEN_A]?.address} a1={tokens[Field.TOKEN_B]?.address} size={30} />
|
||||
</RowFlat>
|
||||
<Row>
|
||||
<Text fontSize="24px">
|
||||
{tokens[Field.INPUT]?.symbol + '/' + tokens[Field.OUTPUT]?.symbol + ' Pool Tokens'}
|
||||
{tokens[Field.TOKEN_A]?.symbol + '/' + tokens[Field.TOKEN_B]?.symbol + ' Pool Tokens'}
|
||||
</Text>
|
||||
</Row>
|
||||
<TYPE.italic fontSize={12} textAlign="left" padding={'8px 0 0 0 '}>
|
||||
{`Output is estimated. You will receive at least ${liquidityMinted?.toSignificant(6)} UNI ${
|
||||
tokens[Field.INPUT]?.symbol
|
||||
}/${tokens[Field.OUTPUT]?.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>
|
||||
)
|
||||
@ -554,93 +229,70 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps<
|
||||
return (
|
||||
<>
|
||||
<RowBetween>
|
||||
<TYPE.body>{tokens[Field.INPUT]?.symbol} Deposited</TYPE.body>
|
||||
<TYPE.body>{tokens[Field.TOKEN_A]?.symbol} Deposited</TYPE.body>
|
||||
<RowFixed>
|
||||
<TokenLogo address={tokens[Field.INPUT]?.address} style={{ marginRight: '8px' }} />
|
||||
<TYPE.body>{!!parsedAmounts[Field.INPUT] && parsedAmounts[Field.INPUT].toSignificant(6)}</TYPE.body>
|
||||
<TokenLogo address={tokens[Field.TOKEN_A]?.address} style={{ marginRight: '8px' }} />
|
||||
<TYPE.body>{parsedAmounts[Field.TOKEN_A]?.toSignificant(6)}</TYPE.body>
|
||||
</RowFixed>
|
||||
</RowBetween>
|
||||
<RowBetween>
|
||||
<TYPE.body>{tokens[Field.OUTPUT]?.symbol} Deposited</TYPE.body>
|
||||
<TYPE.body>{tokens[Field.TOKEN_B]?.symbol} Deposited</TYPE.body>
|
||||
<RowFixed>
|
||||
<TokenLogo address={tokens[Field.OUTPUT]?.address} style={{ marginRight: '8px' }} />
|
||||
<TYPE.body>{!!parsedAmounts[Field.OUTPUT] && parsedAmounts[Field.OUTPUT].toSignificant(6)}</TYPE.body>
|
||||
<TokenLogo address={tokens[Field.TOKEN_B]?.address} style={{ marginRight: '8px' }} />
|
||||
<TYPE.body>{parsedAmounts[Field.TOKEN_B]?.toSignificant(6)}</TYPE.body>
|
||||
</RowFixed>
|
||||
</RowBetween>
|
||||
{route && !JSBI.equal(route?.midPrice?.raw?.denominator, JSBI.BigInt(0)) && (
|
||||
<RowBetween>
|
||||
<TYPE.body>Rate</TYPE.body>
|
||||
<TYPE.body>Rates</TYPE.body>
|
||||
<TYPE.body>
|
||||
{`1 ${tokens[Field.INPUT]?.symbol} = ${route?.midPrice &&
|
||||
route?.midPrice?.raw?.denominator &&
|
||||
route?.midPrice?.adjusted?.toSignificant(4)} ${tokens[Field.OUTPUT]?.symbol}`}
|
||||
{`1 ${tokens[Field.TOKEN_A]?.symbol} = ${price?.toSignificant(4)} ${tokens[Field.TOKEN_B]?.symbol}`}
|
||||
</TYPE.body>
|
||||
</RowBetween>
|
||||
<RowBetween style={{ justifyContent: 'flex-end' }}>
|
||||
<TYPE.body>
|
||||
{`1 ${tokens[Field.TOKEN_B]?.symbol} = ${price?.invert().toSignificant(4)} ${
|
||||
tokens[Field.TOKEN_A]?.symbol
|
||||
}`}
|
||||
</TYPE.body>
|
||||
</RowBetween>
|
||||
)}
|
||||
<RowBetween>
|
||||
<TYPE.body>Minted Pool Share:</TYPE.body>
|
||||
<TYPE.body>{noLiquidity ? '100%' : poolTokenPercentage?.toSignificant(6) + '%'}</TYPE.body>
|
||||
<TYPE.body>Share of Pool:</TYPE.body>
|
||||
<TYPE.body>{noLiquidity ? '100' : poolTokenPercentage?.toSignificant(4)}%</TYPE.body>
|
||||
</RowBetween>
|
||||
<ButtonPrimary style={{ margin: '20px 0 0 0' }} onClick={onAdd}>
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
{noLiquidity ? 'Supply & Create Pool' : 'Confirm Supply'}
|
||||
{noLiquidity ? 'Create Pool & Supply' : 'Confirm Supply'}
|
||||
</Text>
|
||||
</ButtonPrimary>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const displayPriceInput = noLiquidity
|
||||
? parsedAmounts[0] &&
|
||||
parsedAmounts[1] &&
|
||||
derivedPrice &&
|
||||
JSBI.greaterThan(parsedAmounts[0].raw, JSBI.BigInt(0)) &&
|
||||
JSBI.greaterThan(parsedAmounts[1].raw, JSBI.BigInt(0))
|
||||
? derivedPrice?.toSignificant(6)
|
||||
: '-'
|
||||
: pair && route && tokens[Field.INPUT]
|
||||
? route?.input.equals(tokens[Field.INPUT])
|
||||
? route.midPrice.toSignificant(6)
|
||||
: route.midPrice.invert().toSignificant(6)
|
||||
: '-'
|
||||
|
||||
const displayPriceOutput = noLiquidity
|
||||
? parsedAmounts[0] &&
|
||||
parsedAmounts[1] &&
|
||||
derivedPrice &&
|
||||
JSBI.greaterThan(parsedAmounts[0].raw, JSBI.BigInt(0)) &&
|
||||
JSBI.greaterThan(parsedAmounts[1].raw, JSBI.BigInt(0))
|
||||
? derivedPrice?.invert().toSignificant(6)
|
||||
: '-'
|
||||
: pair && route && tokens[Field.OUTPUT]
|
||||
? route?.input.equals(tokens[Field.OUTPUT])
|
||||
? route.midPrice.toSignificant(6)
|
||||
: route.midPrice.invert().toSignificant(6)
|
||||
: '-'
|
||||
|
||||
const PriceBar = () => {
|
||||
return (
|
||||
<AutoColumn gap="md" justify="space-between">
|
||||
<AutoRow justify="space-between">
|
||||
<AutoColumn justify="center">
|
||||
<TYPE.black>{displayPriceInput}</TYPE.black>
|
||||
<TYPE.black>{price?.toSignificant(6) ?? '0'}</TYPE.black>
|
||||
<Text fontWeight={500} fontSize={14} color={theme.text2} pt={1}>
|
||||
{tokens[Field.OUTPUT]?.symbol} per {tokens[Field.INPUT]?.symbol}
|
||||
{tokens[Field.TOKEN_B]?.symbol} per {tokens[Field.TOKEN_A]?.symbol}
|
||||
</Text>
|
||||
</AutoColumn>
|
||||
<AutoColumn justify="center">
|
||||
<TYPE.black>{displayPriceOutput}</TYPE.black>
|
||||
<TYPE.black>{price?.invert().toSignificant(6) ?? '0'}</TYPE.black>
|
||||
<Text fontWeight={500} fontSize={14} color={theme.text2} pt={1}>
|
||||
{tokens[Field.INPUT]?.symbol} per {tokens[Field.OUTPUT]?.symbol}
|
||||
{tokens[Field.TOKEN_A]?.symbol} per {tokens[Field.TOKEN_B]?.symbol}
|
||||
</Text>
|
||||
</AutoColumn>
|
||||
<AutoColumn justify="center">
|
||||
<TYPE.black>
|
||||
{noLiquidity && derivedPrice ? '100' : poolTokenPercentage?.toSignificant(4) ?? '0'}
|
||||
{'%'}
|
||||
{noLiquidity && price
|
||||
? '100'
|
||||
: (poolTokenPercentage?.lessThan(ONE_BIPS) ? '<0.01' : poolTokenPercentage?.toFixed(2)) ?? '0'}
|
||||
%
|
||||
</TYPE.black>
|
||||
<Text fontWeight={500} fontSize={14} color={theme.text2} pt={1}>
|
||||
Pool Share
|
||||
Share of Pool
|
||||
</Text>
|
||||
</AutoColumn>
|
||||
</AutoRow>
|
||||
@ -648,11 +300,12 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps<
|
||||
)
|
||||
}
|
||||
|
||||
const pendingText = `Supplying ${parsedAmounts[Field.INPUT]?.toSignificant(6)} ${
|
||||
tokens[Field.INPUT]?.symbol
|
||||
} ${'and'} ${parsedAmounts[Field.OUTPUT]?.toSignificant(6)} ${tokens[Field.OUTPUT]?.symbol}`
|
||||
const pendingText = `Supplying ${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
|
||||
@ -670,12 +323,6 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps<
|
||||
pendingText={pendingText}
|
||||
title={noLiquidity ? 'You are creating a pool' : 'You will receive'}
|
||||
/>
|
||||
<SearchModal
|
||||
isOpen={showSearch}
|
||||
onDismiss={() => {
|
||||
setShowSearch(false)
|
||||
}}
|
||||
/>
|
||||
<AutoColumn gap="20px">
|
||||
{noLiquidity && (
|
||||
<ColumnCenter>
|
||||
@ -695,36 +342,36 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps<
|
||||
</ColumnCenter>
|
||||
)}
|
||||
<CurrencyInputPanel
|
||||
field={Field.INPUT}
|
||||
value={formattedAmounts[Field.INPUT]}
|
||||
disableTokenSelect={true}
|
||||
field={Field.TOKEN_A}
|
||||
value={formattedAmounts[Field.TOKEN_A]}
|
||||
onUserInput={onUserInput}
|
||||
onMax={() => {
|
||||
maxAmountInput && onMax(maxAmountInput.toExact(), Field.INPUT)
|
||||
maxAmounts[Field.TOKEN_A] && onUserInput(Field.TOKEN_A, maxAmounts[Field.TOKEN_A].toExact())
|
||||
}}
|
||||
showMaxButton={!atMaxAmountInput}
|
||||
token={tokens[Field.INPUT]}
|
||||
onTokenSelection={address => onTokenSelection(Field.INPUT, address)}
|
||||
showMaxButton={!atMaxAmounts[Field.TOKEN_A]}
|
||||
token={tokens[Field.TOKEN_A]}
|
||||
pair={pair}
|
||||
label="Input"
|
||||
id="add-liquidity-input-token0"
|
||||
id="add-liquidity-input-tokena"
|
||||
/>
|
||||
<ColumnCenter>
|
||||
<Plus size="16" color={theme.text2} />
|
||||
</ColumnCenter>
|
||||
<CurrencyInputPanel
|
||||
field={Field.OUTPUT}
|
||||
value={formattedAmounts[Field.OUTPUT]}
|
||||
disableTokenSelect={true}
|
||||
field={Field.TOKEN_B}
|
||||
value={formattedAmounts[Field.TOKEN_B]}
|
||||
onUserInput={onUserInput}
|
||||
onMax={() => {
|
||||
maxAmountOutput && onMax(maxAmountOutput?.toExact(), Field.OUTPUT)
|
||||
maxAmounts[Field.TOKEN_B] && onUserInput(Field.TOKEN_B, maxAmounts[Field.TOKEN_B].toExact())
|
||||
}}
|
||||
showMaxButton={!atMaxAmountOutput}
|
||||
token={tokens[Field.OUTPUT]}
|
||||
onTokenSelection={address => onTokenSelection(Field.OUTPUT, address)}
|
||||
showMaxButton={!atMaxAmounts[Field.TOKEN_B]}
|
||||
token={tokens[Field.TOKEN_B]}
|
||||
pair={pair}
|
||||
id="add-liquidity-input-token1"
|
||||
id="add-liquidity-input-tokenb"
|
||||
/>
|
||||
{tokens[Field.OUTPUT] && tokens[Field.INPUT] && (
|
||||
{tokens[Field.TOKEN_A] && tokens[Field.TOKEN_B] && (
|
||||
<>
|
||||
<GreyCard padding="0px" borderRadius={'20px'}>
|
||||
<RowBetween padding="1rem">
|
||||
@ -738,61 +385,58 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps<
|
||||
</GreyCard>
|
||||
</>
|
||||
)}
|
||||
{isValid ? (
|
||||
!inputApproved ? (
|
||||
<ButtonLight
|
||||
onClick={() => {
|
||||
approveAmount(Field.INPUT)
|
||||
}}
|
||||
disabled={pendingApprovalInput}
|
||||
>
|
||||
{pendingApprovalInput ? (
|
||||
<Dots>Approving {tokens[Field.INPUT]?.symbol}</Dots>
|
||||
|
||||
{!account ? (
|
||||
<ButtonLight onClick={toggleWalletModal}>Connect Wallet</ButtonLight>
|
||||
) : approvalA === ApprovalState.NOT_APPROVED || approvalA === ApprovalState.PENDING ? (
|
||||
<ButtonLight onClick={approveACallback} disabled={approvalA === ApprovalState.PENDING}>
|
||||
{approvalA === ApprovalState.PENDING ? (
|
||||
<Dots>Approving {tokens[Field.TOKEN_A]?.symbol}</Dots>
|
||||
) : (
|
||||
'Approve ' + tokens[Field.INPUT]?.symbol
|
||||
'Approve ' + tokens[Field.TOKEN_A]?.symbol
|
||||
)}
|
||||
</ButtonLight>
|
||||
) : !outputApproved ? (
|
||||
<ButtonLight
|
||||
onClick={() => {
|
||||
approveAmount(Field.OUTPUT)
|
||||
}}
|
||||
disabled={pendingApprovalOutput}
|
||||
>
|
||||
{pendingApprovalOutput ? (
|
||||
<Dots>Approving {tokens[Field.OUTPUT]?.symbol}</Dots>
|
||||
) : approvalB === ApprovalState.NOT_APPROVED || approvalB === ApprovalState.PENDING ? (
|
||||
<ButtonLight onClick={approveBCallback} disabled={approvalB === ApprovalState.PENDING}>
|
||||
{approvalB === ApprovalState.PENDING ? (
|
||||
<Dots>Approving {tokens[Field.TOKEN_B]?.symbol}</Dots>
|
||||
) : (
|
||||
'Approve ' + tokens[Field.OUTPUT]?.symbol
|
||||
'Approve ' + tokens[Field.TOKEN_B]?.symbol
|
||||
)}
|
||||
</ButtonLight>
|
||||
) : (
|
||||
<ButtonPrimary
|
||||
<ButtonError
|
||||
onClick={() => {
|
||||
setShowConfirm(true)
|
||||
}}
|
||||
disabled={!isValid}
|
||||
error={!isValid && !!parsedAmounts[Field.TOKEN_A] && !!parsedAmounts[Field.TOKEN_B]}
|
||||
>
|
||||
<Text fontSize={20} fontWeight={500}>
|
||||
Supply
|
||||
{error ?? 'Supply'}
|
||||
</Text>
|
||||
</ButtonPrimary>
|
||||
)
|
||||
) : (
|
||||
<ButtonPrimary disabled={true}>
|
||||
<Text fontSize={20} fontWeight={500}>
|
||||
{generalError ? generalError : inputError ? inputError : outputError ? outputError : 'Supply'}
|
||||
</Text>
|
||||
</ButtonPrimary>
|
||||
</ButtonError>
|
||||
)}
|
||||
</AutoColumn>
|
||||
|
||||
{!noLiquidity && (
|
||||
<FixedBottom>
|
||||
<AutoColumn>
|
||||
<PositionCard pair={pair} minimal={true} />
|
||||
</AutoColumn>
|
||||
</FixedBottom>
|
||||
)}
|
||||
</Wrapper>
|
||||
</AppBody>
|
||||
|
||||
{isValid && !!parsedAmounts[Field.TOKEN_A] && !!parsedAmounts[Field.TOKEN_B] ? (
|
||||
<AdvancedSwapDetailsDropdown
|
||||
rawSlippage={allowedSlippage}
|
||||
deadline={deadline}
|
||||
showAdvanced={showAdvanced}
|
||||
setShowAdvanced={setShowAdvanced}
|
||||
setDeadline={setDeadline}
|
||||
setRawSlippage={setAllowedSlippage}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{pair && !noLiquidity ? (
|
||||
<AutoColumn style={{ minWidth: '20rem', marginTop: '1rem' }}>
|
||||
<PositionCard pair={pair} minimal={true} />
|
||||
</AutoColumn>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -67,6 +67,10 @@ const BackgroundGradient = styled.div`
|
||||
}
|
||||
`
|
||||
|
||||
const Marginer = styled.div`
|
||||
margin-top: 5rem;
|
||||
`
|
||||
|
||||
let Router: React.ComponentType
|
||||
if (process.env.PUBLIC_URL === '.') {
|
||||
Router = HashRouter
|
||||
@ -99,6 +103,7 @@ export default function App() {
|
||||
<Route component={RedirectPathToSwapOnly} />
|
||||
</Switch>
|
||||
</Web3ReactManager>
|
||||
<Marginer />
|
||||
<Footer />
|
||||
</BodyWrapper>
|
||||
<BackgroundGradient />
|
||||
|
@ -2,17 +2,15 @@ import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import NavigationTabs from '../components/NavigationTabs'
|
||||
|
||||
export const Body = styled.div`
|
||||
const Body = styled.div`
|
||||
position: relative;
|
||||
max-width: 420px;
|
||||
width: 100%;
|
||||
/* min-height: 340px; */
|
||||
background: ${({ theme }) => theme.bg1};
|
||||
box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.01), 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 16px 24px rgba(0, 0, 0, 0.04),
|
||||
0px 24px 32px rgba(0, 0, 0, 0.01);
|
||||
border-radius: 30px;
|
||||
padding: 1rem;
|
||||
position: relative;
|
||||
margin-bottom: 10rem;
|
||||
`
|
||||
|
||||
/**
|
||||
|
@ -30,14 +30,20 @@ import { useSendCallback } from '../../hooks/useSendCallback'
|
||||
import { useSwapCallback } from '../../hooks/useSwapCallback'
|
||||
import { useWalletModalToggle } from '../../state/application/hooks'
|
||||
import { Field } from '../../state/swap/actions'
|
||||
import { useDefaultsFromURL, useDerivedSwapInfo, useSwapActionHandlers, useSwapState } from '../../state/swap/hooks'
|
||||
import {
|
||||
useDefaultsFromURLSearch,
|
||||
useDerivedSwapInfo,
|
||||
useSwapActionHandlers,
|
||||
useSwapState
|
||||
} from '../../state/swap/hooks'
|
||||
import { useAllTokenBalancesTreatingWETHasETH } from '../../state/wallet/hooks'
|
||||
import { CursorPointer, TYPE } from '../../theme'
|
||||
import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown, warningServerity } from '../../utils/prices'
|
||||
import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown, warningSeverity } from '../../utils/prices'
|
||||
import AppBody from '../AppBody'
|
||||
import { PriceSlippageWarningCard } from '../../components/swap/PriceSlippageWarningCard'
|
||||
|
||||
export default function Send({ location: { search } }: RouteComponentProps) {
|
||||
useDefaultsFromURL(search)
|
||||
useDefaultsFromURLSearch(search)
|
||||
|
||||
// text translation
|
||||
// const { t } = useTranslation()
|
||||
@ -173,7 +179,7 @@ export default function Send({ location: { search } }: RouteComponentProps) {
|
||||
const [showInverted, setShowInverted] = useState<boolean>(false)
|
||||
|
||||
// warnings on slippage
|
||||
const severity = !sendingWithSwap ? 0 : warningServerity(priceImpactWithoutFee)
|
||||
const severity = !sendingWithSwap ? 0 : warningSeverity(priceImpactWithoutFee)
|
||||
|
||||
function modalHeader() {
|
||||
if (!sendingWithSwap) {
|
||||
@ -492,6 +498,9 @@ export default function Send({ location: { search } }: RouteComponentProps) {
|
||||
)}
|
||||
<V1TradeLink v1TradeLinkIfBetter={v1TradeLinkIfBetter} />
|
||||
</BottomGrouping>
|
||||
</Wrapper>
|
||||
</AppBody>
|
||||
|
||||
{bestTrade && (
|
||||
<AdvancedSwapDetailsDropdown
|
||||
trade={bestTrade}
|
||||
@ -499,13 +508,16 @@ export default function Send({ location: { search } }: RouteComponentProps) {
|
||||
deadline={deadline}
|
||||
showAdvanced={showAdvanced}
|
||||
setShowAdvanced={setShowAdvanced}
|
||||
priceImpactWithoutFee={priceImpactWithoutFee}
|
||||
setDeadline={setDeadline}
|
||||
setRawSlippage={setAllowedSlippage}
|
||||
/>
|
||||
)}
|
||||
</Wrapper>
|
||||
</AppBody>
|
||||
|
||||
{priceImpactWithoutFee && severity > 2 && (
|
||||
<AutoColumn gap="lg" style={{ marginTop: '1rem' }}>
|
||||
<PriceSlippageWarningCard priceSlippage={priceImpactWithoutFee} />
|
||||
</AutoColumn>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -27,23 +27,30 @@ import { useApproveCallbackFromTrade, ApprovalState } from '../../hooks/useAppro
|
||||
import { useSwapCallback } from '../../hooks/useSwapCallback'
|
||||
import { useWalletModalToggle } from '../../state/application/hooks'
|
||||
import { Field } from '../../state/swap/actions'
|
||||
import { useDefaultsFromURL, useDerivedSwapInfo, useSwapActionHandlers, useSwapState } from '../../state/swap/hooks'
|
||||
import {
|
||||
useDefaultsFromURLSearch,
|
||||
useDerivedSwapInfo,
|
||||
useSwapActionHandlers,
|
||||
useSwapState
|
||||
} from '../../state/swap/hooks'
|
||||
import { CursorPointer, TYPE } from '../../theme'
|
||||
import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown, warningServerity } from '../../utils/prices'
|
||||
import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown, warningSeverity } from '../../utils/prices'
|
||||
import AppBody from '../AppBody'
|
||||
import { PriceSlippageWarningCard } from '../../components/swap/PriceSlippageWarningCard'
|
||||
|
||||
export default function Swap({ location: { search } }: RouteComponentProps) {
|
||||
useDefaultsFromURL(search)
|
||||
// text translation
|
||||
// const { t } = useTranslation()
|
||||
useDefaultsFromURLSearch(search)
|
||||
|
||||
const { chainId, account } = useActiveWeb3React()
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
// toggle wallet when disconnected
|
||||
const toggleWalletModal = useWalletModalToggle()
|
||||
|
||||
// swap state
|
||||
const { independentField, typedValue } = useSwapState()
|
||||
const { bestTrade, tokenBalances, parsedAmounts, tokens, error, v1TradeLinkIfBetter } = useDerivedSwapInfo()
|
||||
const { onSwitchTokens, onTokenSelection, onUserInput } = useSwapActionHandlers()
|
||||
const isValid = !error
|
||||
const dependentField: Field = independentField === Field.INPUT ? Field.OUTPUT : Field.INPUT
|
||||
|
||||
@ -58,6 +65,11 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
|
||||
const [deadline, setDeadline] = useState<number>(DEFAULT_DEADLINE_FROM_NOW)
|
||||
const [allowedSlippage, setAllowedSlippage] = useState<number>(INITIAL_ALLOWED_SLIPPAGE)
|
||||
|
||||
const formattedAmounts = {
|
||||
[independentField]: typedValue,
|
||||
[dependentField]: parsedAmounts[dependentField] ? parsedAmounts[dependentField].toSignificant(6) : ''
|
||||
}
|
||||
|
||||
const route = bestTrade?.route
|
||||
const userHasSpecifiedInputOutput =
|
||||
!!tokens[Field.INPUT] &&
|
||||
@ -69,13 +81,6 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
|
||||
// check whether the user has approved the router on the input token
|
||||
const [approval, approveCallback] = useApproveCallbackFromTrade(bestTrade, allowedSlippage)
|
||||
|
||||
const formattedAmounts = {
|
||||
[independentField]: typedValue,
|
||||
[dependentField]: parsedAmounts[dependentField] ? parsedAmounts[dependentField].toSignificant(6) : ''
|
||||
}
|
||||
|
||||
const { onSwitchTokens, onTokenSelection, onUserInput } = useSwapActionHandlers()
|
||||
|
||||
const maxAmountInput: TokenAmount =
|
||||
!!tokenBalances[Field.INPUT] &&
|
||||
!!tokens[Field.INPUT] &&
|
||||
@ -88,7 +93,7 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
|
||||
: tokenBalances[Field.INPUT]
|
||||
: undefined
|
||||
const atMaxAmountInput: boolean =
|
||||
!!maxAmountInput && !!parsedAmounts[Field.INPUT] ? maxAmountInput.equalTo(parsedAmounts[Field.INPUT]) : undefined
|
||||
maxAmountInput && parsedAmounts[Field.INPUT] ? maxAmountInput.equalTo(parsedAmounts[Field.INPUT]) : undefined
|
||||
|
||||
const slippageAdjustedAmounts = computeSlippageAdjustedAmounts(bestTrade, allowedSlippage)
|
||||
|
||||
@ -130,7 +135,7 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
|
||||
const [showInverted, setShowInverted] = useState<boolean>(false)
|
||||
|
||||
// warnings on slippage
|
||||
const priceImpactSeverity = warningServerity(priceImpactWithoutFee)
|
||||
const priceImpactSeverity = warningSeverity(priceImpactWithoutFee)
|
||||
|
||||
function modalHeader() {
|
||||
return (
|
||||
@ -259,13 +264,7 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
|
||||
</AutoColumn>
|
||||
<BottomGrouping>
|
||||
{!account ? (
|
||||
<ButtonLight
|
||||
onClick={() => {
|
||||
toggleWalletModal()
|
||||
}}
|
||||
>
|
||||
Connect Wallet
|
||||
</ButtonLight>
|
||||
<ButtonLight onClick={toggleWalletModal}>Connect Wallet</ButtonLight>
|
||||
) : noRoute && userHasSpecifiedInputOutput ? (
|
||||
<GreyCard style={{ textAlign: 'center' }}>
|
||||
<TYPE.main mb="4px">Insufficient liquidity for this trade.</TYPE.main>
|
||||
@ -294,6 +293,9 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
|
||||
)}
|
||||
<V1TradeLink v1TradeLinkIfBetter={v1TradeLinkIfBetter} />
|
||||
</BottomGrouping>
|
||||
</Wrapper>
|
||||
</AppBody>
|
||||
|
||||
{bestTrade && (
|
||||
<AdvancedSwapDetailsDropdown
|
||||
trade={bestTrade}
|
||||
@ -301,13 +303,16 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
|
||||
deadline={deadline}
|
||||
showAdvanced={showAdvanced}
|
||||
setShowAdvanced={setShowAdvanced}
|
||||
priceImpactWithoutFee={priceImpactWithoutFee}
|
||||
setDeadline={setDeadline}
|
||||
setRawSlippage={setAllowedSlippage}
|
||||
/>
|
||||
)}
|
||||
</Wrapper>
|
||||
</AppBody>
|
||||
|
||||
{priceImpactWithoutFee && priceImpactSeverity > 2 && (
|
||||
<AutoColumn gap="lg" style={{ marginTop: '1rem' }}>
|
||||
<PriceSlippageWarningCard priceSlippage={priceImpactWithoutFee} />
|
||||
</AutoColumn>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -1,11 +1,14 @@
|
||||
import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit'
|
||||
import { save, load } from 'redux-localstorage-simple'
|
||||
|
||||
import application from './application/reducer'
|
||||
import { updateVersion } from './user/actions'
|
||||
import user from './user/reducer'
|
||||
import transactions from './transactions/reducer'
|
||||
import wallet from './wallet/reducer'
|
||||
import swap from './swap/reducer'
|
||||
import transactions from './transactions/reducer'
|
||||
import { save, load } from 'redux-localstorage-simple'
|
||||
import mint from './mint/reducer'
|
||||
|
||||
import { updateVersion } from './user/actions'
|
||||
|
||||
const PERSISTED_KEYS: string[] = ['user', 'transactions']
|
||||
|
||||
@ -15,7 +18,8 @@ const store = configureStore({
|
||||
user,
|
||||
transactions,
|
||||
wallet,
|
||||
swap
|
||||
swap,
|
||||
mint
|
||||
},
|
||||
middleware: [...getDefaultMiddleware(), save({ states: PERSISTED_KEYS })],
|
||||
preloadedState: load({ states: PERSISTED_KEYS })
|
||||
|
13
src/state/mint/actions.ts
Normal file
13
src/state/mint/actions.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { createAction } from '@reduxjs/toolkit'
|
||||
import { RouteComponentProps } from 'react-router-dom'
|
||||
|
||||
export enum Field {
|
||||
TOKEN_A = 'TOKEN_A',
|
||||
TOKEN_B = 'TOKEN_B'
|
||||
}
|
||||
|
||||
export const setDefaultsFromURLMatchParams = createAction<{
|
||||
chainId: number
|
||||
params: RouteComponentProps<{ [k: string]: string }>['match']['params']
|
||||
}>('setDefaultsFromMatch')
|
||||
export const typeInput = createAction<{ field: Field; typedValue: string; noLiquidity: boolean }>('typeInputMint')
|
202
src/state/mint/hooks.ts
Normal file
202
src/state/mint/hooks.ts
Normal file
@ -0,0 +1,202 @@
|
||||
import { useEffect, useCallback, useMemo } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { Token, TokenAmount, Route, JSBI, Price, Percent, Pair } from '@uniswap/sdk'
|
||||
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { AppDispatch, AppState } from '../index'
|
||||
import { setDefaultsFromURLMatchParams, Field, typeInput } from './actions'
|
||||
import { useTokenByAddressAndAutomaticallyAdd } from '../../hooks/Tokens'
|
||||
import { useTokenBalancesTreatWETHAsETH } from '../wallet/hooks'
|
||||
import { usePair } from '../../data/Reserves'
|
||||
import { useTotalSupply } from '../../data/TotalSupply'
|
||||
import { tryParseAmount } from '../swap/hooks'
|
||||
|
||||
const ZERO = JSBI.BigInt(0)
|
||||
|
||||
export function useMintState(): AppState['mint'] {
|
||||
return useSelector<AppState, AppState['mint']>(state => state.mint)
|
||||
}
|
||||
|
||||
export function useDerivedMintInfo(): {
|
||||
dependentField: Field
|
||||
tokens: { [field in Field]?: Token }
|
||||
pair?: Pair | null
|
||||
tokenBalances: { [field in Field]?: TokenAmount }
|
||||
parsedAmounts: { [field in Field]?: TokenAmount }
|
||||
price?: Price
|
||||
noLiquidity?: boolean
|
||||
liquidityMinted?: TokenAmount
|
||||
poolTokenPercentage?: Percent
|
||||
error?: string
|
||||
} {
|
||||
const { account } = useActiveWeb3React()
|
||||
|
||||
const {
|
||||
independentField,
|
||||
typedValue,
|
||||
otherTypedValue,
|
||||
[Field.TOKEN_A]: { address: tokenAAddress },
|
||||
[Field.TOKEN_B]: { address: tokenBAddress }
|
||||
} = useMintState()
|
||||
|
||||
const dependentField = independentField === Field.TOKEN_A ? Field.TOKEN_B : Field.TOKEN_A
|
||||
|
||||
// tokens
|
||||
const tokenA = useTokenByAddressAndAutomaticallyAdd(tokenAAddress)
|
||||
const tokenB = useTokenByAddressAndAutomaticallyAdd(tokenBAddress)
|
||||
const tokens: { [field in Field]?: Token } = useMemo(
|
||||
() => ({
|
||||
[Field.TOKEN_A]: tokenA,
|
||||
[Field.TOKEN_B]: tokenB
|
||||
}),
|
||||
[tokenA, tokenB]
|
||||
)
|
||||
|
||||
// pair
|
||||
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 = useMemo(
|
||||
() =>
|
||||
!noLiquidity && pair && tokens[independentField] ? new Route([pair], tokens[Field.TOKEN_A] as Token) : undefined,
|
||||
[noLiquidity, pair, tokens, independentField]
|
||||
)
|
||||
|
||||
// balances
|
||||
const relevantTokenBalances = useTokenBalancesTreatWETHAsETH(account ?? undefined, [
|
||||
tokens[Field.TOKEN_A],
|
||||
tokens[Field.TOKEN_B]
|
||||
])
|
||||
const tokenBalances: { [field in Field]?: TokenAmount } = {
|
||||
[Field.TOKEN_A]: relevantTokenBalances?.[tokens[Field.TOKEN_A]?.address ?? ''],
|
||||
[Field.TOKEN_B]: relevantTokenBalances?.[tokens[Field.TOKEN_B]?.address ?? '']
|
||||
}
|
||||
|
||||
// amounts
|
||||
const independentAmount = tryParseAmount(typedValue, tokens[independentField])
|
||||
const dependentAmount = useMemo(() => {
|
||||
if (noLiquidity && otherTypedValue && tokens[dependentField]) {
|
||||
return tryParseAmount(otherTypedValue, tokens[dependentField])
|
||||
} else if (route && independentAmount) {
|
||||
return dependentField === Field.TOKEN_B
|
||||
? route.midPrice.quote(independentAmount)
|
||||
: route.midPrice.invert().quote(independentAmount)
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}, [noLiquidity, otherTypedValue, tokens, dependentField, independentAmount, route])
|
||||
const parsedAmounts = {
|
||||
[Field.TOKEN_A]: independentField === Field.TOKEN_A ? independentAmount : dependentAmount,
|
||||
[Field.TOKEN_B]: independentField === Field.TOKEN_A ? dependentAmount : independentAmount
|
||||
}
|
||||
|
||||
const price = useMemo(() => {
|
||||
if (
|
||||
noLiquidity &&
|
||||
tokens[Field.TOKEN_A] &&
|
||||
tokens[Field.TOKEN_B] &&
|
||||
parsedAmounts[Field.TOKEN_A] &&
|
||||
parsedAmounts[Field.TOKEN_B]
|
||||
) {
|
||||
return new Price(
|
||||
tokens[Field.TOKEN_A] as Token,
|
||||
tokens[Field.TOKEN_B] as Token,
|
||||
(parsedAmounts[Field.TOKEN_A] as TokenAmount).raw,
|
||||
(parsedAmounts[Field.TOKEN_B] as TokenAmount).raw
|
||||
)
|
||||
} else if (route) {
|
||||
return route.midPrice
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}, [noLiquidity, tokens, parsedAmounts, route])
|
||||
|
||||
// liquidity minted
|
||||
const totalSupply = useTotalSupply(pair?.liquidityToken)
|
||||
const liquidityMinted = useMemo(() => {
|
||||
if (pair && totalSupply && parsedAmounts[Field.TOKEN_A] && parsedAmounts[Field.TOKEN_B]) {
|
||||
return pair.getLiquidityMinted(
|
||||
totalSupply,
|
||||
parsedAmounts[Field.TOKEN_A] as TokenAmount,
|
||||
parsedAmounts[Field.TOKEN_B] as TokenAmount
|
||||
)
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}, [pair, totalSupply, parsedAmounts])
|
||||
|
||||
const poolTokenPercentage = useMemo(() => {
|
||||
if (liquidityMinted && totalSupply) {
|
||||
return new Percent(liquidityMinted.raw, totalSupply.add(liquidityMinted).raw)
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}, [liquidityMinted, totalSupply])
|
||||
|
||||
let error: string | undefined
|
||||
if (!account) {
|
||||
error = 'Connect Wallet'
|
||||
}
|
||||
|
||||
if (!parsedAmounts[Field.TOKEN_A] || !parsedAmounts[Field.TOKEN_B]) {
|
||||
error = error ?? 'Enter an amount'
|
||||
}
|
||||
|
||||
if (
|
||||
parsedAmounts[Field.TOKEN_A] &&
|
||||
tokenBalances?.[Field.TOKEN_A]?.lessThan(parsedAmounts[Field.TOKEN_A] as TokenAmount)
|
||||
) {
|
||||
error = 'Insufficient ' + tokens[Field.TOKEN_A]?.symbol + ' balance'
|
||||
}
|
||||
|
||||
if (
|
||||
parsedAmounts[Field.TOKEN_B] &&
|
||||
tokenBalances?.[Field.TOKEN_B]?.lessThan(parsedAmounts[Field.TOKEN_B] as TokenAmount)
|
||||
) {
|
||||
error = 'Insufficient ' + tokens[Field.TOKEN_B]?.symbol + ' balance'
|
||||
}
|
||||
|
||||
return {
|
||||
dependentField,
|
||||
tokens,
|
||||
pair,
|
||||
tokenBalances,
|
||||
parsedAmounts,
|
||||
price,
|
||||
noLiquidity,
|
||||
liquidityMinted,
|
||||
poolTokenPercentage,
|
||||
error
|
||||
}
|
||||
}
|
||||
|
||||
export function useMintActionHandlers(): {
|
||||
onUserInput: (field: Field, typedValue: string) => void
|
||||
} {
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
|
||||
const { noLiquidity } = useDerivedMintInfo()
|
||||
|
||||
const onUserInput = useCallback(
|
||||
(field: Field, typedValue: string) => {
|
||||
dispatch(typeInput({ field, typedValue, noLiquidity: noLiquidity === true ? true : false }))
|
||||
},
|
||||
[dispatch, noLiquidity]
|
||||
)
|
||||
|
||||
return {
|
||||
onUserInput
|
||||
}
|
||||
}
|
||||
|
||||
// updates the mint 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])
|
||||
}
|
40
src/state/mint/reducer.test.ts
Normal file
40
src/state/mint/reducer.test.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { ChainId, WETH } from '@uniswap/sdk'
|
||||
import { createStore, Store } from 'redux'
|
||||
|
||||
import { Field, setDefaultsFromURLMatchParams } from './actions'
|
||||
import reducer, { MintState } from './reducer'
|
||||
|
||||
describe('mint reducer', () => {
|
||||
let store: Store<MintState>
|
||||
|
||||
beforeEach(() => {
|
||||
store = createStore(reducer, {
|
||||
independentField: Field.TOKEN_A,
|
||||
typedValue: '',
|
||||
otherTypedValue: '',
|
||||
[Field.TOKEN_A]: { address: '' },
|
||||
[Field.TOKEN_B]: { address: '' }
|
||||
})
|
||||
})
|
||||
|
||||
describe('setDefaultsFromURLMatchParams', () => {
|
||||
test('ETH to DAI', () => {
|
||||
store.dispatch(
|
||||
setDefaultsFromURLMatchParams({
|
||||
chainId: ChainId.MAINNET,
|
||||
params: {
|
||||
tokens: 'ETH-0x6b175474e89094c44da98b954eedeac495271d0f'
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
expect(store.getState()).toEqual({
|
||||
independentField: Field.TOKEN_A,
|
||||
typedValue: '',
|
||||
otherTypedValue: '',
|
||||
[Field.TOKEN_A]: { address: WETH[ChainId.MAINNET].address },
|
||||
[Field.TOKEN_B]: { address: '0x6b175474e89094c44da98b954eedeac495271d0f' }
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
97
src/state/mint/reducer.ts
Normal file
97
src/state/mint/reducer.ts
Normal file
@ -0,0 +1,97 @@
|
||||
import { createReducer } from '@reduxjs/toolkit'
|
||||
import { ChainId, WETH } from '@uniswap/sdk'
|
||||
|
||||
import { isAddress } from '../../utils'
|
||||
import { Field, setDefaultsFromURLMatchParams, typeInput } from './actions'
|
||||
|
||||
export interface MintState {
|
||||
readonly independentField: Field
|
||||
readonly typedValue: string
|
||||
readonly otherTypedValue: string // for the case when there's no liquidity
|
||||
readonly [Field.TOKEN_A]: {
|
||||
readonly address: string
|
||||
}
|
||||
readonly [Field.TOKEN_B]: {
|
||||
readonly address: string
|
||||
}
|
||||
}
|
||||
|
||||
const initialState: MintState = {
|
||||
independentField: Field.TOKEN_A,
|
||||
typedValue: '',
|
||||
otherTypedValue: '',
|
||||
[Field.TOKEN_A]: {
|
||||
address: ''
|
||||
},
|
||||
[Field.TOKEN_B]: {
|
||||
address: ''
|
||||
}
|
||||
}
|
||||
|
||||
function parseTokens(chainId: number, tokens: string): string[] {
|
||||
return (
|
||||
tokens
|
||||
// split by '-'
|
||||
.split('-')
|
||||
// map to addresses
|
||||
.map((token): string =>
|
||||
isAddress(token)
|
||||
? token
|
||||
: token.toLowerCase() === 'ETH'.toLowerCase()
|
||||
? WETH[chainId as ChainId]?.address ?? ''
|
||||
: ''
|
||||
)
|
||||
//remove duplicates
|
||||
.filter((token, i, array) => array.indexOf(token) === i)
|
||||
// add two empty elements for cases where the array is length 0
|
||||
.concat(['', ''])
|
||||
// only consider the first 2 elements
|
||||
.slice(0, 2)
|
||||
)
|
||||
}
|
||||
|
||||
export default createReducer<MintState>(initialState, builder =>
|
||||
builder
|
||||
.addCase(setDefaultsFromURLMatchParams, (state, { payload: { chainId, params } }) => {
|
||||
const tokens = parseTokens(chainId, params?.tokens ?? '')
|
||||
return {
|
||||
independentField: Field.TOKEN_A,
|
||||
typedValue: '',
|
||||
otherTypedValue: '',
|
||||
[Field.TOKEN_A]: {
|
||||
address: tokens[0]
|
||||
},
|
||||
[Field.TOKEN_B]: {
|
||||
address: tokens[1]
|
||||
}
|
||||
}
|
||||
})
|
||||
.addCase(typeInput, (state, { payload: { field, typedValue, noLiquidity } }) => {
|
||||
if (noLiquidity) {
|
||||
// they're typing into the field they've last typed in
|
||||
if (field === state.independentField) {
|
||||
return {
|
||||
...state,
|
||||
independentField: field,
|
||||
typedValue
|
||||
}
|
||||
}
|
||||
// they're typing into a new field, store the other value
|
||||
else {
|
||||
return {
|
||||
...state,
|
||||
independentField: field,
|
||||
typedValue,
|
||||
otherTypedValue: state.typedValue
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
...state,
|
||||
independentField: field,
|
||||
typedValue,
|
||||
otherTypedValue: ''
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
@ -5,7 +5,7 @@ export enum Field {
|
||||
OUTPUT = 'OUTPUT'
|
||||
}
|
||||
|
||||
export const setDefaultsFromURL = createAction<{ chainId: number; queryString?: string }>('setDefaultsFromURL')
|
||||
export const setDefaultsFromURLSearch = createAction<{ chainId: number; queryString?: string }>('setDefaultsFromURL')
|
||||
export const selectToken = createAction<{ field: Field; address: string }>('selectToken')
|
||||
export const switchTokens = createAction<void>('switchTokens')
|
||||
export const typeInput = createAction<{ field: Field; typedValue: string }>('typeInput')
|
||||
|
@ -7,7 +7,7 @@ import { useTokenByAddressAndAutomaticallyAdd } from '../../hooks/Tokens'
|
||||
import { useTradeExactIn, useTradeExactOut } from '../../hooks/Trades'
|
||||
import { AppDispatch, AppState } from '../index'
|
||||
import { useTokenBalancesTreatWETHAsETH } from '../wallet/hooks'
|
||||
import { Field, selectToken, setDefaultsFromURL, switchTokens, typeInput } from './actions'
|
||||
import { Field, selectToken, setDefaultsFromURLSearch, switchTokens, typeInput } from './actions'
|
||||
import { useV1TradeLinkIfBetter } from '../../data/V1'
|
||||
import { V1_TRADE_LINK_THRESHOLD } from '../../constants'
|
||||
|
||||
@ -52,7 +52,7 @@ export function useSwapActionHandlers(): {
|
||||
}
|
||||
|
||||
// try to parse a user entered amount for a given token
|
||||
function tryParseAmount(value?: string, token?: Token): TokenAmount | undefined {
|
||||
export function tryParseAmount(value?: string, token?: Token): TokenAmount | undefined {
|
||||
if (!value || !token) {
|
||||
return
|
||||
}
|
||||
@ -155,11 +155,11 @@ export function useDerivedSwapInfo(): {
|
||||
|
||||
// updates the swap state to use the defaults for a given network whenever the query
|
||||
// string updates
|
||||
export function useDefaultsFromURL(search?: string) {
|
||||
export function useDefaultsFromURLSearch(search?: string) {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
useEffect(() => {
|
||||
if (!chainId) return
|
||||
dispatch(setDefaultsFromURL({ chainId, queryString: search }))
|
||||
dispatch(setDefaultsFromURLSearch({ chainId, queryString: search }))
|
||||
}, [dispatch, search, chainId])
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { ChainId, WETH } from '@uniswap/sdk'
|
||||
import { createStore, Store } from 'redux'
|
||||
import { Field, setDefaultsFromURL } from './actions'
|
||||
import { Field, setDefaultsFromURLSearch } from './actions'
|
||||
import reducer, { SwapState } from './reducer'
|
||||
|
||||
describe('swap reducer', () => {
|
||||
@ -18,7 +18,7 @@ describe('swap reducer', () => {
|
||||
describe('setDefaultsFromURL', () => {
|
||||
test('ETH to DAI', () => {
|
||||
store.dispatch(
|
||||
setDefaultsFromURL({
|
||||
setDefaultsFromURLSearch({
|
||||
chainId: ChainId.MAINNET,
|
||||
queryString:
|
||||
'?inputCurrency=ETH&outputCurrency=0x6b175474e89094c44da98b954eedeac495271d0f&exactAmount=20.5&exactField=outPUT'
|
||||
@ -35,7 +35,7 @@ describe('swap reducer', () => {
|
||||
|
||||
test('does not duplicate eth for invalid output token', () => {
|
||||
store.dispatch(
|
||||
setDefaultsFromURL({
|
||||
setDefaultsFromURLSearch({
|
||||
chainId: ChainId.MAINNET,
|
||||
queryString: '?outputCurrency=invalid'
|
||||
})
|
||||
@ -51,7 +51,7 @@ describe('swap reducer', () => {
|
||||
|
||||
test('output ETH only', () => {
|
||||
store.dispatch(
|
||||
setDefaultsFromURL({
|
||||
setDefaultsFromURLSearch({
|
||||
chainId: ChainId.MAINNET,
|
||||
queryString: '?outputCurrency=eth&exactAmount=20.5'
|
||||
})
|
||||
|
@ -2,7 +2,7 @@ import { parse } from 'qs'
|
||||
import { createReducer } from '@reduxjs/toolkit'
|
||||
import { ChainId, WETH } from '@uniswap/sdk'
|
||||
import { isAddress } from '../../utils'
|
||||
import { Field, selectToken, setDefaultsFromURL, switchTokens, typeInput } from './actions'
|
||||
import { Field, selectToken, setDefaultsFromURLSearch, switchTokens, typeInput } from './actions'
|
||||
|
||||
export interface SwapState {
|
||||
readonly independentField: Field
|
||||
@ -47,7 +47,7 @@ function parseIndependentFieldURLParameter(urlParam: any): Field {
|
||||
|
||||
export default createReducer<SwapState>(initialState, builder =>
|
||||
builder
|
||||
.addCase(setDefaultsFromURL, (state, { payload: { queryString, chainId } }) => {
|
||||
.addCase(setDefaultsFromURLSearch, (_, { payload: { queryString, chainId } }) => {
|
||||
if (queryString && queryString.length > 1) {
|
||||
const parsedQs = parse(queryString, { parseArrays: false, ignoreQueryPrefix: true })
|
||||
|
||||
|
@ -51,7 +51,7 @@ export function computeSlippageAdjustedAmounts(
|
||||
}
|
||||
}
|
||||
|
||||
export function warningServerity(priceImpact: Percent): 0 | 1 | 2 | 3 {
|
||||
export function warningSeverity(priceImpact: Percent): 0 | 1 | 2 | 3 {
|
||||
if (!priceImpact?.lessThan(ALLOWED_PRICE_IMPACT_HIGH)) return 3
|
||||
if (!priceImpact?.lessThan(ALLOWED_PRICE_IMPACT_MEDIUM)) return 2
|
||||
if (!priceImpact?.lessThan(ALLOWED_PRICE_IMPACT_LOW)) return 1
|
||||
|
Loading…
Reference in New Issue
Block a user