Compare commits

...

28 Commits

Author SHA1 Message Date
Ian Lapham
37a4e2f6e3 More UI bug fixes (#1515)
* fix for error token map parsings

* update varios UI styles

* update padding on & amounts
2021-05-11 22:29:25 -04:00
Moody Salem
19a3b12ca8 bump typechain for faster/less noisy type generation 2021-05-11 16:43:08 -05:00
Noah Zinsmeister
22c1ddf393 swaps to .5% slippage 2021-05-11 15:00:51 -04:00
Noah Zinsmeister
b44ae1a267 clean up parseCurrencyFromURLParameter 2021-05-11 14:19:56 -04:00
Ian Lapham
418dcf0cb2 Various bug fixes (#1501)
* fix for error token map parsings

* fix error on formatting sig figs

* fix various bugs

* no hover cursor

Co-authored-by: Noah Zinsmeister <noahwz@gmail.com>
2021-05-11 14:03:02 -04:00
Noah Zinsmeister
58a508c9d6 .25% -> .30% slippage for v3 swaps 2021-05-11 13:53:15 -04:00
Jorropo
3198129af2 feat(routing): support mirror protocol routing as additional bases (#1375)
* feat(routing): support mirror protocol routing as additional bases

* Fix code style issues with ESLint

Co-authored-by: Lint Action <lint-action@samuelmeuli.com>
2021-05-11 13:28:50 -04:00
John Shutt
89d484d882 feat(uma): uma call option routing (#1385)
* feat(uma): uma call option routing

Signed-off-by: John Shutt <john.d.shutt@gmail.com>

* Fix code style issues with ESLint

Co-authored-by: Lint Action <lint-action@samuelmeuli.com>
Co-authored-by: Moody Salem <moodysalem@users.noreply.github.com>
2021-05-11 13:25:51 -04:00
Callil Capuozzo
fa4688d96c UI improvements (#1505)
* Change price ratio using slash to "per"

* Fix header, toggle copy and increase copy

* Add clearer V2 and migrate buttons

* Fix link

* fix account modal background color

* tweak sig figs

Co-authored-by: Noah Zinsmeister <noahwz@gmail.com>
2021-05-11 13:25:04 -04:00
Moody Salem
7ee761a59e feat: automatic slippage tolerance (#1463)
* automatic slippage tolerance start

* get it compiling

* out of range/in range behavior of slippage tolerance in add

* small useDerivedSwapInfo refactor

* improve useSwapSlippageTolerance

* fix unit test

* thread placeholder slippage through

* small improvement to slippage input behavior

* fix the display bug

* fix tx settings modal ux

* don't pass props unnecessarily

* switch back to static swap slippage for now

bump migrate slippage to .75%

* fix font size

* add flag for auto slippage migration

validate version updates even more

Co-authored-by: Noah Zinsmeister <noahwz@gmail.com>
2021-05-11 13:00:42 -04:00
jochenboesmans
78e95f6073 Add App-level error boundary, referring users to GitHub issue creation (#1464)
* Add App-level error boundary, referring users to GitHub issue creation on page crashes. (#1452)

* Class component is used as boundary since catching errors is apparently not yet possible with hooks.

* EventListener in window was removed and replaced by error boundary's error catch, which now fires a GA exception. The fields it passes are slightly different because React uses slightly different error types.

* Pre-filling issues with dynamic data is possible with POST requests to GitHub's API, but the GH web client seems to only support pre-fill based on templates. Therefore users still need to copy error info themselves.

* Prefill GitHub issues with crash data.

* Added package 'react-device-detect' to include device data such as OS, browser etc. in crash report.
* Included error stack in issue body.
* Used <code> html tag for displaying stack to user.

* Slightly reduce vertical padding on code block.

* Add ua-parser-js for parsing user agent.

* Revert react-device-detect to ^1.6.2 (which is used for mobile detection etc. in components)
2021-05-11 12:09:01 -04:00
Noah Zinsmeister
c67e57505a make price sig figs more consistent 2021-05-11 10:17:06 -04:00
Justin Domingue
30f7385db7 optimize sandtexture.png with .webp (#1502)
Co-authored-by: Justin Domingue <domingue.justin@gmail.com>
2021-05-10 21:32:53 -04:00
Noah Zinsmeister
c0f58ae810 don't use a signer for callStatic contract 2021-05-10 16:59:11 -04:00
Noah Zinsmeister
54dd5476ca fetch fees directly from collect via callStatic (#1500)
* fetch fees directly from collect via callStatic

* don't clear state
2021-05-10 16:30:02 -04:00
Moody Salem
57786335df fix calculateSlippageAmount (#1497) 2021-05-10 14:22:26 -05:00
Joe Butler
948e01a196 Fix typo (#1454) 2021-05-10 14:01:35 -04:00
Noah Zinsmeister
abf127c596 sdk bugfix bump (#1492) 2021-05-10 13:04:07 -04:00
Moody Salem
4d3f870b93 add a test for calculating slippage amounts 2021-05-09 12:31:45 -05:00
Moody Salem
452f2dc3c0 fix slippage amount bug https://github.com/Uniswap/uniswap-interface/issues/1473 2021-05-09 11:30:56 -05:00
Ian Lapham
b6bd59f2b1 Fix bug on formatted token amounts when decimals < sig fig (#1479)
* fix for error token map parsings

* fix error on formatting sig figs
2021-05-07 16:52:00 -04:00
Noah Zinsmeister
0190b5a408 bump sdk to fix add/remove slippage 2021-05-06 18:20:36 -04:00
Noah Zinsmeister
d6030dcd45 add settings tab to migrate 2021-05-06 17:44:05 -04:00
Moody Salem
f0e2a491dc fix(slippage settings): improve slippage tolerance warnings 2021-05-06 11:19:36 -04:00
Moody Salem
021aab6547 fix(wallet): workaround the ethers bug to fix other wallets 2021-05-06 11:10:45 -04:00
Noah Zinsmeister
81af31eec1 estimate gas in migrate v2 2021-05-06 10:40:19 -04:00
Moody Salem
d3898cf900 fix(wallet): workaround for coinbase wallet / fortmatic 2021-05-06 10:09:41 -04:00
Moody Salem
b8f61d5f90 fix(positions list): base/currency ordering 2021-05-06 09:57:12 -04:00
55 changed files with 903 additions and 543 deletions

View File

@@ -20,7 +20,7 @@
"@storybook/react": "^6.1.17",
"@storybook/theming": "^6.1.17",
"@styled-system/css": "^5.1.5",
"@typechain/ethers-v5": "^6.0.5",
"@typechain/ethers-v5": "^7.0.0",
"@types/jest": "^25.2.1",
"@types/lodash.flatmap": "^4.5.6",
"@types/luxon": "^1.24.4",
@@ -36,6 +36,7 @@
"@types/rebass": "^4.0.7",
"@types/styled-components": "^5.1.0",
"@types/testing-library__cypress": "^5.0.5",
"@types/ua-parser-js": "^0.7.35",
"@types/wcag-contrast": "^3.0.0",
"@typescript-eslint/eslint-plugin": "^4.1.0",
"@typescript-eslint/parser": "^4.1.0",
@@ -49,7 +50,7 @@
"@uniswap/v2-sdk": "^1.0.9",
"@uniswap/v3-core": "1.0.0",
"@uniswap/v3-periphery": "1.0.0",
"@uniswap/v3-sdk": "^1.0.3",
"@uniswap/v3-sdk": "^1.0.8",
"@web3-react/core": "^6.0.9",
"@web3-react/fortmatic-connector": "^6.0.9",
"@web3-react/injected-connector": "^6.0.7",
@@ -103,8 +104,9 @@
"start-server-and-test": "^1.11.0",
"styled-components": "^4.2.0",
"styled-system": "^5.1.5",
"typechain": "^4.0.3",
"typechain": "^5.0.0",
"typescript": "^4.2.3",
"ua-parser-js": "^0.7.28",
"use-count-up": "^2.2.5",
"wcag-contrast": "^3.0.0",
"workbox-core": "^6.1.0",
@@ -115,8 +117,8 @@
},
"scripts": {
"compile-contract-types": "yarn compile-external-abi-types && yarn compile-v3-contract-types",
"compile-external-abi-types": "npx typechain --target ethers-v5 --outDir src/abis/types './src/abis/**/*.json'",
"compile-v3-contract-types": "npx typechain --target ethers-v5 --outDir src/types/v3 './node_modules/@uniswap/?(v3-core|v3-periphery)/artifacts/contracts/**/*.json'",
"compile-external-abi-types": "npx typechain --target ethers-v5 --out-dir src/abis/types './src/abis/**/*.json'",
"compile-v3-contract-types": "npx typechain --target ethers-v5 --out-dir src/types/v3 './node_modules/@uniswap/?(v3-core|v3-periphery)/artifacts/contracts/**/*.json'",
"build": "yarn compile-contract-types && react-scripts build",
"integration-test": "start-server-and-test 'serve build -l 3000' http://localhost:3000 'cypress run'",
"postinstall": "yarn compile-contract-types",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 2.2 MiB

View File

@@ -76,7 +76,6 @@ const AccountGroupingRow = styled.div`
`
const AccountSection = styled.div`
background-color: ${({ theme }) => theme.bg1};
padding: 0rem 1rem;
${({ theme }) => theme.mediaWidth.upToMedium`padding: 0rem 1rem 1.5rem 1rem;`};
`

View File

@@ -46,7 +46,7 @@ export default function RangeBadge({
<BadgeWrapper>
{removed ? (
<MouseoverTooltip text={`Your position has 0 liquidity, and is not earning fees.`}>
<Badge variant={BadgeVariant.WARNING_OUTLINE}>
<Badge variant={BadgeVariant.DEFAULT}>
<AlertCircle width={14} height={14} />
&nbsp;
<BadgeText>{t('Inactive')}</BadgeText>

View File

@@ -3,6 +3,7 @@ import React, { useMemo } from 'react'
import useTheme from '../../hooks/useTheme'
import { TYPE } from '../../theme'
import { warningSeverity } from '../../utils/prices'
import HoverInlineText from 'components/HoverInlineText'
export function FiatValue({
fiatValue,
@@ -23,7 +24,8 @@ export function FiatValue({
return (
<TYPE.body fontSize={14} color={fiatValue ? theme.text2 : theme.text4}>
{fiatValue ? '~' : ''}${fiatValue ? Number(fiatValue?.toSignificant(6)).toLocaleString('en') : '-'}
{fiatValue ? '~' : ''}$
<HoverInlineText text={fiatValue ? Number(fiatValue?.toSignificant(6)).toLocaleString('en') : '-'} />{' '}
{priceImpact ? (
<span style={{ color: priceImpactColor }}> ({priceImpact.multiply(-100).toSignificant(3)}%)</span>
) : null}

View File

@@ -0,0 +1,144 @@
import React, { ErrorInfo } from 'react'
import { ExternalLink, ThemedBackground, TYPE } from '../../theme'
import { AutoColumn } from '../Column'
import styled from 'styled-components'
import ReactGA from 'react-ga'
import { getUserAgent } from '../../utils/getUserAgent'
const FallbackWrapper = styled.div`
display: flex;
flex-direction: column;
width: 100%;
align-items: center;
z-index: 1;
`
const BodyWrapper = styled.div<{ margin?: string }>`
position: relative;
margin-top: 1rem;
max-width: 60%;
width: 100%;
`
const CodeBlockWrapper = styled.div`
background: ${({ theme }) => theme.bg0};
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: 24px;
padding: 18px 24px;
color: ${({ theme }) => theme.text1};
`
const LinkWrapper = styled.div`
color: ${({ theme }) => theme.blue1};
padding: 6px 24px;
`
const SomethingWentWrongWrapper = styled.div`
padding: 6px 24px;
`
type ErrorBoundaryState = {
error: Error | null
}
export default class ErrorBoundary extends React.Component<unknown, ErrorBoundaryState> {
constructor(props: unknown) {
super(props)
this.state = { error: null }
}
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { error }
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
ReactGA.exception({
...error,
...errorInfo,
fatal: true,
})
}
render() {
const { error } = this.state
if (error !== null) {
const encodedBody = encodeURIComponent(issueBody(error))
return (
<FallbackWrapper>
<ThemedBackground />
<BodyWrapper>
<AutoColumn gap={'md'}>
<SomethingWentWrongWrapper>
<TYPE.label fontSize={24} fontWeight={600}>
Something went wrong
</TYPE.label>
</SomethingWentWrongWrapper>
<CodeBlockWrapper>
<code>
<TYPE.main fontSize={10}>{error.stack}</TYPE.main>
</code>
</CodeBlockWrapper>
<LinkWrapper>
<ExternalLink
id={`create-github-issue-link`}
href={`https://github.com/Uniswap/uniswap-interface/issues/new?assignees=&labels=bug&body=${encodedBody}&title=Crash report`}
target="_blank"
>
<TYPE.link fontSize={16}>
Create an issue on GitHub
<span></span>
</TYPE.link>
</ExternalLink>
</LinkWrapper>
</AutoColumn>
</BodyWrapper>
</FallbackWrapper>
)
}
return this.props.children
}
}
function issueBody(error: Error): string {
if (!error) throw new Error('no error to report')
const deviceData = getUserAgent()
return `**Bug Description**
App crashed
**Steps to Reproduce**
1. Go to ...
2. Click on ...
...
${
error.name &&
`**Error**
\`\`\`
${error.name}${error.message && `: ${error.message}`}
\`\`\`
`
}
${
error.stack &&
`**Stacktrace**
\`\`\`
${error.stack}
\`\`\`
`
}
${
deviceData &&
`**Device data**
\`\`\`json5
${JSON.stringify(deviceData, null, 2)}
\`\`\`
`
}
`
}

View File

@@ -90,7 +90,7 @@ const HeaderElement = styled.div`
}
${({ theme }) => theme.mediaWidth.upToMedium`
flex-direction: row-reverse;
flex-direction: row-reverse;
align-items: center;
`};
`
@@ -116,6 +116,9 @@ const HeaderLinks = styled(Row)`
grid-auto-flow: column;
grid-gap: 10px;
overflow: auto;
${({ theme }) => theme.mediaWidth.upToMedium`
justify-self: flex-end;
`};
`
const AccountElement = styled.div<{ active: boolean }>`

View File

@@ -0,0 +1,63 @@
import Tooltip from 'components/Tooltip'
import React, { useState } from 'react'
import styled from 'styled-components'
const TextWrapper = styled.span<{ margin: boolean; link: boolean; fontSize?: string; adjustSize?: boolean }>`
position: relative;
margin-left: ${({ margin }) => margin && '4px'};
color: ${({ theme, link }) => (link ? theme.blue1 : theme.text1)};
font-size: ${({ fontSize }) => fontSize ?? 'inherit'};
@media screen and (max-width: 600px) {
font-size: ${({ adjustSize }) => adjustSize && '12px'};
}
`
const HoverInlineText = ({
text,
maxCharacters = 20,
margin = false,
adjustSize = false,
fontSize,
link,
...rest
}: {
text: string
maxCharacters?: number
margin?: boolean
adjustSize?: boolean
fontSize?: string
link?: boolean
}) => {
const [showHover, setShowHover] = useState(false)
if (!text) {
return <span></span>
}
if (text.length > maxCharacters) {
return (
<Tooltip text={text} show={showHover}>
<TextWrapper
onMouseEnter={() => setShowHover(true)}
onMouseLeave={() => setShowHover(false)}
margin={margin}
adjustSize={adjustSize}
link={!!link}
fontSize={fontSize}
{...rest}
>
{' ' + text.slice(0, maxCharacters - 1) + '...'}
</TextWrapper>
</Tooltip>
)
}
return (
<TextWrapper margin={margin} adjustSize={adjustSize} link={!!link} fontSize={fontSize} {...rest}>
{text}
</TextWrapper>
)
}
export default HoverInlineText

View File

@@ -139,7 +139,7 @@ const StepCounter = ({
}}
/>
<InputTitle fontSize={12} textAlign="center">
{tokenB + ' / ' + tokenA}
{tokenB + ' per ' + tokenA}
</InputTitle>
</AutoColumn>
{!locked ? (

View File

@@ -3,10 +3,11 @@ import styled from 'styled-components'
import { darken } from 'polished'
import { useTranslation } from 'react-i18next'
import { NavLink, Link as HistoryLink } from 'react-router-dom'
import { Percent } from '@uniswap/sdk-core'
import { ArrowLeft } from 'react-feather'
import { RowBetween } from '../Row'
import Settings from '../Settings'
import SettingsTab from '../Settings'
import { useDispatch } from 'react-redux'
import { AppDispatch } from 'state'
import { resetMintState } from 'state/mint/actions'
@@ -80,7 +81,6 @@ export function FindPoolTabs({ origin }: { origin: string }) {
<StyledArrowLeft />
</HistoryLink>
<ActiveText>Import Pool</ActiveText>
<Settings />
</RowBetween>
</Tabs>
)
@@ -90,10 +90,12 @@ export function AddRemoveTabs({
adding,
creating,
positionID,
defaultSlippage,
}: {
adding: boolean
creating: boolean
positionID?: string | undefined
defaultSlippage: Percent
}) {
const theme = useTheme()
@@ -118,7 +120,7 @@ export function AddRemoveTabs({
<TYPE.mediumHeader fontWeight={500} fontSize={20}>
{creating ? 'Create a pair' : adding ? 'Add Liquidity' : 'Remove Liquidity'}
</TYPE.mediumHeader>
<Settings />
<SettingsTab placeholderSlippage={defaultSlippage} />
</RowBetween>
</Tabs>
)

View File

@@ -198,28 +198,23 @@ export default function FullPositionCard({ pair, border, stakedBalance }: Positi
<CardNoise />
<AutoColumn gap="12px">
<FixedHeightRow>
<AutoRow gap="8px">
<AutoRow gap="8px" style={{ marginLeft: '8px' }}>
<DoubleCurrencyLogo currency0={currency0} currency1={currency1} size={20} />
<Text fontWeight={500} fontSize={20}>
{!currency0 || !currency1 ? <Dots>Loading</Dots> : `${currency0.symbol}/${currency1.symbol}`}
</Text>
</AutoRow>
<RowFixed gap="8px">
<ButtonEmpty
padding="6px 8px"
borderRadius="12px"
width="fit-content"
onClick={() => setShowMore(!showMore)}
>
<RowFixed gap="8px" style={{ marginRight: '4px' }}>
<ButtonEmpty padding="6px 8px" borderRadius="12px" width="100%" onClick={() => setShowMore(!showMore)}>
{showMore ? (
<>
Manage
<ChevronUp size="20" style={{ marginLeft: '10px' }} />
<ChevronUp size="20" style={{ marginLeft: '8px', height: '20px', minWidth: '20px' }} />
</>
) : (
<>
Manage
<ChevronDown size="20" style={{ marginLeft: '10px' }} />
<ChevronDown size="20" style={{ marginLeft: '8px', height: '20px', minWidth: '20px' }} />
</>
)}
</ButtonEmpty>
@@ -303,12 +298,21 @@ export default function FullPositionCard({ pair, border, stakedBalance }: Positi
</ButtonSecondary>
{userDefaultPoolBalance && JSBI.greaterThan(userDefaultPoolBalance.raw, BIG_INT_ZERO) && (
<RowBetween marginTop="10px">
<ButtonPrimary
padding="8px"
borderRadius="8px"
as={Link}
to={`/migrate/v2/${pair.liquidityToken.address}`}
width="32%"
>
Migrate
</ButtonPrimary>
<ButtonPrimary
padding="8px"
borderRadius="8px"
as={Link}
to={`/add/v2/${currencyId(currency0)}/${currencyId(currency1)}`}
width="48%"
width="32%"
>
Add
</ButtonPrimary>
@@ -316,7 +320,7 @@ export default function FullPositionCard({ pair, border, stakedBalance }: Positi
padding="8px"
borderRadius="8px"
as={Link}
width="48%"
width="32%"
to={`/remove/v2/${currencyId(currency0)}/${currencyId(currency1)}`}
>
Remove

View File

@@ -15,6 +15,7 @@ import { unwrappedToken } from 'utils/wrappedCurrency'
import { DAI, USDC, USDT, WBTC } from '../../constants'
import RangeBadge from 'components/Badge/RangeBadge'
import { RowFixed } from 'components/Row'
import HoverInlineText from 'components/HoverInlineText'
const Row = styled(Link)`
align-items: center;
@@ -202,8 +203,8 @@ export default function PositionListItem({ positionDetails }: PositionListItemPr
// prices
let { priceLower, priceUpper, base, quote } = getPriceOrderingFromPositionForUI(position)
const inverted = token1 ? base?.equals(token1) : undefined
const currencyQuote = inverted ? currency0 : currency1
const currencyBase = inverted ? currency1 : currency0
const currencyQuote = inverted ? currency1 : currency0
const currencyBase = inverted ? currency0 : currency1
// check if price is within range
const outOfRange: boolean = pool ? pool.tickCurrent < tickLower || pool.tickCurrent >= tickUpper : false
@@ -244,8 +245,10 @@ export default function PositionListItem({ positionDetails }: PositionListItemPr
>
<RangeText>
<ExtentsText>Min: </ExtentsText>
{formatPrice(priceLower, 4)} {manuallyInverted ? currencyQuote?.symbol : currencyBase?.symbol} {' / '}{' '}
{manuallyInverted ? currencyBase?.symbol : currencyQuote?.symbol}
{formatPrice(priceLower, 5)}{' '}
<HoverInlineText text={manuallyInverted ? currencyQuote?.symbol ?? '' : currencyBase?.symbol ?? ''} />{' '}
{' per '}{' '}
<HoverInlineText text={manuallyInverted ? currencyBase?.symbol ?? '' : currencyQuote?.symbol ?? ''} />
</RangeText>{' '}
<HideSmall>
<DoubleArrow></DoubleArrow>{' '}
@@ -255,8 +258,13 @@ export default function PositionListItem({ positionDetails }: PositionListItemPr
</SmallOnly>
<RangeText>
<ExtentsText>Max:</ExtentsText>
{formatPrice(priceUpper, 4)} {manuallyInverted ? currencyQuote?.symbol : currencyBase?.symbol} {' / '}{' '}
{manuallyInverted ? currencyBase?.symbol : currencyQuote?.symbol}
{formatPrice(priceUpper, 5)}{' '}
<HoverInlineText text={manuallyInverted ? currencyQuote?.symbol ?? '' : currencyBase?.symbol ?? ''} />{' '}
{' per '}{' '}
<HoverInlineText
maxCharacters={10}
text={manuallyInverted ? currencyBase?.symbol ?? '' : currencyQuote?.symbol ?? ''}
/>
</RangeText>{' '}
</RangeLineItem>
</>

View File

@@ -135,7 +135,7 @@ export const PositionPreview = ({
<TYPE.main
textAlign="center"
fontSize="12px"
>{` ${quoteCurrency.symbol}/${baseCurrency.symbol}`}</TYPE.main>
>{` ${quoteCurrency.symbol} per ${baseCurrency.symbol}`}</TYPE.main>
<TYPE.small textAlign="center" color={theme.text3} style={{ marginTop: '4px' }}>
Your position will be 100% composed of {quoteCurrency?.symbol} at this price
</TYPE.small>
@@ -145,11 +145,11 @@ export const PositionPreview = ({
<LightCard padding="12px ">
<AutoColumn gap="4px" justify="center">
<TYPE.main fontSize="12px">Current price</TYPE.main>
<TYPE.mediumHeader>{`${price.toSignificant(6)} `}</TYPE.mediumHeader>
<TYPE.mediumHeader>{`${price.toSignificant(5)} `}</TYPE.mediumHeader>
<TYPE.main
textAlign="center"
fontSize="12px"
>{` ${quoteCurrency.symbol}/${baseCurrency.symbol}`}</TYPE.main>
>{` ${quoteCurrency.symbol} per ${baseCurrency.symbol}`}</TYPE.main>
</AutoColumn>
</LightCard>
</AutoColumn>

View File

@@ -26,10 +26,10 @@ export default function RateToggle({
<div style={{ width: 'fit-content', display: 'flex', alignItems: 'center' }}>
<ToggleWrapper width="fit-content">
<ToggleElement isActive={isSorted} fontSize="12px" onClick={handleRateToggle}>
{isSorted ? currencyB.symbol + ' / ' + currencyA.symbol : currencyA.symbol + ' / ' + currencyB.symbol}{' '}
{isSorted ? currencyA.symbol + ' price ' : currencyB.symbol + ' price '}
</ToggleElement>
<ToggleElement isActive={!isSorted} fontSize="12px" onClick={handleRateToggle}>
{isSorted ? currencyA.symbol + ' / ' + currencyB.symbol : currencyB.symbol + ' / ' + currencyA.symbol}
{isSorted ? currencyB.symbol + ' price ' : currencyA.symbol + ' price '}
</ToggleElement>
</ToggleWrapper>
</div>

View File

@@ -1,4 +1,3 @@
import JSBI from 'jsbi'
import React, { useContext, useRef, useState } from 'react'
import { Settings, X } from 'react-feather'
import ReactGA from 'react-ga'
@@ -7,12 +6,7 @@ import styled, { ThemeContext } from 'styled-components'
import { useOnClickOutside } from '../../hooks/useOnClickOutside'
import { ApplicationModal } from '../../state/application/actions'
import { useModalOpen, useToggleSettingsMenu } from '../../state/application/hooks'
import {
useExpertModeManager,
useUserTransactionTTL,
useUserSlippageTolerance,
useUserSingleHopOnly,
} from '../../state/user/hooks'
import { useExpertModeManager, useUserSingleHopOnly } from '../../state/user/hooks'
import { TYPE } from '../../theme'
import { ButtonError } from '../Button'
import { AutoColumn } from '../Column'
@@ -21,6 +15,7 @@ import QuestionHelper from '../QuestionHelper'
import { RowBetween, RowFixed } from '../Row'
import Toggle from '../Toggle'
import TransactionSettings from '../TransactionSettings'
import { Percent } from '@uniswap/sdk-core'
const StyledMenuIcon = styled(Settings)`
height: 20px;
@@ -116,15 +111,12 @@ const ModalContentWrapper = styled.div`
border-radius: 20px;
`
export default function SettingsTab() {
export default function SettingsTab({ placeholderSlippage }: { placeholderSlippage: Percent }) {
const node = useRef<HTMLDivElement>()
const open = useModalOpen(ApplicationModal.SETTINGS)
const toggle = useToggleSettingsMenu()
const theme = useContext(ThemeContext)
const [userSlippageTolerance, setUserslippageTolerance] = useUserSlippageTolerance()
const [ttl, setTtl] = useUserTransactionTTL()
const [expertMode, toggleExpertMode] = useExpertModeManager()
@@ -191,12 +183,7 @@ export default function SettingsTab() {
<Text fontWeight={600} fontSize={14}>
Transaction Settings
</Text>
<TransactionSettings
rawSlippage={JSBI.toNumber(userSlippageTolerance.numerator)}
setRawSlippage={setUserslippageTolerance}
deadline={ttl}
setDeadline={setTtl}
/>
<TransactionSettings placeholderSlippage={placeholderSlippage} />
<Text fontWeight={600} fontSize={14}>
Interface Settings
</Text>

View File

@@ -5,8 +5,8 @@ import Popover, { PopoverProps } from '../Popover'
const TooltipContainer = styled.div`
width: 256px;
padding: 0.6rem 1rem;
/* line-height: 150%; */
font-weight: 400;
word-break: break-word;
`
interface TooltipProps extends Omit<PopoverProps, 'content'> {

View File

@@ -1,17 +1,17 @@
import React, { useState, useRef, useContext } from 'react'
import React, { useState, useContext } from 'react'
import { Percent } from '@uniswap/sdk-core'
import styled, { ThemeContext } from 'styled-components'
import QuestionHelper from '../QuestionHelper'
import { TYPE } from '../../theme'
import { AutoColumn } from '../Column'
import { RowBetween, RowFixed } from '../Row'
import { DEFAULT_DEADLINE_FROM_NOW } from 'constants/index'
import { darken } from 'polished'
import { useSetUserSlippageTolerance, useUserSlippageTolerance, useUserTransactionTTL } from 'state/user/hooks'
enum SlippageError {
InvalidInput = 'InvalidInput',
RiskyLow = 'RiskyLow',
RiskyHigh = 'RiskyHigh',
}
enum DeadlineError {
@@ -64,7 +64,8 @@ const OptionCustom = styled(FancyButton)<{ active?: boolean; warning?: boolean }
position: relative;
padding: 0 0.75rem;
flex: 1;
border: ${({ theme, active, warning }) => active && `1px solid ${warning ? theme.red1 : theme.primary1}`};
border: ${({ theme, active, warning }) =>
active ? `1px solid ${warning ? theme.red1 : theme.primary1}` : warning && `1px solid ${theme.red1}`};
:hover {
border: ${({ theme, active, warning }) =>
active && `1px solid ${warning ? darken(0.1, theme.red1) : darken(0.1, theme.primary1)}`};
@@ -85,63 +86,63 @@ const SlippageEmojiContainer = styled.span`
`}
`
export interface SlippageTabsProps {
rawSlippage: number
setRawSlippage: (rawSlippage: number) => void
deadline: number
setDeadline: (deadline: number) => void
export interface TransactionSettingsProps {
placeholderSlippage: Percent // varies according to the context in which the settings dialog is placed
}
export default function SlippageTabs({ rawSlippage, setRawSlippage, deadline, setDeadline }: SlippageTabsProps) {
export default function TransactionSettings({ placeholderSlippage }: TransactionSettingsProps) {
const theme = useContext(ThemeContext)
const inputRef = useRef<HTMLInputElement>()
const userSlippageTolerance = useUserSlippageTolerance()
const setUserSlippageTolerance = useSetUserSlippageTolerance()
const [deadline, setDeadline] = useUserTransactionTTL()
const [slippageInput, setSlippageInput] = useState('')
const [slippageError, setSlippageError] = useState<SlippageError | false>(false)
const [deadlineInput, setDeadlineInput] = useState('')
const [deadlineError, setDeadlineError] = useState<DeadlineError | false>(false)
const slippageInputIsValid =
slippageInput === '' || (rawSlippage / 100).toFixed(2) === Number.parseFloat(slippageInput).toFixed(2)
const deadlineInputIsValid = deadlineInput === '' || (deadline / 60).toString() === deadlineInput
let slippageError: SlippageError | undefined
if (slippageInput !== '' && !slippageInputIsValid) {
slippageError = SlippageError.InvalidInput
} else if (slippageInputIsValid && rawSlippage < 50) {
slippageError = SlippageError.RiskyLow
} else if (slippageInputIsValid && rawSlippage > 500) {
slippageError = SlippageError.RiskyHigh
} else {
slippageError = undefined
}
let deadlineError: DeadlineError | undefined
if (deadlineInput !== '' && !deadlineInputIsValid) {
deadlineError = DeadlineError.InvalidInput
} else {
deadlineError = undefined
}
function parseCustomSlippage(value: string) {
function parseSlippageInput(value: string) {
// populate what the user typed and clear the error
setSlippageInput(value)
setSlippageError(false)
try {
const valueAsIntFromRoundedFloat = Number.parseInt((Number.parseFloat(value) * 100).toString())
if (!Number.isNaN(valueAsIntFromRoundedFloat) && valueAsIntFromRoundedFloat < 5000) {
setRawSlippage(valueAsIntFromRoundedFloat)
if (value.length === 0) {
setUserSlippageTolerance('auto')
} else {
const parsed = Math.floor(Number.parseFloat(value) * 100)
if (!Number.isInteger(parsed) || parsed < 0 || parsed > 5000) {
setUserSlippageTolerance('auto')
if (value !== '.') {
setSlippageError(SlippageError.InvalidInput)
}
} else {
setUserSlippageTolerance(new Percent(parsed, 10_000))
}
} catch {}
}
}
const tooLow = userSlippageTolerance !== 'auto' && userSlippageTolerance.lessThan(new Percent(5, 10_000))
const tooHigh = userSlippageTolerance !== 'auto' && userSlippageTolerance.greaterThan(new Percent(1, 100))
function parseCustomDeadline(value: string) {
// populate what the user typed and clear the error
setDeadlineInput(value)
setDeadlineError(false)
try {
const valueAsInt: number = Number.parseInt(value) * 60
if (!Number.isNaN(valueAsInt) && valueAsInt > 0) {
setDeadline(valueAsInt)
if (value.length === 0) {
setDeadline(DEFAULT_DEADLINE_FROM_NOW)
} else {
const parsed: number = Math.floor(Number.parseFloat(value) * 60)
if (!Number.isInteger(parsed) || parsed < 60) {
setDeadlineError(DeadlineError.InvalidInput)
} else {
setDeadline(parsed)
}
} catch {}
}
}
return (
@@ -156,71 +157,56 @@ export default function SlippageTabs({ rawSlippage, setRawSlippage, deadline, se
<RowBetween>
<Option
onClick={() => {
setSlippageInput('')
setRawSlippage(10)
parseSlippageInput('')
}}
active={rawSlippage === 10}
active={userSlippageTolerance === 'auto'}
>
0.1%
Auto
</Option>
<Option
onClick={() => {
setSlippageInput('')
setRawSlippage(50)
}}
active={rawSlippage === 50}
>
0.5%
</Option>
<Option
onClick={() => {
setSlippageInput('')
setRawSlippage(100)
}}
active={rawSlippage === 100}
>
1%
</Option>
<OptionCustom active={![10, 50, 100].includes(rawSlippage)} warning={!slippageInputIsValid} tabIndex={-1}>
<OptionCustom active={userSlippageTolerance !== 'auto'} warning={!!slippageError} tabIndex={-1}>
<RowBetween>
{!!slippageInput &&
(slippageError === SlippageError.RiskyLow || slippageError === SlippageError.RiskyHigh) ? (
{tooLow || tooHigh ? (
<SlippageEmojiContainer>
<span role="img" aria-label="warning">
</span>
</SlippageEmojiContainer>
) : null}
{/* https://github.com/DefinitelyTyped/DefinitelyTyped/issues/30451 */}
<Input
ref={inputRef as any}
placeholder={(rawSlippage / 100).toFixed(2)}
value={slippageInput}
placeholder={placeholderSlippage.toFixed(2)}
value={
slippageInput.length > 0
? slippageInput
: userSlippageTolerance === 'auto'
? ''
: userSlippageTolerance.toFixed(2)
}
onChange={(e) => parseSlippageInput(e.target.value)}
onBlur={() => {
parseCustomSlippage((rawSlippage / 100).toFixed(2))
setSlippageInput('')
setSlippageError(false)
}}
onChange={(e) => parseCustomSlippage(e.target.value)}
color={!slippageInputIsValid ? 'red' : ''}
color={slippageError ? 'red' : ''}
/>
%
</RowBetween>
</OptionCustom>
</RowBetween>
{!!slippageError && (
{slippageError || tooLow || tooHigh ? (
<RowBetween
style={{
fontSize: '14px',
paddingTop: '7px',
color: slippageError === SlippageError.InvalidInput ? 'red' : '#F3841E',
color: slippageError ? 'red' : '#F3841E',
}}
>
{slippageError === SlippageError.InvalidInput
{slippageError
? 'Enter a valid slippage percentage'
: slippageError === SlippageError.RiskyLow
: tooLow
? 'Your transaction may fail'
: 'Your transaction may be frontrun'}
</RowBetween>
)}
) : null}
</AutoColumn>
<AutoColumn gap="sm">
@@ -231,15 +217,22 @@ export default function SlippageTabs({ rawSlippage, setRawSlippage, deadline, se
<QuestionHelper text="Your transaction will revert if it is pending for more than this period of time." />
</RowFixed>
<RowFixed>
<OptionCustom style={{ width: '80px' }} tabIndex={-1}>
<OptionCustom style={{ width: '80px' }} warning={!!deadlineError} tabIndex={-1}>
<Input
color={!!deadlineError ? 'red' : undefined}
onBlur={() => {
parseCustomDeadline((deadline / 60).toString())
}}
placeholder={(deadline / 60).toString()}
value={deadlineInput}
placeholder={(DEFAULT_DEADLINE_FROM_NOW / 60).toString()}
value={
deadlineInput.length > 0
? deadlineInput
: deadline === DEFAULT_DEADLINE_FROM_NOW
? ''
: (deadline / 60).toString()
}
onChange={(e) => parseCustomDeadline(e.target.value)}
onBlur={() => {
setDeadlineInput('')
setDeadlineError(false)
}}
color={deadlineError ? 'red' : ''}
/>
</OptionCustom>
<TYPE.body style={{ paddingLeft: '8px' }} fontSize={14}>

View File

@@ -1,8 +1,8 @@
import { Percent } from '@uniswap/sdk-core'
import { Trade as V2Trade } from '@uniswap/v2-sdk'
import { Trade as V3Trade } from '@uniswap/v3-sdk'
import React, { useContext } from 'react'
import { ThemeContext } from 'styled-components'
import { useUserSlippageTolerance } from '../../state/user/hooks'
import { TYPE } from '../../theme'
import { computePriceImpactWithMaximumSlippage } from '../../utils/computePriceImpactWithMaximumSlippage'
import { computeRealizedLPFeeAmount } from '../../utils/prices'
@@ -13,13 +13,13 @@ import SwapRoute from './SwapRoute'
export interface AdvancedSwapDetailsProps {
trade?: V2Trade | V3Trade
allowedSlippage: Percent
}
export function AdvancedSwapDetails({ trade }: AdvancedSwapDetailsProps) {
export function AdvancedSwapDetails({ trade, allowedSlippage }: AdvancedSwapDetailsProps) {
const theme = useContext(ThemeContext)
const realizedLPFee = computeRealizedLPFeeAmount(trade)
const [allowedSlippage] = useUserSlippageTolerance()
return !trade ? null : (
<AutoColumn gap="8px">
@@ -55,6 +55,17 @@ export function AdvancedSwapDetails({ trade }: AdvancedSwapDetailsProps) {
<FormattedPriceImpact priceImpact={computePriceImpactWithMaximumSlippage(trade, allowedSlippage)} />
</TYPE.black>
</RowBetween>
<RowBetween>
<RowFixed>
<TYPE.black fontSize={12} fontWeight={400} color={theme.text2}>
Slippage tolerance
</TYPE.black>
</RowFixed>
<TYPE.black textAlign="right" fontSize={12} color={theme.text1}>
{allowedSlippage.toFixed(2)}%
</TYPE.black>
</RowBetween>
</AutoColumn>
)
}

View File

@@ -1,6 +1,7 @@
import React from 'react'
import styled from 'styled-components'
import Settings from '../Settings'
import SettingsTab from '../Settings'
import { Percent } from '@uniswap/sdk-core'
import { RowBetween, RowFixed } from '../Row'
import { TYPE } from '../../theme'
@@ -11,7 +12,7 @@ const StyledSwapHeader = styled.div`
color: ${({ theme }) => theme.text2};
`
export default function SwapHeader() {
export default function SwapHeader({ allowedSlippage }: { allowedSlippage: Percent }) {
return (
<StyledSwapHeader>
<RowBetween>
@@ -21,9 +22,7 @@ export default function SwapHeader() {
</TYPE.black>
</RowFixed>
<RowFixed>
{/* <TradeInfo disabled={!trade} trade={trade} /> */}
{/* <div style={{ width: '8px' }}></div> */}
<Settings />
<SettingsTab placeholderSlippage={allowedSlippage} />
</RowFixed>
</RowBetween>
</StyledSwapHeader>

View File

@@ -135,7 +135,7 @@ export default function SwapModalHeader({
</RowBetween>
<LightCard style={{ padding: '.75rem', marginTop: '0.5rem' }}>
<AdvancedSwapDetails trade={trade} />
<AdvancedSwapDetails trade={trade} allowedSlippage={allowedSlippage} />
</LightCard>
{showAcceptChanges ? (

View File

@@ -25,7 +25,7 @@ export default memo(function SwapRoute({ trade }: { trade: V2Trade | V3Trade })
return (
<Fragment key={i}>
<Flex alignItems="end">
<TYPE.black fontSize={14} color={theme.text1} ml="0.145rem" mr="0.145rem">
<TYPE.black color={theme.text1} ml="0.145rem" mr="0.145rem">
{currency.symbol}
</TYPE.black>
</Flex>

View File

@@ -39,6 +39,37 @@ export const TRIBE = new Token(ChainId.MAINNET, '0xc7283b66Eb1EB5FB86327f08e1B58
export const FRAX = new Token(ChainId.MAINNET, '0x853d955aCEf822Db058eb8505911ED77F175b99e', 18, 'FRAX', 'Frax')
export const FXS = new Token(ChainId.MAINNET, '0x3432B6A60D23Ca0dFCa7761B7ab56459D9C964D0', 18, 'FXS', 'Frax Share')
export const renBTC = new Token(ChainId.MAINNET, '0xEB4C2781e4ebA804CE9a9803C67d0893436bB27D', 8, 'renBTC', 'renBTC')
export const UMA = new Token(
ChainId.MAINNET,
'0x04Fa0d235C4abf4BcF4787aF4CF447DE572eF828',
18,
'UMA',
'UMA Voting Token v1'
)
// Mirror Protocol compat.
export const UST = new Token(ChainId.MAINNET, '0xa47c8bf37f92abed4a126bda807a7b7498661acd', 18, 'UST', 'Wrapped UST')
export const MIR = new Token(ChainId.MAINNET, '0x09a3ecafa817268f77be1283176b946c4ff2e608', 18, 'MIR', 'Wrapped MIR')
// List of all mirror's assets addresses.
// Last pulled from : https://whitelist.mirror.finance/eth/tokenlists.json
// TODO: Generate this programaticaly ?
const mAssetsAdditionalBases: { [tokenAddress: string]: Token[] } = {
[UST.address]: [MIR],
[MIR.address]: [UST],
'0xd36932143F6eBDEDD872D5Fb0651f4B72Fd15a84': [MIR, UST], // mAAPL
'0x59A921Db27Dd6d4d974745B7FfC5c33932653442': [MIR, UST], // mGOOGL
'0x21cA39943E91d704678F5D00b6616650F066fD63': [MIR, UST], // mTSLA
'0xC8d674114bac90148d11D3C1d33C61835a0F9DCD': [MIR, UST], // mNFLX
'0x13B02c8dE71680e71F0820c996E4bE43c2F57d15': [MIR, UST], // mQQQ
'0xEdb0414627E6f1e3F082DE65cD4F9C693D78CCA9': [MIR, UST], // mTWTR
'0x41BbEDd7286dAab5910a1f15d12CBda839852BD7': [MIR, UST], // mMSFT
'0x0cae9e4d663793c2a2A0b211c1Cf4bBca2B9cAa7': [MIR, UST], // mAMZN
'0x56aA298a19C93c6801FDde870fA63EF75Cc0aF72': [MIR, UST], // mBABA
'0x1d350417d9787E000cc1b95d70E9536DcD91F373': [MIR, UST], // mIAU
'0x9d1555d8cB3C846Bb4f7D5B1B1080872c3166676': [MIR, UST], // mSLV
'0x31c63146a635EB7465e5853020b39713AC356991': [MIR, UST], // mUSO
'0xf72FCd9DCF0190923Fadd44811E240Ef4533fc86': [MIR, UST], // mVIXY
}
// Block time here is slightly higher (~1s) than average in order to avoid ongoing proposals past the displayed time
export const AVERAGE_BLOCK_TIME_IN_SECS = 13
@@ -84,8 +115,10 @@ export const BASES_TO_CHECK_TRADES_AGAINST: ChainTokenList = {
export const ADDITIONAL_BASES: { [chainId in ChainId]?: { [tokenAddress: string]: Token[] } } = {
[ChainId.MAINNET]: {
...mAssetsAdditionalBases,
'0xA948E86885e12Fb09AfEF8C52142EBDbDf73cD18': [UNI[ChainId.MAINNET]],
'0x561a4717537ff4AF5c687328c0f7E90a319705C0': [UNI[ChainId.MAINNET]],
'0xa6e3454fec677772dd771788a079355e43910638': [UMA],
[FEI.address]: [TRIBE],
[TRIBE.address]: [FEI],
[FRAX.address]: [FXS],
@@ -205,10 +238,8 @@ export const SUPPORTED_WALLETS: { [key: string]: WalletInfo } = {
export const NetworkContextName = 'NETWORK'
// default allowed slippage, in bips
export const INITIAL_ALLOWED_SLIPPAGE = new Percent(10, 10_000)
// 20 minutes, denominated in seconds
export const DEFAULT_DEADLINE_FROM_NOW = 60 * 20
// 30 minutes, denominated in seconds
export const DEFAULT_DEADLINE_FROM_NOW = 60 * 30
// used for rewards deadlines
export const BIG_INT_SECONDS_IN_WEEK = JSBI.BigInt(60 * 60 * 24 * 7)

View File

@@ -160,10 +160,10 @@ export function useSocksController(): Unisocks | null {
) as Unisocks | null
}
export function useV3NFTPositionManagerContract(): NonfungiblePositionManager | null {
export function useV3NFTPositionManagerContract(withSignerIfPossible?: boolean): NonfungiblePositionManager | null {
const { chainId } = useActiveWeb3React()
const address = chainId ? NONFUNGIBLE_POSITION_MANAGER_ADDRESSES[chainId] : undefined
return useContract(address, NFTPositionManagerABI) as NonfungiblePositionManager | null
return useContract(address, NFTPositionManagerABI, withSignerIfPossible) as NonfungiblePositionManager | null
}
export function useV3Factory(): UniswapV3Factory | null {

View File

@@ -3,7 +3,6 @@ import { Router, Trade as V2Trade } from '@uniswap/v2-sdk'
import { SwapRouter, Trade as V3Trade } from '@uniswap/v3-sdk'
import { ChainId, Percent, TradeType } from '@uniswap/sdk-core'
import { useMemo } from 'react'
import { INITIAL_ALLOWED_SLIPPAGE } from '../constants'
import { SWAP_ROUTER_ADDRESSES } from '../constants/v3'
import { getTradeVersion } from '../utils/getTradeVersion'
import { useTransactionAdder } from '../state/transactions/hooks'
@@ -51,7 +50,7 @@ interface FailedCall extends SwapCallEstimate {
*/
function useSwapCallArguments(
trade: V2Trade | V3Trade | undefined, // trade to execute, required
allowedSlippage: Percent = INITIAL_ALLOWED_SLIPPAGE, // in bips
allowedSlippage: Percent, // in bips
recipientAddressOrName: string | null, // the ENS name or address of the recipient of the trade, or null if swap should be returned to sender
signatureData: SignatureData | null | undefined
): SwapCall[] {
@@ -138,7 +137,7 @@ function useSwapCallArguments(
// and the user has approved the slippage adjusted input amount for the trade
export function useSwapCallback(
trade: V2Trade | V3Trade | undefined, // trade to execute, required
allowedSlippage: Percent = INITIAL_ALLOWED_SLIPPAGE, // in bips
allowedSlippage: Percent, // in bips
recipientAddressOrName: string | null, // the ENS name or address of the recipient of the trade, or null if swap should be returned to sender
signatureData: SignatureData | undefined | null
): { state: SwapCallbackState; callback: null | (() => Promise<string>); error: string | null } {

View File

@@ -0,0 +1,18 @@
import { Percent } from '@uniswap/sdk-core'
import { Trade as V3Trade } from '@uniswap/v3-sdk'
import { Trade as V2Trade } from '@uniswap/v2-sdk'
import { useMemo } from 'react'
import { useUserSlippageToleranceWithDefault } from '../state/user/hooks'
const V2_SWAP_DEFAULT_SLIPPAGE = new Percent(50, 10_000) // .50%
const V3_SWAP_DEFAULT_SLIPPAGE = new Percent(50, 10_000) // .50%
const ONE_TENTHS_PERCENT = new Percent(10, 10_000) // .10%
export default function useSwapSlippageTolerance(trade: V2Trade | V3Trade | undefined): Percent {
const defaultSlippageTolerance = useMemo(() => {
if (!trade) return ONE_TENTHS_PERCENT
if (trade instanceof V2Trade) return V2_SWAP_DEFAULT_SLIPPAGE
return V3_SWAP_DEFAULT_SLIPPAGE
}, [trade])
return useUserSlippageToleranceWithDefault(defaultSlippageTolerance)
}

View File

@@ -1,126 +1,53 @@
import { useSingleCallResult } from 'state/multicall/hooks'
import { useMemo } from 'react'
import { useEffect, useState } from 'react'
import { PositionDetails } from 'types/position'
import { useV3Pool } from './useContract'
import { useV3NFTPositionManagerContract } from './useContract'
import { BigNumber } from '@ethersproject/bignumber'
import { computePoolAddress, Pool } from '@uniswap/v3-sdk'
import { V3_CORE_FACTORY_ADDRESSES } from 'constants/v3'
import { useActiveWeb3React } from 'hooks'
import { Pool } from '@uniswap/v3-sdk'
import { TokenAmount } from '@uniswap/sdk-core'
import { useBlockNumber } from 'state/application/hooks'
// TODO port these utility functions to the SDK
function subIn256(x: BigNumber, y: BigNumber): BigNumber {
const difference = x.sub(y)
return difference.lt(0) ? BigNumber.from(2).pow(256).add(difference) : difference
}
function getCounterfactualFees(
feeGrowthGlobal: BigNumber,
feeGrowthOutsideLower: BigNumber,
feeGrowthOutsideUpper: BigNumber,
feeGrowthInsideLast: BigNumber,
pool: Pool,
liquidity: BigNumber,
tickLower: number,
tickUpper: number
) {
let feeGrowthBelow: BigNumber
if (pool.tickCurrent >= tickLower) {
feeGrowthBelow = feeGrowthOutsideLower
} else {
feeGrowthBelow = subIn256(feeGrowthGlobal, feeGrowthOutsideLower)
}
let feeGrowthAbove: BigNumber
if (pool.tickCurrent < tickUpper) {
feeGrowthAbove = feeGrowthOutsideUpper
} else {
feeGrowthAbove = subIn256(feeGrowthGlobal, feeGrowthOutsideUpper)
}
const feeGrowthInside = subIn256(subIn256(feeGrowthGlobal, feeGrowthBelow), feeGrowthAbove)
return subIn256(feeGrowthInside, feeGrowthInsideLast).mul(liquidity).div(BigNumber.from(2).pow(128))
}
const MAX_UINT128 = BigNumber.from(2).pow(128).sub(1)
// compute current + counterfactual fees for a v3 position
export function useV3PositionFees(
pool?: Pool,
positionDetails?: PositionDetails
): [TokenAmount, TokenAmount] | [undefined, undefined] {
const { chainId } = useActiveWeb3React()
const positionManager = useV3NFTPositionManagerContract(false)
const owner = useSingleCallResult(positionDetails?.tokenId ? positionManager : null, 'ownerOf', [
positionDetails?.tokenId,
]).result?.[0]
const poolAddress = useMemo(() => {
try {
return chainId && V3_CORE_FACTORY_ADDRESSES[chainId] && pool && positionDetails
? computePoolAddress({
factoryAddress: V3_CORE_FACTORY_ADDRESSES[chainId] as string,
tokenA: pool.token0,
tokenB: pool.token1,
fee: positionDetails.fee,
})
: undefined
} catch {
return undefined
const tokenId = positionDetails?.tokenId?.toHexString()
const latestBlockNumber = useBlockNumber()
// TODO find a way to get this into multicall
// because fees don't ever go down, we don't actually need to clear this state
// latestBlockNumber is included to ensure data stays up-to-date fresh
const [amounts, setAmounts] = useState<[BigNumber, BigNumber]>()
useEffect(() => {
if (positionManager && tokenId && owner && typeof latestBlockNumber === 'number') {
positionManager.callStatic
.collect(
{
tokenId,
recipient: owner, // some tokens might fail if transferred to address(0)
amount0Max: MAX_UINT128,
amount1Max: MAX_UINT128,
},
{ from: owner } // need to simulate the call as the owner
)
.then((results) => {
setAmounts([results.amount0, results.amount1])
})
}
}, [chainId, pool, positionDetails])
const poolContract = useV3Pool(poolAddress)
}, [positionManager, tokenId, owner, latestBlockNumber])
// data fetching
const feeGrowthGlobal0: BigNumber | undefined = useSingleCallResult(poolContract, 'feeGrowthGlobal0X128')?.result?.[0]
const feeGrowthGlobal1: BigNumber | undefined = useSingleCallResult(poolContract, 'feeGrowthGlobal1X128')?.result?.[0]
const { feeGrowthOutside0X128: feeGrowthOutsideLower0 } = (useSingleCallResult(poolContract, 'ticks', [
positionDetails?.tickLower,
])?.result ?? {}) as { feeGrowthOutside0X128?: BigNumber }
const { feeGrowthOutside1X128: feeGrowthOutsideLower1 } = (useSingleCallResult(poolContract, 'ticks', [
positionDetails?.tickLower,
])?.result ?? {}) as { feeGrowthOutside1X128?: BigNumber }
const { feeGrowthOutside0X128: feeGrowthOutsideUpper0 } = (useSingleCallResult(poolContract, 'ticks', [
positionDetails?.tickUpper,
])?.result ?? {}) as { feeGrowthOutside0X128?: BigNumber }
const { feeGrowthOutside1X128: feeGrowthOutsideUpper1 } = (useSingleCallResult(poolContract, 'ticks', [
positionDetails?.tickUpper,
])?.result ?? {}) as { feeGrowthOutside1X128?: BigNumber }
// calculate fees
const counterfactualFees0 =
positionDetails && pool && feeGrowthGlobal0 && feeGrowthOutsideLower0 && feeGrowthOutsideUpper0
? getCounterfactualFees(
feeGrowthGlobal0,
feeGrowthOutsideLower0,
feeGrowthOutsideUpper0,
positionDetails.feeGrowthInside0LastX128,
pool,
positionDetails.liquidity,
positionDetails.tickLower,
positionDetails.tickUpper
)
: undefined
const counterfactualFees1 =
positionDetails && pool && feeGrowthGlobal1 && feeGrowthOutsideLower1 && feeGrowthOutsideUpper1
? getCounterfactualFees(
feeGrowthGlobal1,
feeGrowthOutsideLower1,
feeGrowthOutsideUpper1,
positionDetails.feeGrowthInside1LastX128,
pool,
positionDetails.liquidity,
positionDetails.tickLower,
positionDetails.tickUpper
)
: undefined
if (
pool &&
positionDetails?.tokensOwed0 &&
positionDetails?.tokensOwed1 &&
counterfactualFees0 &&
counterfactualFees1
) {
if (pool && positionDetails && amounts) {
return [
new TokenAmount(pool.token0, positionDetails.tokensOwed0.add(counterfactualFees0).toString()),
new TokenAmount(pool.token1, positionDetails.tokensOwed1.add(counterfactualFees1).toString()),
new TokenAmount(pool.token0, positionDetails.tokensOwed0.add(amounts[0]).toString()),
new TokenAmount(pool.token1, positionDetails.tokensOwed1.add(amounts[1]).toString()),
]
} else {
return [undefined, undefined]

View File

@@ -46,13 +46,6 @@ if (typeof GOOGLE_ANALYTICS_ID === 'string') {
ReactGA.initialize('test', { testMode: true, debug: true })
}
window.addEventListener('error', (error) => {
ReactGA.exception({
description: `${error.message} @ ${error.filename}:${error.lineno}:${error.colno}`,
fatal: true,
})
})
function Updaters() {
return (
<>

View File

@@ -1,9 +1,10 @@
import React, { useCallback, useContext, useMemo, useState, useEffect } from 'react'
import { TransactionResponse } from '@ethersproject/providers'
import { Currency, TokenAmount, ETHER, currencyEquals } from '@uniswap/sdk-core'
import { Currency, TokenAmount, ETHER, currencyEquals, Percent } from '@uniswap/sdk-core'
import { WETH9 } from '@uniswap/sdk-core'
import { AlertTriangle, AlertCircle } from 'react-feather'
import ReactGA from 'react-ga'
import { ZERO_PERCENT } from '../../constants'
import { useV3NFTPositionManagerContract } from '../../hooks/useContract'
import { RouteComponentProps } from 'react-router-dom'
import { Text } from 'rebass'
@@ -13,7 +14,7 @@ import { YellowCard, OutlineCard, BlueCard, LightCard } from '../../components/C
import { AutoColumn } from '../../components/Column'
import TransactionConfirmationModal, { ConfirmationModalContent } from '../../components/TransactionConfirmationModal'
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
import { RowBetween } from '../../components/Row'
import { RowBetween, RowFixed } from '../../components/Row'
import { useIsSwapUnsupported } from '../../hooks/useIsSwapUnsupported'
import { useUSDCValue } from '../../hooks/useUSDCPrice'
import Review from './Review'
@@ -25,7 +26,7 @@ import { useWalletModalToggle } from '../../state/application/hooks'
import { Field, Bound } from '../../state/mint/v3/actions'
import { useTransactionAdder } from '../../state/transactions/hooks'
import { useIsExpertMode, useUserSlippageTolerance } from '../../state/user/hooks'
import { useIsExpertMode, useUserSlippageToleranceWithDefault } from '../../state/user/hooks'
import { TYPE, ExternalLink } from '../../theme'
import { maxAmountSpend } from '../../utils/maxAmountSpend'
import AppBody from '../AppBody'
@@ -51,6 +52,9 @@ import RateToggle from 'components/RateToggle'
import { BigNumber } from '@ethersproject/bignumber'
import { calculateGasMargin } from 'utils'
import { AddRemoveTabs } from 'components/NavigationTabs'
import HoverInlineText from 'components/HoverInlineText'
const DEFAULT_ADD_IN_RANGE_SLIPPAGE_TOLERANCE = new Percent(50, 10_000)
export default function AddLiquidity({
match: {
@@ -148,7 +152,7 @@ export default function AddLiquidity({
// txn values
const deadline = useTransactionDeadline() // custom from users settings
const [allowedSlippage] = useUserSlippageTolerance() // custom from users
const [txHash, setTxHash] = useState<string>('')
// get formatted amounts
@@ -193,6 +197,10 @@ export default function AddLiquidity({
chainId ? NONFUNGIBLE_POSITION_MANAGER_ADDRESSES[chainId] : undefined
)
const allowedSlippage = useUserSlippageToleranceWithDefault(
outOfRange ? ZERO_PERCENT : DEFAULT_ADD_IN_RANGE_SLIPPAGE_TOLERANCE
)
async function onAdd() {
if (!chainId || !library || !account) return
@@ -396,7 +404,12 @@ export default function AddLiquidity({
pendingText={pendingText}
/>
<AppBody>
<AddRemoveTabs creating={false} adding={true} positionID={tokenId} />
<AddRemoveTabs
creating={false}
adding={true}
positionID={tokenId}
defaultSlippage={DEFAULT_ADD_IN_RANGE_SLIPPAGE_TOLERANCE}
/>
<Wrapper>
<AutoColumn gap="32px">
{!hasExistingPosition && (
@@ -486,8 +499,13 @@ export default function AddLiquidity({
<TYPE.main>
{price ? (
<TYPE.main>
{invertPrice ? price?.invert()?.toSignificant(8) : price?.toSignificant(8)}{' '}
{quoteCurrency?.symbol}
<RowFixed>
<HoverInlineText
maxCharacters={20}
text={invertPrice ? price?.invert()?.toSignificant(5) : price?.toSignificant(5)}
/>{' '}
<span style={{ marginLeft: '4px' }}>{quoteCurrency?.symbol}</span>
</RowFixed>
</TYPE.main>
) : (
'-'
@@ -540,7 +558,7 @@ export default function AddLiquidity({
<TYPE.main fontSize={14} fontWeight={400} style={{ marginBottom: '.5rem', lineHeight: '125%' }}>
Your liquidity will only earn fees when the market price of the pair is within your range.{' '}
<ExternalLink
href={'https://docs.uniswap.org/concepts/introduction/liquidity-user-guide'}
href={'https://docs.uniswap.org/concepts/introduction/liquidity-user-guide#4-set-price-range'}
style={{ fontSize: '14px' }}
>
Need help picking a range?
@@ -568,10 +586,13 @@ export default function AddLiquidity({
Current Price
</TYPE.main>
<TYPE.body fontWeight={500} textAlign="center" fontSize={20}>
{invertPrice ? price.invert().toSignificant(3) : price.toSignificant(3)}{' '}
<HoverInlineText
maxCharacters={20}
text={invertPrice ? price.invert().toSignificant(5) : price.toSignificant(5)}
/>{' '}
</TYPE.body>
<TYPE.main fontWeight={500} textAlign="center" fontSize={12}>
{quoteCurrency?.symbol} {' / '}
{quoteCurrency?.symbol} {' per '}
{baseCurrency.symbol}
</TYPE.main>
</AutoColumn>

View File

@@ -1,6 +1,6 @@
import { BigNumber } from '@ethersproject/bignumber'
import { TransactionResponse } from '@ethersproject/providers'
import { Currency, currencyEquals, ETHER, TokenAmount, WETH9 } from '@uniswap/sdk-core'
import { Currency, currencyEquals, ETHER, Percent, TokenAmount, WETH9 } from '@uniswap/sdk-core'
import React, { useCallback, useContext, useState } from 'react'
import { Plus } from 'react-feather'
import ReactGA from 'react-ga'
@@ -30,7 +30,7 @@ import { Field } from '../../state/mint/actions'
import { useDerivedMintInfo, useMintActionHandlers, useMintState } from '../../state/mint/hooks'
import { useTransactionAdder } from '../../state/transactions/hooks'
import { useIsExpertMode, useUserSlippageTolerance } from '../../state/user/hooks'
import { useIsExpertMode, useUserSlippageToleranceWithDefault } from '../../state/user/hooks'
import { TYPE } from '../../theme'
import { calculateGasMargin, calculateSlippageAmount } from '../../utils'
import { maxAmountSpend } from '../../utils/maxAmountSpend'
@@ -42,6 +42,8 @@ import { currencyId } from '../../utils/currencyId'
import { PoolPriceBar } from './PoolPriceBar'
import UnsupportedCurrencyFooter from 'components/swap/UnsupportedCurrencyFooter'
const DEFAULT_ADD_V2_SLIPPAGE_TOLERANCE = new Percent(50, 10_000)
export default function AddLiquidity({
match: {
params: { currencyIdA, currencyIdB },
@@ -90,7 +92,7 @@ export default function AddLiquidity({
// txn values
const deadline = useTransactionDeadline() // custom from users settings
const [allowedSlippage] = useUserSlippageTolerance() // custom from users
const allowedSlippage = useUserSlippageToleranceWithDefault(DEFAULT_ADD_V2_SLIPPAGE_TOLERANCE) // custom from users
const [txHash, setTxHash] = useState<string>('')
// get formatted amounts
@@ -315,7 +317,7 @@ export default function AddLiquidity({
return (
<>
<AppBody>
<AddRemoveTabs creating={isCreate} adding={true} />
<AddRemoveTabs creating={isCreate} adding={true} defaultSlippage={DEFAULT_ADD_V2_SLIPPAGE_TOLERANCE} />
<Wrapper>
<TransactionConfirmationModal
isOpen={showConfirm}

View File

@@ -8,6 +8,7 @@ import Polling from '../components/Header/Polling'
// import URLWarning from '../components/Header/URLWarning'
import Popups from '../components/Popups'
import Web3ReactManager from '../components/Web3ReactManager'
import ErrorBoundary from '../components/ErrorBoundary'
import { ApplicationModal } from '../state/application/actions'
import { useModalOpen, useToggleModal } from '../state/application/hooks'
import DarkModeQueryParamReader from '../theme/DarkModeQueryParamReader'
@@ -72,62 +73,69 @@ function TopLevelModals() {
export default function App() {
return (
<Suspense fallback={null}>
<Route component={GoogleAnalyticsReporter} />
<Route component={DarkModeQueryParamReader} />
<AppWrapper>
<HeaderWrapper>
<Header />
</HeaderWrapper>
<BodyWrapper>
<ThemedBackground />
<Popups />
<Polling />
<TopLevelModals />
<Web3ReactManager>
<Switch>
<Route exact strict path="/vote" component={Vote} />
<Route exact strict path="/vote/:id" component={VotePage} />
<Route exact strict path="/claim" component={OpenClaimAddressModalAndRedirectToSwap} />
<Route exact strict path="/uni" component={Earn} />
<Route exact strict path="/uni/:currencyIdA/:currencyIdB" component={Manage} />
<ErrorBoundary>
<Suspense fallback={null}>
<Route component={GoogleAnalyticsReporter} />
<Route component={DarkModeQueryParamReader} />
<AppWrapper>
<HeaderWrapper>
<Header />
</HeaderWrapper>
<BodyWrapper>
<ThemedBackground />
<Popups />
<Polling />
<TopLevelModals />
<Web3ReactManager>
<Switch>
<Route exact strict path="/vote" component={Vote} />
<Route exact strict path="/vote/:id" component={VotePage} />
<Route exact strict path="/claim" component={OpenClaimAddressModalAndRedirectToSwap} />
<Route exact strict path="/uni" component={Earn} />
<Route exact strict path="/uni/:currencyIdA/:currencyIdB" component={Manage} />
<Route exact strict path="/send" component={RedirectPathToSwapOnly} />
<Route exact strict path="/swap/:outputCurrency" component={RedirectToSwap} />
<Route exact strict path="/swap" component={Swap} />
<Route exact strict path="/send" component={RedirectPathToSwapOnly} />
<Route exact strict path="/swap/:outputCurrency" component={RedirectToSwap} />
<Route exact strict path="/swap" component={Swap} />
<Route exact strict path="/find" component={PoolFinder} />
<Route exact strict path="/pool/v2" component={PoolV2} />
<Route exact strict path="/pool" component={Pool} />
<Route exact strict path="/pool/:tokenId" component={PositionPage} />
<Route exact strict path="/find" component={PoolFinder} />
<Route exact strict path="/pool/v2" component={PoolV2} />
<Route exact strict path="/pool" component={Pool} />
<Route exact strict path="/pool/:tokenId" component={PositionPage} />
<Route exact strict path="/add/v2/:currencyIdA?/:currencyIdB?" component={RedirectDuplicateTokenIdsV2} />
<Route
exact
strict
path="/add/:currencyIdA?/:currencyIdB?/:feeAmount?"
component={RedirectDuplicateTokenIds}
/>
<Route
exact
strict
path="/add/v2/:currencyIdA?/:currencyIdB?"
component={RedirectDuplicateTokenIdsV2}
/>
<Route
exact
strict
path="/add/:currencyIdA?/:currencyIdB?/:feeAmount?"
component={RedirectDuplicateTokenIds}
/>
<Route
exact
strict
path="/increase/:currencyIdA?/:currencyIdB?/:feeAmount?/:tokenId?"
component={AddLiquidity}
/>
<Route
exact
strict
path="/increase/:currencyIdA?/:currencyIdB?/:feeAmount?/:tokenId?"
component={AddLiquidity}
/>
<Route exact strict path="/remove/v2/:currencyIdA/:currencyIdB" component={RemoveLiquidity} />
<Route exact strict path="/remove/:tokenId" component={RemoveLiquidityV3} />
<Route exact strict path="/remove/v2/:currencyIdA/:currencyIdB" component={RemoveLiquidity} />
<Route exact strict path="/remove/:tokenId" component={RemoveLiquidityV3} />
<Route exact strict path="/migrate/v2" component={MigrateV2} />
<Route exact strict path="/migrate/v2/:address" component={MigrateV2Pair} />
<Route exact strict path="/migrate/v2" component={MigrateV2} />
<Route exact strict path="/migrate/v2/:address" component={MigrateV2Pair} />
<Route component={RedirectPathToSwapOnly} />
</Switch>
</Web3ReactManager>
<Marginer />
</BodyWrapper>
</AppWrapper>
</Suspense>
<Route component={RedirectPathToSwapOnly} />
</Switch>
</Web3ReactManager>
<Marginer />
</BodyWrapper>
</AppWrapper>
</Suspense>
</ErrorBoundary>
)
}

View File

@@ -1,12 +1,11 @@
import React, { useCallback, useMemo, useState, useEffect } from 'react'
import { Fraction, Price, Token, TokenAmount, WETH9 } from '@uniswap/sdk-core'
import { Fraction, Percent, Price, Token, TokenAmount, WETH9 } from '@uniswap/sdk-core'
import { FACTORY_ADDRESS, JSBI } from '@uniswap/v2-sdk'
import { Redirect, RouteComponentProps } from 'react-router'
import { Text } from 'rebass'
import { AutoColumn } from '../../components/Column'
import CurrencyLogo from '../../components/CurrencyLogo'
import FormattedCurrencyAmount from '../../components/FormattedCurrencyAmount'
import QuestionHelper from '../../components/QuestionHelper'
import { AutoRow, RowBetween, RowFixed } from '../../components/Row'
import { useV2LiquidityTokenPermit } from '../../hooks/useERC20Permit'
import { useTotalSupply } from '../../hooks/useTotalSupply'
@@ -16,7 +15,7 @@ import { usePairContract, useV2MigratorContract } from '../../hooks/useContract'
import { NEVER_RELOAD, useSingleCallResult } from '../../state/multicall/hooks'
import { useTokenBalance } from '../../state/wallet/hooks'
import { BackArrow, ExternalLink, TYPE } from '../../theme'
import { getEtherscanLink, isAddress } from '../../utils'
import { calculateGasMargin, getEtherscanLink, isAddress } from '../../utils'
import { BodyWrapper } from '../AppBody'
import { V3_MIGRATOR_ADDRESSES } from 'constants/v3'
import { PoolState, usePool } from 'hooks/usePools'
@@ -26,7 +25,7 @@ import { ApprovalState, useApproveCallback } from 'hooks/useApproveCallback'
import { Dots } from 'components/swap/styleds'
import { ButtonConfirmed } from 'components/Button'
import useTransactionDeadline from 'hooks/useTransactionDeadline'
import { useUserSlippageTolerance } from 'state/user/hooks'
import { useUserSlippageToleranceWithDefault } from 'state/user/hooks'
import ReactGA from 'react-ga'
import { TransactionResponse } from '@ethersproject/providers'
import { useIsTransactionPending, useTransactionAdder } from 'state/transactions/hooks'
@@ -46,9 +45,12 @@ import DoubleCurrencyLogo from 'components/DoubleLogo'
import Badge, { BadgeVariant } from 'components/Badge'
import { useDispatch } from 'react-redux'
import { AppDispatch } from 'state'
import SettingsTab from 'components/Settings'
const ZERO = JSBI.BigInt(0)
const DEFAULT_MIGRATE_SLIPPAGE_TOLERANCE = new Percent(75, 10_000)
function EmptyState({ message }: { message: string }) {
return (
<AutoColumn style={{ minHeight: 200, justifyContent: 'center', alignItems: 'center' }}>
@@ -119,7 +121,7 @@ function V2PairMigration({
const deadline = useTransactionDeadline() // custom from users settings
const blockTimestamp = useCurrentBlockTimestamp()
const [allowedSlippage] = useUserSlippageTolerance() // custom from users
const allowedSlippage = useUserSlippageToleranceWithDefault(DEFAULT_MIGRATE_SLIPPAGE_TOLERANCE) // custom from users
const currency0 = unwrappedToken(token0)
const currency1 = unwrappedToken(token1)
@@ -274,7 +276,7 @@ function V2PairMigration({
const deadlineToUse = signatureData?.deadline ?? deadline
const data = []
const data: string[] = []
// permit if necessary
if (signatureData) {
@@ -324,19 +326,24 @@ function V2PairMigration({
)
setConfirmingMigration(true)
migrator
.multicall(data)
.then((response: TransactionResponse) => {
ReactGA.event({
category: 'Migrate',
action: `${isNotUniswap ? 'SushiSwap' : 'V2'}->V3`,
label: `${currency0.symbol}/${currency1.symbol}`,
})
addTransaction(response, {
summary: `Migrate ${currency0.symbol}/${currency1.symbol} liquidity to V3`,
})
setPendingMigrationHash(response.hash)
migrator.estimateGas
.multicall(data)
.then((gasEstimate) => {
return migrator
.multicall(data, { gasLimit: calculateGasMargin(gasEstimate) })
.then((response: TransactionResponse) => {
ReactGA.event({
category: 'Migrate',
action: `${isNotUniswap ? 'SushiSwap' : 'V2'}->V3`,
label: `${currency0.symbol}/${currency1.symbol}`,
})
addTransaction(response, {
summary: `Migrate ${currency0.symbol}/${currency1.symbol} liquidity to V3`,
})
setPendingMigrationHash(response.hash)
})
})
.catch(() => {
setConfirmingMigration(false)
@@ -668,9 +675,7 @@ export default function MigrateV2Pair({
<AutoRow style={{ alignItems: 'center', justifyContent: 'space-between' }} gap="8px">
<BackArrow to="/migrate/v2" />
<TYPE.mediumHeader>Migrate V2 Liquidity</TYPE.mediumHeader>
<div style={{ opacity: 0 }}>
<QuestionHelper text="Migrate your liquidity tokens from Uniswap V2 to Uniswap V3." />
</div>
<SettingsTab placeholderSlippage={DEFAULT_MIGRATE_SLIPPAGE_TOLERANCE} />
</AutoRow>
{!account ? (

View File

@@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next'
import { ExternalLink } from '../../theme'
import { AutoColumn } from 'components/Column'
import Squiggle from '../../assets/images/squiggle.png'
import Texture from '../../assets/images/sandtexture.png'
import Texture from '../../assets/images/sandtexture.webp'
import { RowBetween } from 'components/Row'
const CTASection = styled.section`
@@ -28,6 +28,7 @@ const CTA1 = styled(ExternalLink)`
border-radius: 20px;
display: flex;
flex-direction: column;
position: relative;
justify-content: space-between;
border: 1px solid ${({ theme }) => theme.bg3};
@@ -99,8 +100,8 @@ const HeaderText = styled(TYPE.label)`
`
const ResponsiveColumn = styled(AutoColumn)`
grid-template-columns: 1fr;
gap: 12px;
height: 100%;
${({ theme }) => theme.mediaWidth.upToMedium`
gap: 8px;
`};
@@ -108,6 +109,7 @@ const ResponsiveColumn = styled(AutoColumn)`
`
const StyledImage = styled.img`
height: 114px;
margin-top: -28px;
${({ theme }) => theme.mediaWidth.upToMedium`
height: 80px;
@@ -134,12 +136,10 @@ export default function CTACards() {
</CTA1>
<CTA2 href={'https://info.uniswap.org/#/pools'}>
<ResponsiveColumn>
<AutoColumn gap="0px">
<HeaderText style={{ alignSelf: 'flex-start' }}>{t('Top pools')}</HeaderText>
<TYPE.body fontWeight={300} style={{ alignSelf: 'flex-start' }}>
{t('Explore popular pools on Uniswap Analytics.')}
</TYPE.body>
</AutoColumn>
<HeaderText style={{ alignSelf: 'flex-start' }}>{t('Top pools')}</HeaderText>
<TYPE.body fontWeight={300} style={{ alignSelf: 'flex-start' }}>
{t('Explore popular pools on Uniswap Analytics.')}
</TYPE.body>
<HeaderText style={{ alignSelf: 'flex-end' }}>{t('↗')}</HeaderText>
</ResponsiveColumn>
</CTA2>

View File

@@ -16,7 +16,7 @@ import { ExternalLink, HideExtraSmall, TYPE } from 'theme'
import Badge from 'components/Badge'
import { calculateGasMargin, getEtherscanLink } from 'utils'
import { ButtonConfirmed, ButtonPrimary, ButtonGray } from 'components/Button'
import { DarkCard, DarkGreyCard, LightCard } from 'components/Card'
import { DarkCard, LightCard } from 'components/Card'
import CurrencyLogo from 'components/CurrencyLogo'
import { useTranslation } from 'react-i18next'
import { currencyId } from 'utils/currencyId'
@@ -147,9 +147,9 @@ function CurrentPriceCard({
<AutoColumn gap="8px" justify="center">
<ExtentsText>{t('Current price')}</ExtentsText>
<TYPE.mediumHeader textAlign="center">
{(inverted ? pool.token1Price : pool.token0Price).toSignificant(4)}{' '}
{(inverted ? pool.token1Price : pool.token0Price).toSignificant(5)}{' '}
</TYPE.mediumHeader>
<ExtentsText>{currencyQuote?.symbol + ' / ' + currencyBase?.symbol}</ExtentsText>
<ExtentsText>{currencyQuote?.symbol + ' per ' + currencyBase?.symbol}</ExtentsText>
</AutoColumn>
</LightCard>
)
@@ -420,7 +420,7 @@ export function PositionPage({
borderRadius="12px"
style={{ marginRight: '8px' }}
>
{t('Add Liquidity')}
{t('Increase Liquidity')}
</ButtonGray>
) : null}
{tokenId && !removed ? (
@@ -498,11 +498,9 @@ export function PositionPage({
{inverted ? position?.amount0.toSignificant(4) : position?.amount1.toSignificant(4)}
</TYPE.main>
{typeof ratio === 'number' && !removed ? (
<DarkGreyCard padding="4px 6px" style={{ width: 'fit-content', marginLeft: '8px' }}>
<TYPE.main color={theme.text2} fontSize={11}>
{inverted ? ratio : 100 - ratio}%
</TYPE.main>
</DarkGreyCard>
<Badge style={{ marginLeft: '10px' }}>
<TYPE.main fontSize={11}>{inverted ? ratio : 100 - ratio}%</TYPE.main>
</Badge>
) : null}
</RowFixed>
</RowBetween>
@@ -516,11 +514,11 @@ export function PositionPage({
{inverted ? position?.amount1.toSignificant(4) : position?.amount0.toSignificant(4)}
</TYPE.main>
{typeof ratio === 'number' && !removed ? (
<DarkGreyCard padding="4px 6px" style={{ width: 'fit-content', marginLeft: '8px' }}>
<Badge style={{ marginLeft: '10px' }}>
<TYPE.main color={theme.text2} fontSize={11}>
{inverted ? 100 - ratio : ratio}%
</TYPE.main>
</DarkGreyCard>
</Badge>
) : null}
</RowFixed>
</RowBetween>
@@ -640,8 +638,8 @@ export function PositionPage({
<LightCard padding="12px" width="100%">
<AutoColumn gap="8px" justify="center">
<ExtentsText>Min price</ExtentsText>
<TYPE.mediumHeader textAlign="center">{priceLower?.toSignificant(4)}</TYPE.mediumHeader>
<ExtentsText> {currencyQuote?.symbol + ' / ' + currencyBase?.symbol}</ExtentsText>
<TYPE.mediumHeader textAlign="center">{priceLower?.toSignificant(5)}</TYPE.mediumHeader>
<ExtentsText> {currencyQuote?.symbol + ' per ' + currencyBase?.symbol}</ExtentsText>
{inRange && (
<TYPE.small color={theme.text3}>
@@ -655,8 +653,8 @@ export function PositionPage({
<LightCard padding="12px" width="100%">
<AutoColumn gap="8px" justify="center">
<ExtentsText>Max price</ExtentsText>
<TYPE.mediumHeader textAlign="center">{priceUpper?.toSignificant(4)}</TYPE.mediumHeader>
<ExtentsText> {currencyQuote?.symbol + ' / ' + currencyBase?.symbol}</ExtentsText>
<TYPE.mediumHeader textAlign="center">{priceUpper?.toSignificant(5)}</TYPE.mediumHeader>
<ExtentsText> {currencyQuote?.symbol + ' per ' + currencyBase?.symbol}</ExtentsText>
{inRange && (
<TYPE.small color={theme.text3}>

View File

@@ -1,5 +1,5 @@
import React, { useContext } from 'react'
import { ButtonGray, ButtonPrimary } from 'components/Button'
import { ButtonGray, ButtonOutlined, ButtonPrimary } from 'components/Button'
import { AutoColumn } from 'components/Column'
import { FlyoutAlignment, NewMenu } from 'components/Menu'
import { SwapPoolTabs } from 'components/NavigationTabs'
@@ -217,14 +217,40 @@ export default function Pool() {
)}
</MainContentWrapper>
<RowFixed justify="center" style={{ width: '100%' }}>
<ButtonGray
<ButtonOutlined
as={Link}
to="/pool/v2"
id="import-pool-link"
style={{ padding: '8px 16px', borderRadius: '12px', width: 'fit-content' }}
style={{
padding: '8px 16px',
margin: '0 4px',
borderRadius: '12px',
width: 'fit-content',
fontSize: '14px',
}}
>
<TYPE.subHeader>{t('Looking for your V2 Liquidity')}?</TYPE.subHeader>
</ButtonGray>
<Layers size={14} style={{ marginRight: '8px' }} />
{t('View V2 Liquidity')}
</ButtonOutlined>
{positions && positions.length > 0 && (
<ButtonOutlined
as={Link}
to="/migrate/v2"
id="import-pool-link"
style={{
padding: '8px 16px',
margin: '0 4px',
borderRadius: '12px',
width: 'fit-content',
fontSize: '14px',
}}
>
<ChevronsRight size={16} style={{ marginRight: '8px' }} />
{t('Migrate Liquidity')}
</ButtonOutlined>
)}
</RowFixed>
</AutoColumn>
</AutoColumn>

View File

@@ -9,7 +9,9 @@ import { ExternalLink, TYPE, HideSmall } from '../../theme'
import { Text } from 'rebass'
import Card from '../../components/Card'
import { RowBetween, RowFixed } from '../../components/Row'
import { ButtonPrimary, ButtonSecondary } from '../../components/Button'
import { ButtonPrimary, ButtonSecondary, ButtonOutlined } from '../../components/Button'
import { ChevronsRight } from 'react-feather'
import { AutoColumn } from '../../components/Column'
import { useActiveWeb3React } from '../../hooks'
@@ -151,7 +153,7 @@ export default function Pool() {
</VoteCard>
<AutoColumn gap="lg" justify="center">
<AutoColumn gap="lg" style={{ width: '100%' }}>
<AutoColumn gap="md" style={{ width: '100%' }}>
<TitleRow style={{ marginTop: '1rem' }} padding={'0'}>
<HideSmall>
<TYPE.mediumHeader style={{ marginTop: '0.5rem', justifySelf: 'flex-start' }}>
@@ -170,7 +172,7 @@ export default function Pool() {
to="/add/v2/ETH"
>
<Text fontWeight={500} fontSize={16}>
Add Liquidity
Add V2 Liquidity
</Text>
</ResponsiveButtonPrimary>
</ButtonRow>
@@ -211,6 +213,23 @@ export default function Pool() {
/>
)
)}
<RowFixed justify="center" style={{ width: '100%' }}>
<ButtonOutlined
as={Link}
to="/migrate/v2"
id="import-pool-link"
style={{
padding: '8px 16px',
margin: '0 4px',
borderRadius: '12px',
width: 'fit-content',
fontSize: '14px',
}}
>
<ChevronsRight size={16} style={{ marginRight: '8px' }} />
Migrate Liquidity to V3
</ButtonOutlined>
</RowFixed>
</>
) : (
<EmptyProposals>

View File

@@ -15,13 +15,13 @@ import { Text } from 'rebass'
import CurrencyLogo from 'components/CurrencyLogo'
import FormattedCurrencyAmount from 'components/FormattedCurrencyAmount'
import { useV3NFTPositionManagerContract } from 'hooks/useContract'
import { useUserSlippageTolerance } from 'state/user/hooks'
import { useUserSlippageToleranceWithDefault } from 'state/user/hooks'
import useTransactionDeadline from 'hooks/useTransactionDeadline'
import ReactGA from 'react-ga'
import { useActiveWeb3React } from 'hooks'
import { TransactionResponse } from '@ethersproject/providers'
import { useTransactionAdder } from 'state/transactions/hooks'
import { WETH9, CurrencyAmount } from '@uniswap/sdk-core'
import { WETH9, CurrencyAmount, Percent } from '@uniswap/sdk-core'
import { TYPE } from 'theme'
import { Wrapper, SmallMaxButton, ResponsiveHeaderText } from './styled'
import Loader from 'components/Loader'
@@ -37,6 +37,8 @@ import RangeBadge from 'components/Badge/RangeBadge'
export const UINT128MAX = BigNumber.from(2).pow(128).sub(1)
const DEFAULT_REMOVE_V3_LIQUIDITY_SLIPPAGE_TOLERANCE = new Percent(5, 100)
// redirect invalid tokenIds
export default function RemoveLiquidityV3({
location,
@@ -89,7 +91,7 @@ function Remove({ tokenId }: { tokenId: BigNumber }) {
const [percentForSlider, onPercentSelectForSlider] = useDebouncedChangeHandler(percent, onPercentSelect)
const deadline = useTransactionDeadline() // custom from users settings
const [allowedSlippage] = useUserSlippageTolerance() // custom from users
const allowedSlippage = useUserSlippageToleranceWithDefault(DEFAULT_REMOVE_V3_LIQUIDITY_SLIPPAGE_TOLERANCE) // custom from users
const [showConfirm, setShowConfirm] = useState(false)
const [attemptingTxn, setAttemptingTxn] = useState(false)
@@ -274,7 +276,12 @@ function Remove({ tokenId }: { tokenId: BigNumber }) {
pendingText={pendingText}
/>
<AppBody>
<AddRemoveTabs creating={false} adding={false} positionID={tokenId.toString()} />
<AddRemoveTabs
creating={false}
adding={false}
positionID={tokenId.toString()}
defaultSlippage={DEFAULT_REMOVE_V3_LIQUIDITY_SLIPPAGE_TOLERANCE}
/>
<Wrapper>
{position ? (
<AutoColumn gap="lg">

View File

@@ -40,9 +40,11 @@ import { useBurnActionHandlers } from '../../state/burn/hooks'
import { useDerivedBurnInfo, useBurnState } from '../../state/burn/hooks'
import { Field } from '../../state/burn/actions'
import { useWalletModalToggle } from '../../state/application/hooks'
import { useUserSlippageTolerance } from '../../state/user/hooks'
import { useUserSlippageToleranceWithDefault } from '../../state/user/hooks'
import { BigNumber } from '@ethersproject/bignumber'
const DEFAULT_REMOVE_LIQUIDITY_SLIPPAGE_TOLERANCE = new Percent(5, 100)
export default function RemoveLiquidity({
history,
match: {
@@ -76,7 +78,7 @@ export default function RemoveLiquidity({
// txn values
const [txHash, setTxHash] = useState<string>('')
const deadline = useTransactionDeadline()
const [allowedSlippage] = useUserSlippageTolerance()
const allowedSlippage = useUserSlippageToleranceWithDefault(DEFAULT_REMOVE_LIQUIDITY_SLIPPAGE_TOLERANCE)
const formattedAmounts = {
[Field.LIQUIDITY_PERCENT]: parsedAmounts[Field.LIQUIDITY_PERCENT].equalTo('0')
@@ -426,7 +428,7 @@ export default function RemoveLiquidity({
return (
<>
<AppBody>
<AddRemoveTabs creating={false} adding={false} />
<AddRemoveTabs creating={false} adding={false} defaultSlippage={DEFAULT_REMOVE_LIQUIDITY_SLIPPAGE_TOLERANCE} />
<Wrapper>
<TransactionConfirmationModal
isOpen={showConfirm}

View File

@@ -46,7 +46,7 @@ import {
useSwapActionHandlers,
useSwapState,
} from '../../state/swap/hooks'
import { useExpertModeManager, useUserSingleHopOnly, useUserSlippageTolerance } from '../../state/user/hooks'
import { useExpertModeManager, useUserSingleHopOnly } from '../../state/user/hooks'
import { HideSmall, LinkStyledButton, TYPE } from '../../theme'
import { computeFiatValuePriceImpact } from '../../utils/computeFiatValuePriceImpact'
import { computePriceImpactWithMaximumSlippage } from '../../utils/computePriceImpactWithMaximumSlippage'
@@ -100,19 +100,21 @@ export default function Swap({ history }: RouteComponentProps) {
// for expert mode
const [isExpertMode] = useExpertModeManager()
// get custom setting values for user
const [allowedSlippage] = useUserSlippageTolerance()
// get version from the url
const toggledVersion = useToggledVersion()
// swap state
const { independentField, typedValue, recipient } = useSwapState()
const {
v2Trade,
v3TradeState: { trade: v3Trade, state: v3TradeState },
toggledTrade: trade,
allowedSlippage,
currencyBalances,
parsedAmount,
currencies,
inputError: swapInputError,
} = useDerivedSwapInfo()
} = useDerivedSwapInfo(toggledVersion)
const { wrapType, execute: onWrap, inputError: wrapInputError } = useWrapCallback(
currencies[Field.INPUT],
@@ -121,13 +123,6 @@ export default function Swap({ history }: RouteComponentProps) {
)
const showWrap: boolean = wrapType !== WrapType.NOT_APPLICABLE
const { address: recipientAddress } = useENSAddress(recipient)
const toggledVersion = useToggledVersion()
const trade = showWrap
? undefined
: {
[Version.v2]: v2Trade,
[Version.v3]: v3Trade ?? undefined,
}[toggledVersion]
const parsedAmounts = useMemo(
() =>
@@ -357,7 +352,7 @@ export default function Swap({ history }: RouteComponentProps) {
onDismiss={handleDismissTokenWarning}
/>
<AppBody>
<SwapHeader />
<SwapHeader allowedSlippage={allowedSlippage} />
<Wrapper id="swap-page">
<ConfirmSwapModal
isOpen={showConfirm}
@@ -484,7 +479,9 @@ export default function Swap({ history }: RouteComponentProps) {
showInverted={showInverted}
setShowInverted={setShowInverted}
/>
<MouseoverTooltipContent content={<AdvancedSwapDetails trade={trade} />}>
<MouseoverTooltipContent
content={<AdvancedSwapDetails trade={trade} allowedSlippage={allowedSlippage} />}
>
<StyledInfo />
</MouseoverTooltipContent>
</RowFixed>

View File

@@ -235,7 +235,7 @@ export default function Vote() {
})}
</TopSection>
<TYPE.subHeader color="text3">
A minimum threshhold of 1% of the total UNI supply is required to submit proposals
A minimum threshold of 1% of the total UNI supply is required to submit proposals
</TYPE.subHeader>
</PageWrapper>
)

View File

@@ -10,6 +10,7 @@ import {
TickMath,
tickToPrice,
TICK_SPACINGS,
encodeSqrtRatioX96,
} from '@uniswap/v3-sdk/dist/'
import { Currency, CurrencyAmount, ETHER, Price, Rounding } from '@uniswap/sdk-core'
import { useCallback, useMemo } from 'react'
@@ -185,16 +186,29 @@ export function useV3DerivedMintInfo(
}
}, [noLiquidity, startPriceTypedValue, invertPrice, token1, token0, pool])
// check for invalid price input (converts to invalid ratio)
const invalidPrice = useMemo(() => {
const sqrtRatioX96 = price ? encodeSqrtRatioX96(price.raw.numerator, price.raw.denominator) : undefined
const invalid =
price &&
sqrtRatioX96 &&
!(
JSBI.greaterThanOrEqual(sqrtRatioX96, TickMath.MIN_SQRT_RATIO) &&
JSBI.lessThan(sqrtRatioX96, TickMath.MAX_SQRT_RATIO)
)
return invalid
}, [price])
// used for ratio calculation when pool not initialized
const mockPool = useMemo(() => {
if (tokenA && tokenB && feeAmount && price) {
if (tokenA && tokenB && feeAmount && price && !invalidPrice) {
const currentTick = priceToClosestTick(price)
const currentSqrt = TickMath.getSqrtRatioAtTick(currentTick)
return new Pool(tokenA, tokenB, feeAmount, currentSqrt, JSBI.BigInt(0), currentTick, [])
} else {
return undefined
}
}, [feeAmount, price, tokenA, tokenB])
}, [feeAmount, invalidPrice, price, tokenA, tokenB])
// if pool exists use it, if not use the mock pool
const poolForPosition: Pool | undefined = pool ?? mockPool
@@ -374,6 +388,10 @@ export function useV3DerivedMintInfo(
errorMessage = errorMessage ?? 'Invalid pair'
}
if (invalidPrice) {
errorMessage = errorMessage ?? 'Invalid price input'
}
if (
(!parsedAmounts[Field.CURRENCY_A] && !depositADisabled) ||
(!parsedAmounts[Field.CURRENCY_B] && !depositBDisabled)

View File

@@ -25,8 +25,8 @@ describe('hooks', () => {
expect(
queryParametersToSwapState(parse('?outputCurrency=invalid', { parseArrays: false, ignoreQueryPrefix: true }))
).toEqual({
[Field.INPUT]: { currencyId: '' },
[Field.OUTPUT]: { currencyId: 'ETH' },
[Field.INPUT]: { currencyId: 'ETH' },
[Field.OUTPUT]: { currencyId: '' },
typedValue: '',
independentField: Field.INPUT,
recipient: null,

View File

@@ -2,13 +2,15 @@ import { Trade as V3Trade } from '@uniswap/v3-sdk'
import { useBestV3TradeExactIn, useBestV3TradeExactOut, V3TradeState } from '../../hooks/useBestV3Trade'
import useENS from '../../hooks/useENS'
import { parseUnits } from '@ethersproject/units'
import { Currency, CurrencyAmount, ETHER, Token, TokenAmount } from '@uniswap/sdk-core'
import { Currency, CurrencyAmount, ETHER, Token, TokenAmount, Percent } from '@uniswap/sdk-core'
import { JSBI, Trade as V2Trade } from '@uniswap/v2-sdk'
import { ParsedQs } from 'qs'
import { useCallback, useEffect, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { useActiveWeb3React } from '../../hooks'
import { useCurrency } from '../../hooks/Tokens'
import useSwapSlippageTolerance from '../../hooks/useSwapSlippageTolerance'
import { Version } from '../../hooks/useToggledVersion'
import { useV2TradeExactIn, useV2TradeExactOut } from '../../hooks/useV2Trade'
import useParsedQueryString from '../../hooks/useParsedQueryString'
import { isAddress } from '../../utils'
@@ -16,7 +18,6 @@ import { AppDispatch, AppState } from '../index'
import { useCurrencyBalances } from '../wallet/hooks'
import { Field, replaceSwapState, selectCurrency, setRecipient, switchCurrencies, typeInput } from './actions'
import { SwapState } from './reducer'
import { useUserSlippageTolerance } from '../user/hooks'
export function useSwapState(): AppState['swap'] {
return useSelector<AppState, AppState['swap']>((state) => state.swap)
@@ -109,13 +110,17 @@ function involvesAddress(trade: V2Trade | V3Trade, checksummedAddress: string):
}
// from the current swap inputs, compute the best trade and return it.
export function useDerivedSwapInfo(): {
export function useDerivedSwapInfo(
toggledVersion: Version
): {
currencies: { [field in Field]?: Currency }
currencyBalances: { [field in Field]?: CurrencyAmount }
parsedAmount: CurrencyAmount | undefined
v2Trade: V2Trade | undefined
inputError?: string
v2Trade: V2Trade | undefined
v3TradeState: { trade: V3Trade | null; state: V3TradeState }
toggledTrade: V2Trade | V3Trade | undefined
allowedSlippage: Percent
} {
const { account } = useActiveWeb3React()
@@ -185,7 +190,8 @@ export function useDerivedSwapInfo(): {
}
}
const [allowedSlippage] = useUserSlippageTolerance()
const toggledTrade = (toggledVersion === Version.v2 ? v2Trade : v3Trade.trade) ?? undefined
const allowedSlippage = useSwapSlippageTolerance(toggledTrade)
// compare input balance to max input based on version
const [balanceIn, amountIn] = [currencyBalances[Field.INPUT], v2Trade?.maximumAmountIn(allowedSlippage)]
@@ -198,9 +204,11 @@ export function useDerivedSwapInfo(): {
currencies,
currencyBalances,
parsedAmount,
v2Trade: v2Trade ?? undefined,
inputError,
v2Trade: v2Trade ?? undefined,
v3TradeState: v3Trade,
toggledTrade,
allowedSlippage,
}
}
@@ -209,9 +217,8 @@ function parseCurrencyFromURLParameter(urlParam: any): string {
const valid = isAddress(urlParam)
if (valid) return valid
if (urlParam.toUpperCase() === 'ETH') return 'ETH'
if (valid === false) return 'ETH'
}
return 'ETH' ?? ''
return ''
}
function parseTokenAmountURLParameter(urlParam: any): string {
@@ -236,12 +243,12 @@ function validatedRecipient(recipient: any): string | null {
export function queryParametersToSwapState(parsedQs: ParsedQs): SwapState {
let inputCurrency = parseCurrencyFromURLParameter(parsedQs.inputCurrency)
let outputCurrency = parseCurrencyFromURLParameter(parsedQs.outputCurrency)
if (inputCurrency === outputCurrency) {
if (typeof parsedQs.outputCurrency === 'string') {
inputCurrency = ''
} else {
outputCurrency = ''
}
if (inputCurrency === '' && outputCurrency === '') {
// default to ETH input
inputCurrency = 'ETH'
} else if (inputCurrency === outputCurrency) {
// clear output if identical
outputCurrency = ''
}
const recipient = validatedRecipient(parsedQs.recipient)

View File

@@ -17,7 +17,7 @@ export const updateMatchesDarkMode = createAction<{ matchesDarkMode: boolean }>(
export const updateUserDarkMode = createAction<{ userDarkMode: boolean }>('user/updateUserDarkMode')
export const updateUserExpertMode = createAction<{ userExpertMode: boolean }>('user/updateUserExpertMode')
export const updateUserSingleHopOnly = createAction<{ userSingleHopOnly: boolean }>('user/updateUserSingleHopOnly')
export const updateUserSlippageTolerance = createAction<{ userSlippageTolerance: number }>(
export const updateUserSlippageTolerance = createAction<{ userSlippageTolerance: number | 'auto' }>(
'user/updateUserSlippageTolerance'
)
export const updateUserDeadline = createAction<{ userDeadline: number }>('user/updateUserDeadline')

View File

@@ -1,5 +1,6 @@
import { ChainId, Percent, Token } from '@uniswap/sdk-core'
import { Pair } from '@uniswap/v2-sdk'
import JSBI from 'jsbi'
import flatMap from 'lodash.flatmap'
import { useCallback, useMemo } from 'react'
import { shallowEqual, useDispatch, useSelector } from 'react-redux'
@@ -14,12 +15,12 @@ import {
removeSerializedToken,
SerializedPair,
SerializedToken,
toggleURLWarning,
updateUserDarkMode,
updateUserDeadline,
updateUserExpertMode,
updateUserSlippageTolerance,
toggleURLWarning,
updateUserSingleHopOnly,
updateUserSlippageTolerance,
} from './actions'
function serializeToken(token: Token): SerializedToken {
@@ -100,22 +101,51 @@ export function useUserSingleHopOnly(): [boolean, (newSingleHopOnly: boolean) =>
return [singleHopOnly, setSingleHopOnly]
}
export function useUserSlippageTolerance(): [Percent, (slippageBips: number) => void] {
export function useSetUserSlippageTolerance(): (slippageTolerance: Percent | 'auto') => void {
const dispatch = useDispatch<AppDispatch>()
return useCallback(
(userSlippageTolerance: Percent | 'auto') => {
let value: 'auto' | number
try {
value =
userSlippageTolerance === 'auto' ? 'auto' : JSBI.toNumber(userSlippageTolerance.multiply(10_000).quotient)
} catch (error) {
value = 'auto'
}
dispatch(
updateUserSlippageTolerance({
userSlippageTolerance: value,
})
)
},
[dispatch]
)
}
/**
* Return the user's slippage tolerance, from the redux store, and a function to update the slippage tolerance
*/
export function useUserSlippageTolerance(): Percent | 'auto' {
const userSlippageTolerance = useSelector<AppState, AppState['user']['userSlippageTolerance']>((state) => {
return state.user.userSlippageTolerance
})
const percentage = useMemo(() => new Percent(userSlippageTolerance, 10_000), [userSlippageTolerance])
return useMemo(() => (userSlippageTolerance === 'auto' ? 'auto' : new Percent(userSlippageTolerance, 10_000)), [
userSlippageTolerance,
])
}
const setUserSlippageTolerance = useCallback(
(userSlippageTolerance: number) => {
dispatch(updateUserSlippageTolerance({ userSlippageTolerance }))
},
[dispatch]
)
return [percentage, setUserSlippageTolerance]
/**
* Same as above but replaces the auto with a default value
* @param defaultSlippageTolerance the default value to replace auto with
*/
export function useUserSlippageToleranceWithDefault(defaultSlippageTolerance: Percent): Percent {
const allowedSlippage = useUserSlippageTolerance()
return useMemo(() => (allowedSlippage === 'auto' ? defaultSlippageTolerance : allowedSlippage), [
allowedSlippage,
defaultSlippageTolerance,
])
}
export function useUserTransactionTTL(): [number, (slippage: number) => void] {

View File

@@ -27,7 +27,7 @@ describe('swap reducer', () => {
} as any)
store.dispatch(updateVersion())
expect(store.getState().userDeadline).toEqual(DEFAULT_DEADLINE_FROM_NOW)
expect(store.getState().userSlippageTolerance).toEqual(10)
expect(store.getState().userSlippageTolerance).toEqual('auto')
})
})
})

View File

@@ -31,7 +31,8 @@ export interface UserState {
userSingleHopOnly: boolean // only allow swaps on direct pairs
// user defined slippage tolerance in bips, used in all txns
userSlippageTolerance: number
userSlippageTolerance: number | 'auto'
userSlippageToleranceHasBeenMigratedToAuto: boolean // temporary flag for migration status
// deadline set by user in minutes, used in all txns
userDeadline: number
@@ -62,7 +63,8 @@ export const initialState: UserState = {
matchesDarkMode: false,
userExpertMode: false,
userSingleHopOnly: false,
userSlippageTolerance: 10,
userSlippageTolerance: 'auto',
userSlippageToleranceHasBeenMigratedToAuto: true,
userDeadline: DEFAULT_DEADLINE_FROM_NOW,
tokens: {},
pairs: {},
@@ -75,13 +77,26 @@ export default createReducer(initialState, (builder) =>
.addCase(updateVersion, (state) => {
// slippage isnt being tracked in local storage, reset to default
// noinspection SuspiciousTypeOfGuard
if (typeof state.userSlippageTolerance !== 'number') {
state.userSlippageTolerance = 10
if (
typeof state.userSlippageTolerance !== 'number' ||
!Number.isInteger(state.userSlippageTolerance) ||
state.userSlippageTolerance < 0 ||
state.userSlippageTolerance > 5000
) {
state.userSlippageTolerance = 'auto'
} else {
if (
!state.userSlippageToleranceHasBeenMigratedToAuto &&
[10, 50, 100].indexOf(state.userSlippageTolerance) !== -1
) {
state.userSlippageTolerance = 'auto'
state.userSlippageToleranceHasBeenMigratedToAuto = true
}
}
// deadline isnt being tracked in local storage, reset to default
// noinspection SuspiciousTypeOfGuard
if (typeof state.userDeadline !== 'number') {
if (typeof state.userDeadline !== 'number' || !Number.isInteger(state.userDeadline) || state.userDeadline < 60) {
state.userDeadline = DEFAULT_DEADLINE_FROM_NOW
}

View File

@@ -10,11 +10,11 @@ export function formatTokenAmount(amount: TokenAmount | undefined, sigFigs: numb
return '0'
}
if (parseFloat(amount.toFixed(sigFigs)) < 0.0001) {
if (parseFloat(amount.toFixed(Math.min(sigFigs, amount.token.decimals))) < 0.0001) {
return '<0.0001'
}
return amount.toSignificant(sigFigs)
return amount.toFixed(Math.min(sigFigs, amount.token.decimals))
}
export function formatPrice(price: Price | undefined, sigFigs: number) {

View File

@@ -1,8 +1,15 @@
import { Web3Provider } from '@ethersproject/providers'
import { Web3Provider, Network } from '@ethersproject/providers'
class WorkaroundWeb3Provider extends Web3Provider {
private _detectNetworkResult: Promise<Network> | null = null
async detectNetwork(): Promise<Network> {
return this._detectNetworkResult ?? (this._detectNetworkResult = this._uncachedDetectNetwork())
}
}
export default function getLibrary(provider: any): Web3Provider {
// latest ethers version tries to detect the network which fails
const library = new Web3Provider(
const library = new WorkaroundWeb3Provider(
provider,
typeof provider.chainId === 'number'
? provider.chainId

View File

@@ -0,0 +1,6 @@
import { UAParser } from 'ua-parser-js'
export function getUserAgent(): UAParser.IResult {
const parser = new UAParser(window.navigator.userAgent)
return parser.getResult()
}

View File

@@ -28,11 +28,40 @@ describe('utils', () => {
describe('#calculateSlippageAmount', () => {
it('bounds are correct', () => {
const tokenAmount = new TokenAmount(new Token(ChainId.MAINNET, AddressZero, 0), '100')
expect(() => calculateSlippageAmount(tokenAmount, new Percent(-1, 10_000))).toThrow()
expect(() => calculateSlippageAmount(tokenAmount, new Percent(-1, 10_000))).toThrow('Unexpected slippage')
expect(() => calculateSlippageAmount(tokenAmount, new Percent(10_001, 10_000))).toThrow('Unexpected slippage')
expect(calculateSlippageAmount(tokenAmount, new Percent(0, 10_000)).map((bound) => bound.toString())).toEqual([
'100',
'100',
])
expect(calculateSlippageAmount(tokenAmount, new Percent(5, 100)).map((bound) => bound.toString())).toEqual([
'95',
'105',
])
expect(calculateSlippageAmount(tokenAmount, new Percent(100, 10_000)).map((bound) => bound.toString())).toEqual([
'99',
'101',
])
expect(calculateSlippageAmount(tokenAmount, new Percent(200, 10_000)).map((bound) => bound.toString())).toEqual([
'98',
'102',
])
expect(
calculateSlippageAmount(tokenAmount, new Percent(10000, 10_000)).map((bound) => bound.toString())
).toEqual(['0', '200'])
})
it('works for 18 decimals', () => {
const tokenAmount = new TokenAmount(new Token(ChainId.MAINNET, AddressZero, 18), '100')
expect(() => calculateSlippageAmount(tokenAmount, new Percent(-1, 10_000))).toThrow('Unexpected slippage')
expect(() => calculateSlippageAmount(tokenAmount, new Percent(10_001, 10_000))).toThrow('Unexpected slippage')
expect(calculateSlippageAmount(tokenAmount, new Percent(0, 10_000)).map((bound) => bound.toString())).toEqual([
'100',
'100',
])
expect(calculateSlippageAmount(tokenAmount, new Percent(5, 100)).map((bound) => bound.toString())).toEqual([
'95',
'105',
])
expect(calculateSlippageAmount(tokenAmount, new Percent(100, 10_000)).map((bound) => bound.toString())).toEqual([
'99',
'101',
@@ -44,7 +73,6 @@ describe('utils', () => {
expect(
calculateSlippageAmount(tokenAmount, new Percent(10000, 10_000)).map((bound) => bound.toString())
).toEqual(['0', '200'])
expect(() => calculateSlippageAmount(tokenAmount, new Percent(10001, 10_000))).toThrow()
})
})

View File

@@ -3,7 +3,7 @@ import { getAddress } from '@ethersproject/address'
import { AddressZero } from '@ethersproject/constants'
import { JsonRpcSigner, Web3Provider } from '@ethersproject/providers'
import { BigNumber } from '@ethersproject/bignumber'
import { ChainId, Percent, Token, CurrencyAmount, Currency, ETHER } from '@uniswap/sdk-core'
import { ChainId, Percent, Token, CurrencyAmount, Currency, ETHER, Fraction } from '@uniswap/sdk-core'
import { JSBI } from '@uniswap/v2-sdk'
import { FeeAmount } from '@uniswap/v3-sdk/dist/'
import { TokenAddressMap } from '../state/lists/hooks'
@@ -63,16 +63,13 @@ export function calculateGasMargin(value: BigNumber): BigNumber {
return value.mul(BigNumber.from(10000).add(BigNumber.from(1000))).div(BigNumber.from(10000))
}
const ONE = new Fraction(1, 1)
export function calculateSlippageAmount(value: CurrencyAmount, slippage: Percent): [JSBI, JSBI] {
if (
JSBI.lessThan(slippage.numerator, JSBI.BigInt(0)) ||
JSBI.greaterThan(slippage.numerator, JSBI.BigInt(10_000)) ||
!JSBI.equal(slippage.denominator, JSBI.BigInt(10_000))
)
throw new Error('Unexpected slippage')
if (slippage.lessThan(0) || slippage.greaterThan(ONE)) throw new Error('Unexpected slippage')
const decimalScaled = JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(value.currency.decimals))
return [
JSBI.divide(JSBI.multiply(value.raw, JSBI.subtract(JSBI.BigInt(10000), slippage.numerator)), JSBI.BigInt(10000)),
JSBI.divide(JSBI.multiply(value.raw, JSBI.add(JSBI.BigInt(10000), slippage.numerator)), JSBI.BigInt(10000)),
value.multiply(ONE.subtract(slippage)).multiply(decimalScaled).quotient,
value.multiply(ONE.add(slippage)).multiply(decimalScaled).quotient,
]
}

View File

@@ -13,46 +13,48 @@ const THIRTY_BIPS_FEE = new Percent(JSBI.BigInt(30), JSBI.BigInt(10000))
const ONE_HUNDRED_PERCENT = new Percent(JSBI.BigInt(10000), JSBI.BigInt(10000))
const INPUT_FRACTION_AFTER_FEE = ONE_HUNDRED_PERCENT.subtract(THIRTY_BIPS_FEE)
// computes price breakdown for the trade
export function computeRealizedLPFeeAmount(trade?: V2Trade | V3Trade | null): CurrencyAmount | undefined {
// computes realized lp fee as a percent
export function computeRealizedLPFeePercent(trade: V2Trade | V3Trade): Percent {
let percent: Percent
if (trade instanceof V2Trade) {
// for each hop in our trade, take away the x*y=k price impact from 0.3% fees
// e.g. for 3 tokens/2 hops: 1 - ((1 - .03) * (1-.03))
const realizedLPFee = !trade
? undefined
: ONE_HUNDRED_PERCENT.subtract(
trade.route.pairs.reduce<Fraction>(
(currentFee: Fraction): Fraction => currentFee.multiply(INPUT_FRACTION_AFTER_FEE),
ONE_HUNDRED_PERCENT
)
)
// the amount of the input that accrues to LPs
return (
realizedLPFee &&
trade &&
(trade.inputAmount instanceof TokenAmount
? new TokenAmount(trade.inputAmount.token, realizedLPFee.multiply(trade.inputAmount.raw).quotient)
: CurrencyAmount.ether(realizedLPFee.multiply(trade.inputAmount.raw).quotient))
percent = ONE_HUNDRED_PERCENT.subtract(
trade.route.pairs.reduce<Fraction>(
(currentFee: Fraction): Fraction => currentFee.multiply(INPUT_FRACTION_AFTER_FEE),
ONE_HUNDRED_PERCENT
)
)
} else if (trade instanceof V3Trade) {
const realizedLPFee = !trade
? undefined
: ONE_HUNDRED_PERCENT.subtract(
trade.route.pools.reduce<Fraction>(
(currentFee: Fraction, pool): Fraction =>
currentFee.multiply(ONE_HUNDRED_PERCENT.subtract(new Fraction(pool.fee, 1_000_000))),
ONE_HUNDRED_PERCENT
)
)
return (
realizedLPFee &&
trade &&
(trade.inputAmount instanceof TokenAmount
? new TokenAmount(trade.inputAmount.token, realizedLPFee.multiply(trade.inputAmount.raw).quotient)
: CurrencyAmount.ether(realizedLPFee.multiply(trade.inputAmount.raw).quotient))
} else {
percent = ONE_HUNDRED_PERCENT.subtract(
trade.route.pools.reduce<Fraction>(
(currentFee: Fraction, pool): Fraction =>
currentFee.multiply(ONE_HUNDRED_PERCENT.subtract(new Fraction(pool.fee, 1_000_000))),
ONE_HUNDRED_PERCENT
)
)
}
return new Percent(percent.numerator, percent.denominator)
}
// computes price breakdown for the trade
export function computeRealizedLPFeeAmount(trade?: V2Trade | V3Trade | null): CurrencyAmount | undefined {
if (trade instanceof V2Trade) {
const realizedLPFee = computeRealizedLPFeePercent(trade)
// the amount of the input that accrues to LPs
return trade.inputAmount instanceof TokenAmount
? new TokenAmount(trade.inputAmount.token, realizedLPFee.multiply(trade.inputAmount.raw).quotient)
: CurrencyAmount.ether(realizedLPFee.multiply(trade.inputAmount.raw).quotient)
} else if (trade instanceof V3Trade) {
const realizedLPFee = computeRealizedLPFeePercent(trade)
return trade.inputAmount instanceof TokenAmount
? new TokenAmount(trade.inputAmount.token, realizedLPFee.multiply(trade.inputAmount.raw).quotient)
: CurrencyAmount.ether(realizedLPFee.multiply(trade.inputAmount.raw).quotient)
}
return undefined
}

View File

@@ -3410,10 +3410,10 @@
lz-string "^1.4.4"
pretty-format "^26.6.2"
"@typechain/ethers-v5@^6.0.5":
version "6.0.5"
resolved "https://registry.yarnpkg.com/@typechain/ethers-v5/-/ethers-v5-6.0.5.tgz#39bbf9baadd0e8d9efad9d16c60152b7cd9a467b"
integrity sha512-KJh+EWuxmX1a17fQWS1ba8DCYcqK7UpdbqMZZwyfiv9FQfn8ZQJX17anbkCMOSU8TV3EvRuJ/vFEKGzKnpkO8g==
"@typechain/ethers-v5@^7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/@typechain/ethers-v5/-/ethers-v5-7.0.0.tgz#cadb5262b3827d1616c21f4ba86a36a71269bd7e"
integrity sha512-ykNaqYcQ1yC928x8bogL9LECUg0osfqqHCKBhP7qbGlNfvC/bvTiIfnjQUgXUYWEJRx5r0Y78vcKMo8F3sJTBA==
"@types/anymatch@*":
version "1.3.1"
@@ -3638,13 +3638,6 @@
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.4.tgz#f0ec25dbf2f0e4b18647313ac031134ca5b24b21"
integrity sha512-1z8k4wzFnNjVK/tlxvrWuK5WMt6mydWWP7+zvH5eFep4oj+UkrfiJTRtjCeBXNpwaA/FYqqtb4/QS4ianFpIRA==
"@types/mkdirp@^0.5.2":
version "0.5.2"
resolved "https://registry.yarnpkg.com/@types/mkdirp/-/mkdirp-0.5.2.tgz#503aacfe5cc2703d5484326b1b27efa67a339c1f"
integrity sha512-U5icWpv7YnZYGsN4/cmh3WD2onMY0aJIiTE6+51TwJCttdHvtCYmkBNOobHlXwrJRL0nkH9jH4kD+1FAdMN4Tg==
dependencies:
"@types/node" "*"
"@types/multicodec@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@types/multicodec/-/multicodec-1.0.0.tgz#9c9c2df84ea5006c65a048873600f71c4565a397"
@@ -3813,7 +3806,7 @@
"@types/styled-system" "*"
"@types/styled-system__css" "*"
"@types/resolve@0.0.8", "@types/resolve@^0.0.8":
"@types/resolve@0.0.8":
version "0.0.8"
resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-0.0.8.tgz#f26074d238e02659e323ce1a13d041eee280e194"
integrity sha512-auApPaJf3NPfe18hSoJkp8EbZzer2ISk7o8mCC3M9he/a04+gbMF97NkpD2S8riMGvm4BMRI59/SZQSaLTKpsQ==
@@ -3888,6 +3881,11 @@
"@testing-library/dom" "^7.11.0"
cypress "*"
"@types/ua-parser-js@^0.7.35":
version "0.7.35"
resolved "https://registry.yarnpkg.com/@types/ua-parser-js/-/ua-parser-js-0.7.35.tgz#cca67a95deb9165e4b1f449471801e6489d3fe93"
integrity sha512-PsPx0RLbo2Un8+ff2buzYJnZjzwhD3jQHPOG2PtVIeOhkRDddMcKU8vJtHpzzfLB95dkUi0qAkfLg2l2Fd0yrQ==
"@types/uglify-js@*":
version "3.13.0"
resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.13.0.tgz#1cad8df1fb0b143c5aba08de5712ea9d1ff71124"
@@ -4149,10 +4147,10 @@
"@uniswap/v3-core" "1.0.0"
base64-sol "1.0.1"
"@uniswap/v3-sdk@^1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@uniswap/v3-sdk/-/v3-sdk-1.0.3.tgz#8f47b5f8cc8997992811a242ef202f9a8c4797cf"
integrity sha512-izIrHTAXCeMhfye0nHntoAS0UTbpa8HyGSD++Zmy+kROeb2gSAcpXvnLHzRDIPxq0G4rOH0h05Y5fhHAxaXj5w==
"@uniswap/v3-sdk@^1.0.8":
version "1.0.8"
resolved "https://registry.yarnpkg.com/@uniswap/v3-sdk/-/v3-sdk-1.0.8.tgz#c85b229ac9448d19dfb1cb4a7891478c003a72f4"
integrity sha512-Kg0P4KZI07m6B6L5EEtkEX/Q6QOdbWnVAHiy8y1XJOxORGw4DS0vklG5IRJAVhgLsVlosbmjxELsBjVMuKtypQ==
dependencies:
"@ethersproject/abi" "^5.0.12"
"@ethersproject/solidity" "^5.0.9"
@@ -18094,11 +18092,6 @@ ts-dedent@^2.0.0:
resolved "https://registry.yarnpkg.com/ts-dedent/-/ts-dedent-2.1.1.tgz#6dd56870bb5493895171334fa5d7e929107e5bbc"
integrity sha512-riHuwnzAUCfdIeTBNUq7+Yj+ANnrMXo/7+Z74dIdudS7ys2k8aSGMzpJRMFDF7CLwUTbtvi1ZZff/Wl+XxmqIA==
ts-essentials@^1.0.0:
version "1.0.4"
resolved "https://registry.yarnpkg.com/ts-essentials/-/ts-essentials-1.0.4.tgz#ce3b5dade5f5d97cf69889c11bf7d2da8555b15a"
integrity sha512-q3N1xS4vZpRouhYHDPwO0bDW3EZ6SK9CrrDHxi/D6BPReSjpVgWIOpLS2o0gSBZm+7q/wyKp6RVM1AeeW7uyfQ==
ts-essentials@^2.0.3:
version "2.0.12"
resolved "https://registry.yarnpkg.com/ts-essentials/-/ts-essentials-2.0.12.tgz#c9303f3d74f75fa7528c3d49b80e089ab09d8745"
@@ -18109,21 +18102,6 @@ ts-essentials@^7.0.1:
resolved "https://registry.yarnpkg.com/ts-essentials/-/ts-essentials-7.0.1.tgz#d205508cae0cdadfb73c89503140cf2228389e2d"
integrity sha512-8lwh3QJtIc1UWhkQtr9XuksXu3O0YQdEE5g79guDfhCaU1FWTDIEDZ1ZSx4HTHUmlJZ8L812j3BZQ4a0aOUkSA==
ts-generator@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/ts-generator/-/ts-generator-0.1.1.tgz#af46f2fb88a6db1f9785977e9590e7bcd79220ab"
integrity sha512-N+ahhZxTLYu1HNTQetwWcx3so8hcYbkKBHTr4b4/YgObFTIKkOSSsaa+nal12w8mfrJAyzJfETXawbNjSfP2gQ==
dependencies:
"@types/mkdirp" "^0.5.2"
"@types/prettier" "^2.1.1"
"@types/resolve" "^0.0.8"
chalk "^2.4.1"
glob "^7.1.2"
mkdirp "^0.5.1"
prettier "^2.1.2"
resolve "^1.8.1"
ts-essentials "^1.0.0"
ts-pnp@1.2.0, ts-pnp@^1.1.6:
version "1.2.0"
resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.2.0.tgz#a500ad084b0798f1c3071af391e65912c86bca92"
@@ -18250,18 +18228,20 @@ type@^2.0.0:
resolved "https://registry.yarnpkg.com/type/-/type-2.5.0.tgz#0a2e78c2e77907b252abe5f298c1b01c63f0db3d"
integrity sha512-180WMDQaIMm3+7hGXWf12GtdniDEy7nYcyFMKJn/eZz/6tSLXrUN9V0wKSbMjej0I1WHWbpREDEKHtqPQa9NNw==
typechain@^4.0.3:
version "4.0.3"
resolved "https://registry.yarnpkg.com/typechain/-/typechain-4.0.3.tgz#e8fcd6c984676858c64eeeb155ea783a10b73779"
integrity sha512-tmoHQeXZWHxIdeLK+i6dU0CU0vOd9Cndr3jFTZIMzak5/YpFZ8XoiYpTZcngygGBqZo+Z1EUmttLbW9KkFZLgQ==
typechain@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/typechain/-/typechain-5.0.0.tgz#730e5fb4709964eed3db03be332b38ba6eaa1d9f"
integrity sha512-Ko2/8co0FUmPUkaXPcb8PC3ncWa5P72nvkiNMgcomd4OAInltJlITF0kcW2cZmI2sFkvmaHV5TZmCnOHgo+i5Q==
dependencies:
"@types/prettier" "^2.1.1"
command-line-args "^4.0.7"
debug "^4.1.1"
fs-extra "^7.0.0"
glob "^7.1.6"
js-sha3 "^0.8.0"
lodash "^4.17.15"
prettier "^2.1.2"
ts-essentials "^7.0.1"
ts-generator "^0.1.1"
typedarray-to-buffer@3.1.5, typedarray-to-buffer@^3.1.5:
version "3.1.5"
@@ -18285,7 +18265,7 @@ typical@^2.6.0, typical@^2.6.1:
resolved "https://registry.yarnpkg.com/typical/-/typical-2.6.1.tgz#5c080e5d661cbbe38259d2e70a3c7253e873881d"
integrity sha1-XAgOXWYcu+OCWdLnCjxyU+hziB0=
ua-parser-js@^0.7.24:
ua-parser-js@^0.7.24, ua-parser-js@^0.7.28:
version "0.7.28"
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.28.tgz#8ba04e653f35ce210239c64661685bf9121dec31"
integrity sha512-6Gurc1n//gjp9eQNXjD9O3M/sMwVtN5S8Lv9bvOYBfKfDNiIIhqiyi01vMBO45u4zkDE420w/e0se7Vs+sIg+g==