Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b2d86ae3a | ||
|
|
40cac44e07 | ||
|
|
4e6d28cff4 | ||
|
|
709fad0804 | ||
|
|
573f4c873a | ||
|
|
d300db669f | ||
|
|
fb8217ddea | ||
|
|
7b9a23d920 | ||
|
|
120ad935fa |
53
.github/pull_request_template.md
vendored
53
.github/pull_request_template.md
vendored
@@ -1,22 +1,45 @@
|
||||
<!-- Your PR title must follow conventional commits: https://github.com/Uniswap/interface#pr-title -->
|
||||
|
||||
## Description
|
||||
|
||||
_[Summary of change, motivation, and context.]_
|
||||
|
||||
- _Link to JIRA ticket, slack thread, or relevant docs helpful for providing context to reviewers._
|
||||
|
||||
- _Note: Your PR title must follow conventions [outlined here](https://github.com/Uniswap/interface#contributions)._
|
||||
|
||||
## Screen Capture
|
||||
| Before | After |
|
||||
| ---------------- |-----------------|
|
||||
| _insert_before_ | _insert_after_ |
|
||||
<!-- Summary of change, including motivation and context. -->
|
||||
<!-- Use verb-driven language: "Fixes XYZ" instead of "This change fixes XYZ" -->
|
||||
|
||||
|
||||
## Test Plan
|
||||
#### Manual
|
||||
<!-- Delete inapplicable lines: -->
|
||||
_JIRA ticket:_
|
||||
_Slack thread:_
|
||||
_Relevant docs:_
|
||||
|
||||
_[Steps of how you are testing the change and ensuring no regression.]_
|
||||
|
||||
#### Automated
|
||||
<!-- Delete this section if your change does not affect UI. -->
|
||||
## Screen capture
|
||||
|
||||
| Before | After (Desktop) | After (Mobile) |
|
||||
| ----------------------- |----------------------- | ---------------------- |
|
||||
| <!-- Paste "before" --> | <!-- Paste "after" --> | <!-- Paste "after" --> |
|
||||
|
||||
|
||||
## Test plan
|
||||
|
||||
<!-- Delete this section if your change is not a bug fix. -->
|
||||
### Reproducing the error
|
||||
|
||||
<!-- Include steps to reproduce the bug. -->
|
||||
1.
|
||||
|
||||
### QA (ie manual testing)
|
||||
|
||||
<!-- Include steps to test the change, ensuring no regression. -->
|
||||
- [ ] N/A
|
||||
|
||||
|
||||
#### Devices
|
||||
<!-- If applicable, include different devices and screen sizes that may be affected, and how you've tested them. -->
|
||||
|
||||
|
||||
### Automated testing
|
||||
|
||||
<!-- If N/A, do not check nor delete, but strike through. -->
|
||||
<!-- eg - [ ] <s>Unit test</s> -->
|
||||
- [ ] Unit test
|
||||
- [ ] Integration/E2E test
|
||||
|
||||
@@ -37,8 +37,12 @@
|
||||
"src/lib/utils/**/*.ts*",
|
||||
"src/pages/**/*.ts*",
|
||||
"src/state/**/*.ts*",
|
||||
"src/tracing/**/*.ts*",
|
||||
"src/utils/**/*.ts*"
|
||||
],
|
||||
"coveragePathIgnorePatterns": [
|
||||
".snap"
|
||||
],
|
||||
"coverageThreshold": {
|
||||
"global": {
|
||||
"branches": 4,
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import * as Sentry from '@sentry/react'
|
||||
import { sendAnalyticsEvent } from '@uniswap/analytics'
|
||||
import { SwapEventName } from '@uniswap/analytics-events'
|
||||
import { ButtonLight, SmallButtonPrimary } from 'components/Button'
|
||||
import { ChevronUpIcon } from 'nft/components/icons'
|
||||
import { useIsMobile } from 'nft/hooks'
|
||||
import React, { PropsWithChildren, useState } from 'react'
|
||||
import { Copy } from 'react-feather'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import styled from 'styled-components/macro'
|
||||
import { isSentryEnabled } from 'utils/env'
|
||||
|
||||
@@ -220,18 +217,14 @@ const updateServiceWorkerInBackground = async () => {
|
||||
}
|
||||
|
||||
export default function ErrorBoundary({ children }: PropsWithChildren): JSX.Element {
|
||||
const { pathname } = useLocation()
|
||||
return (
|
||||
<Sentry.ErrorBoundary
|
||||
fallback={({ error, eventId }) => <Fallback error={error} eventId={eventId} />}
|
||||
beforeCapture={(scope) => {
|
||||
scope.setLevel('fatal')
|
||||
}}
|
||||
onError={(error) => {
|
||||
onError={() => {
|
||||
updateServiceWorkerInBackground()
|
||||
if (pathname === '/swap') {
|
||||
sendAnalyticsEvent(SwapEventName.SWAP_ERROR, { error })
|
||||
}
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
24
src/components/Identicon/StatusIcon.test.tsx
Normal file
24
src/components/Identicon/StatusIcon.test.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { getConnections } from 'connection'
|
||||
import { render } from 'test-utils'
|
||||
|
||||
import StatusIcon from './StatusIcon'
|
||||
|
||||
jest.mock('../../hooks/useSocksBalance', () => ({
|
||||
useHasSocks: () => true,
|
||||
}))
|
||||
|
||||
describe('StatusIcon', () => {
|
||||
it('renders children in correct order, with no account and with socks', () => {
|
||||
const supportedConnections = getConnections()
|
||||
const injectedConnection = supportedConnections[1]
|
||||
const component = render(<StatusIcon connection={injectedConnection} />)
|
||||
expect(component.getByTestId('StatusIconRoot')).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders with no account and showMiniIcons=false', () => {
|
||||
const supportedConnections = getConnections()
|
||||
const injectedConnection = supportedConnections[1]
|
||||
const component = render(<StatusIcon connection={injectedConnection} showMiniIcons={false} />)
|
||||
expect(component.getByTestId('StatusIconRoot').children.length).toEqual(0)
|
||||
})
|
||||
})
|
||||
@@ -90,10 +90,10 @@ export default function StatusIcon({
|
||||
const hasSocks = useHasSocks()
|
||||
|
||||
return (
|
||||
<IconWrapper size={size}>
|
||||
{hasSocks && showMiniIcons && <Socks />}
|
||||
<IconWrapper size={size} data-testid="StatusIconRoot">
|
||||
<MainWalletIcon connection={connection} size={size} />
|
||||
{showMiniIcons && <MiniWalletIcon connection={connection} side="right" />}
|
||||
{hasSocks && showMiniIcons && <Socks />}
|
||||
</IconWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
129
src/components/Identicon/__snapshots__/StatusIcon.test.tsx.snap
Normal file
129
src/components/Identicon/__snapshots__/StatusIcon.test.tsx.snap
Normal file
@@ -0,0 +1,129 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`StatusIcon renders children in correct order, with no account and with socks 1`] = `
|
||||
.c0 {
|
||||
position: relative;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-flex-flow: column nowrap;
|
||||
-ms-flex-flow: column nowrap;
|
||||
flex-flow: column nowrap;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
-webkit-box-pack: center;
|
||||
-webkit-justify-content: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.c0 > img,
|
||||
.c0 span {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
position: absolute;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-pack: center;
|
||||
-webkit-justify-content: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
bottom: -4px;
|
||||
right: -4px;
|
||||
border-radius: 50%;
|
||||
outline: 2px solid #FFFFFF;
|
||||
outline-offset: -0.1px;
|
||||
background-color: #FFFFFF;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.c3 {
|
||||
position: absolute;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-pack: center;
|
||||
-webkit-justify-content: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
bottom: -4px;
|
||||
left: -4px;
|
||||
border-radius: 50%;
|
||||
outline: 2px solid #FFFFFF;
|
||||
outline-offset: -0.1px;
|
||||
background-color: #FFFFFF;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.c2 {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
@media (max-width:960px) {
|
||||
.c0 {
|
||||
-webkit-align-items: flex-end;
|
||||
-webkit-box-align: flex-end;
|
||||
-ms-flex-align: flex-end;
|
||||
align-items: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
@supports (overflow:clip) {
|
||||
.c1 {
|
||||
overflow: clip;
|
||||
}
|
||||
}
|
||||
|
||||
@supports (overflow:clip) {
|
||||
.c3 {
|
||||
overflow: clip;
|
||||
}
|
||||
}
|
||||
|
||||
<div
|
||||
class="c0"
|
||||
data-testid="StatusIconRoot"
|
||||
size="16"
|
||||
>
|
||||
<div
|
||||
class="c1"
|
||||
>
|
||||
<img
|
||||
alt="MetaMask icon"
|
||||
class="c2"
|
||||
src="metamask.svg"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="c3"
|
||||
>
|
||||
<img
|
||||
class="c2"
|
||||
src="socks.svg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,38 +1,91 @@
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
import { render, screen } from 'test-utils'
|
||||
import { SupportedChainId, Token, WETH9 } from '@uniswap/sdk-core'
|
||||
import { FeeAmount, Pool } from '@uniswap/v3-sdk'
|
||||
import { USDC_MAINNET } from 'constants/tokens'
|
||||
import { useToken } from 'hooks/Tokens'
|
||||
import { usePool } from 'hooks/usePools'
|
||||
import { PoolState } from 'hooks/usePools'
|
||||
import { render } from 'test-utils'
|
||||
import { unwrappedToken } from 'utils/unwrappedToken'
|
||||
|
||||
import PositionListItem from '.'
|
||||
|
||||
jest.mock('hooks/Tokens', () => {
|
||||
const originalModule = jest.requireActual('hooks/Tokens')
|
||||
const uniSDK = jest.requireActual('@uniswap/sdk-core')
|
||||
jest.mock('utils/unwrappedToken')
|
||||
const mockUnwrappedToken = unwrappedToken as jest.MockedFunction<typeof unwrappedToken>
|
||||
|
||||
jest.mock('hooks/usePools')
|
||||
const mockUsePool = usePool as jest.MockedFunction<typeof usePool>
|
||||
|
||||
jest.mock('hooks/Tokens')
|
||||
const mockUseToken = useToken as jest.MockedFunction<typeof useToken>
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
jest.mock('components/DoubleLogo', () => () => <div />)
|
||||
|
||||
jest.mock('@web3-react/core', () => {
|
||||
const web3React = jest.requireActual('@web3-react/core')
|
||||
return {
|
||||
__esModule: true,
|
||||
...originalModule,
|
||||
useToken: jest.fn(
|
||||
() =>
|
||||
new uniSDK.Token(
|
||||
1,
|
||||
'0x39AA39c021dfbaE8faC545936693aC917d5E7563',
|
||||
8,
|
||||
'https://www.example.com',
|
||||
'example.com coin'
|
||||
)
|
||||
),
|
||||
...web3React,
|
||||
useWeb3React: () => ({
|
||||
chainId: 1,
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
test('PositionListItem should not render when the name contains a url', () => {
|
||||
const susToken0Address = '0x39AA39c021dfbaE8faC545936693aC917d5E7563'
|
||||
|
||||
beforeEach(() => {
|
||||
const susToken0 = new Token(1, susToken0Address, 8, 'https://www.example.com', 'example.com coin')
|
||||
mockUseToken.mockImplementation((tokenAddress?: string | null | undefined) => {
|
||||
if (!tokenAddress) return null
|
||||
if (tokenAddress === susToken0.address) return susToken0
|
||||
return new Token(1, tokenAddress, 8, 'symbol', 'name')
|
||||
})
|
||||
mockUsePool.mockReturnValue([
|
||||
PoolState.EXISTS,
|
||||
new Pool(susToken0, USDC_MAINNET, FeeAmount.HIGH, '2437312313659959819381354528', '10272714736694327408', -69633),
|
||||
])
|
||||
mockUnwrappedToken.mockReturnValue(susToken0)
|
||||
})
|
||||
|
||||
test('PositionListItem should not render when token0 symbol contains a url', () => {
|
||||
const positionDetails = {
|
||||
token0: '0x39AA39c021dfbaE8faC545936693aC917d5E7563',
|
||||
token1: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
|
||||
token0: susToken0Address,
|
||||
token1: USDC_MAINNET.address,
|
||||
tokenId: BigNumber.from(436148),
|
||||
fee: 100,
|
||||
liquidity: BigNumber.from('0x5c985aff8059be04'),
|
||||
tickLower: -800,
|
||||
tickUpper: 1600,
|
||||
}
|
||||
render(<PositionListItem {...positionDetails} />)
|
||||
screen.debug()
|
||||
expect(screen.queryByText('.com', { exact: false })).toBe(null)
|
||||
const { container } = render(<PositionListItem {...positionDetails} />)
|
||||
expect(container).toBeEmptyDOMElement()
|
||||
})
|
||||
|
||||
test('PositionListItem should not render when token1 symbol contains a url', () => {
|
||||
const positionDetails = {
|
||||
token0: USDC_MAINNET.address,
|
||||
token1: susToken0Address,
|
||||
tokenId: BigNumber.from(436148),
|
||||
fee: 100,
|
||||
liquidity: BigNumber.from('0x5c985aff8059be04'),
|
||||
tickLower: -800,
|
||||
tickUpper: 1600,
|
||||
}
|
||||
const { container } = render(<PositionListItem {...positionDetails} />)
|
||||
expect(container).toBeEmptyDOMElement()
|
||||
})
|
||||
|
||||
test('PositionListItem should render a position', () => {
|
||||
const positionDetails = {
|
||||
token0: USDC_MAINNET.address,
|
||||
token1: WETH9[SupportedChainId.MAINNET].address,
|
||||
tokenId: BigNumber.from(436148),
|
||||
fee: 100,
|
||||
liquidity: BigNumber.from('0x5c985aff8059be04'),
|
||||
tickLower: -800,
|
||||
tickUpper: 1600,
|
||||
}
|
||||
const { container } = render(<PositionListItem {...positionDetails} />)
|
||||
expect(container).not.toBeEmptyDOMElement()
|
||||
})
|
||||
|
||||
@@ -203,12 +203,9 @@ export default function PositionListItem({
|
||||
|
||||
const removed = liquidity?.eq(0)
|
||||
|
||||
const containsURL = useMemo(
|
||||
() => [token0?.name, token0?.symbol, token1?.name, token1?.symbol].some((testString) => hasURL(testString)),
|
||||
[token0?.name, token0?.symbol, token1?.name, token1?.symbol]
|
||||
)
|
||||
const shouldHidePosition = hasURL(token0?.symbol) || hasURL(token1?.symbol)
|
||||
|
||||
if (containsURL) {
|
||||
if (shouldHidePosition) {
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ import MiniPortfolio from './MiniPortfolio'
|
||||
import { portfolioFadeInAnimation } from './MiniPortfolio/PortfolioRow'
|
||||
|
||||
const AuthenticatedHeaderWrapper = styled.div`
|
||||
padding: 20px 16px;
|
||||
padding: 14px 12px 16px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
import { SupportedChainId, WETH9 } from '@uniswap/sdk-core'
|
||||
import { FeeAmount, Pool, Position } from '@uniswap/v3-sdk'
|
||||
import { USDC_MAINNET } from 'constants/tokens'
|
||||
import { render } from 'test-utils'
|
||||
|
||||
import Pools from '.'
|
||||
import useMultiChainPositions from './useMultiChainPositions'
|
||||
|
||||
jest.mock('./useMultiChainPositions')
|
||||
const mockUseMultiChainPositions = useMultiChainPositions as jest.MockedFunction<typeof useMultiChainPositions>
|
||||
|
||||
const owner = '0xf5b6bb25f5beaea03dd014c6ef9fa9f3926bf36c'
|
||||
|
||||
const pool = new Pool(
|
||||
USDC_MAINNET,
|
||||
WETH9[SupportedChainId.MAINNET],
|
||||
FeeAmount.MEDIUM,
|
||||
'1851127709498178402383049949138810',
|
||||
'7076437181775065414',
|
||||
201189
|
||||
)
|
||||
|
||||
const position = new Position({
|
||||
pool,
|
||||
liquidity: 1341008833950736,
|
||||
tickLower: 200040,
|
||||
tickUpper: 202560,
|
||||
})
|
||||
const details = {
|
||||
nonce: BigNumber.from('0'),
|
||||
tokenId: BigNumber.from('0'),
|
||||
operator: '0x0',
|
||||
token0: USDC_MAINNET.address,
|
||||
token1: WETH9[SupportedChainId.MAINNET].address,
|
||||
fee: FeeAmount.MEDIUM,
|
||||
tickLower: -100,
|
||||
tickUpper: 100,
|
||||
liquidity: BigNumber.from('9000'),
|
||||
feeGrowthInside0LastX128: BigNumber.from('0'),
|
||||
feeGrowthInside1LastX128: BigNumber.from('0'),
|
||||
tokensOwed0: BigNumber.from('0'),
|
||||
tokensOwed1: BigNumber.from('0'),
|
||||
}
|
||||
const useMultiChainPositionsReturnValue = {
|
||||
positions: [
|
||||
{
|
||||
owner,
|
||||
chainId: SupportedChainId.MAINNET,
|
||||
position,
|
||||
pool,
|
||||
details,
|
||||
inRange: true,
|
||||
closed: false,
|
||||
},
|
||||
],
|
||||
loading: false,
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mockUseMultiChainPositions.mockReturnValue(useMultiChainPositionsReturnValue)
|
||||
})
|
||||
test('Pools should render LP positions', () => {
|
||||
const props = { account: owner }
|
||||
const { container } = render(<Pools {...props} />)
|
||||
expect(container).not.toBeEmptyDOMElement()
|
||||
})
|
||||
@@ -111,15 +111,9 @@ function PositionListItem({ positionInfo }: { positionInfo: PositionInfo }) {
|
||||
[chainId, pool.token0.address, pool.token0.symbol, pool.token1.address, pool.token1.symbol]
|
||||
)
|
||||
|
||||
const containsURL = useMemo(
|
||||
() =>
|
||||
[pool.token0.name, pool.token0.symbol, pool.token1.name, pool.token1.symbol].some((testString) =>
|
||||
hasURL(testString)
|
||||
),
|
||||
[pool]
|
||||
)
|
||||
const shouldHidePosition = hasURL(pool.token0.symbol) || hasURL(pool.token1.symbol)
|
||||
|
||||
if (containsURL) {
|
||||
if (shouldHidePosition) {
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
@@ -76,22 +76,16 @@ const WalletDropdownScrollWrapper = styled.div`
|
||||
border-radius: 12px;
|
||||
`
|
||||
|
||||
const Wrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: calc(100% - 2 * ${DRAWER_MARGIN});
|
||||
overflow: hidden;
|
||||
position: fixed;
|
||||
right: ${DRAWER_MARGIN};
|
||||
top: ${DRAWER_MARGIN};
|
||||
z-index: ${Z_INDEX.fixed};
|
||||
`
|
||||
|
||||
const WalletDropdownWrapper = styled.div<{ open: boolean }>`
|
||||
margin-right: ${({ open }) => (open ? 0 : '-' + DRAWER_WIDTH)};
|
||||
height: 100%;
|
||||
position: fixed;
|
||||
top: ${DRAWER_MARGIN};
|
||||
right: ${({ open }) => (open ? DRAWER_MARGIN : '-' + DRAWER_WIDTH)};
|
||||
z-index: ${Z_INDEX.fixed};
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
height: calc(100% - 2 * ${DRAWER_MARGIN});
|
||||
|
||||
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) {
|
||||
z-index: ${Z_INDEX.modal};
|
||||
top: unset;
|
||||
@@ -107,7 +101,7 @@ const WalletDropdownWrapper = styled.div<{ open: boolean }>`
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1440px) {
|
||||
margin-right: ${({ open }) => (open ? 0 : '-' + DRAWER_WIDTH_XL)};
|
||||
right: ${({ open }) => (open ? DRAWER_MARGIN : '-' + DRAWER_WIDTH_XL)};
|
||||
width: ${DRAWER_WIDTH_XL};
|
||||
}
|
||||
|
||||
@@ -118,7 +112,7 @@ const WalletDropdownWrapper = styled.div<{ open: boolean }>`
|
||||
border: ${({ theme }) => `1px solid ${theme.backgroundOutline}`};
|
||||
|
||||
box-shadow: ${({ theme }) => theme.deepShadow};
|
||||
transition: margin-right ${({ theme }) => theme.transition.duration.medium},
|
||||
transition: right ${({ theme }) => theme.transition.duration.medium},
|
||||
bottom ${({ theme }) => theme.transition.duration.medium};
|
||||
`
|
||||
|
||||
@@ -129,7 +123,11 @@ const CloseIcon = styled(ChevronsRight).attrs({ size: 24 })`
|
||||
const CloseDrawer = styled.div`
|
||||
${ClickableStyle}
|
||||
cursor: pointer;
|
||||
height: 100%;
|
||||
height: calc(100% - 2 * ${DRAWER_MARGIN});
|
||||
position: fixed;
|
||||
right: calc(${DRAWER_MARGIN} + ${DRAWER_WIDTH} - ${DRAWER_OFFSET});
|
||||
top: 4px;
|
||||
z-index: ${Z_INDEX.dropdown};
|
||||
// When the drawer is not hovered, the icon should be 18px from the edge of the sidebar.
|
||||
padding: 24px calc(18px + ${DRAWER_OFFSET}) 24px 14px;
|
||||
border-radius: 20px 0 0 20px;
|
||||
@@ -142,6 +140,9 @@ const CloseDrawer = styled.div`
|
||||
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) {
|
||||
display: none;
|
||||
}
|
||||
@media screen and (min-width: 1440px) {
|
||||
right: calc(${DRAWER_MARGIN} + ${DRAWER_WIDTH_XL} - ${DRAWER_OFFSET});
|
||||
}
|
||||
`
|
||||
|
||||
function WalletDropdown() {
|
||||
@@ -186,7 +187,7 @@ function WalletDropdown() {
|
||||
}, [walletDrawerOpen, toggleWalletDrawer])
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<>
|
||||
{walletDrawerOpen && (
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
@@ -205,7 +206,7 @@ function WalletDropdown() {
|
||||
<DefaultMenu />
|
||||
</WalletDropdownScrollWrapper>
|
||||
</WalletDropdownWrapper>
|
||||
</Wrapper>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
21
src/state/user/hooks.test.tsx
Normal file
21
src/state/user/hooks.test.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { USDC_MAINNET } from 'constants/tokens'
|
||||
|
||||
import { deserializeToken, serializeToken } from './hooks'
|
||||
|
||||
describe('serializeToken', () => {
|
||||
it('serializes the token', () => {
|
||||
expect(serializeToken(USDC_MAINNET)).toEqual({
|
||||
chainId: 1,
|
||||
decimals: 6,
|
||||
name: 'USD//C',
|
||||
symbol: 'USDC',
|
||||
address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('deserializeToken', () => {
|
||||
it('deserializes the token', () => {
|
||||
expect(deserializeToken(serializeToken(USDC_MAINNET))).toEqual(USDC_MAINNET)
|
||||
})
|
||||
})
|
||||
@@ -2,7 +2,33 @@ import { createStore, Store } from 'redux'
|
||||
|
||||
import { DEFAULT_DEADLINE_FROM_NOW } from '../../constants/misc'
|
||||
import { updateVersion } from '../global/actions'
|
||||
import reducer, { initialState, UserState } from './reducer'
|
||||
import reducer, {
|
||||
addSerializedPair,
|
||||
addSerializedToken,
|
||||
initialState,
|
||||
updateHideClosedPositions,
|
||||
updateHideUniswapWalletBanner,
|
||||
updateSelectedWallet,
|
||||
updateUserClientSideRouter,
|
||||
updateUserDeadline,
|
||||
updateUserExpertMode,
|
||||
updateUserLocale,
|
||||
updateUserSlippageTolerance,
|
||||
UserState,
|
||||
} from './reducer'
|
||||
|
||||
function buildSerializedPair(token0Address: string, token1Address: string, chainId: number) {
|
||||
return {
|
||||
token0: {
|
||||
chainId,
|
||||
address: token0Address,
|
||||
},
|
||||
token1: {
|
||||
chainId,
|
||||
address: token1Address,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
describe('swap reducer', () => {
|
||||
let store: Store<UserState>
|
||||
@@ -30,5 +56,176 @@ describe('swap reducer', () => {
|
||||
expect(store.getState().userDeadline).toEqual(DEFAULT_DEADLINE_FROM_NOW)
|
||||
expect(store.getState().userSlippageTolerance).toEqual('auto')
|
||||
})
|
||||
it('sets allowed slippage and deadline to auto', () => {
|
||||
store = createStore(reducer, {
|
||||
...initialState,
|
||||
userSlippageTolerance: 10,
|
||||
userSlippageToleranceHasBeenMigratedToAuto: undefined,
|
||||
} as any)
|
||||
store.dispatch(updateVersion())
|
||||
expect(store.getState().userSlippageToleranceHasBeenMigratedToAuto).toEqual(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateSelectedWallet', () => {
|
||||
it('updates the selected wallet', () => {
|
||||
store.dispatch(updateSelectedWallet({ wallet: 'metamask' }))
|
||||
expect(store.getState().selectedWallet).toEqual('metamask')
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateUserExpertMode', () => {
|
||||
it('updates the userExpertMode', () => {
|
||||
store.dispatch(updateUserExpertMode({ userExpertMode: true }))
|
||||
expect(store.getState().userExpertMode).toEqual(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateUserLocale', () => {
|
||||
it('updates the userLocale', () => {
|
||||
store.dispatch(updateUserLocale({ userLocale: 'en' }))
|
||||
expect(store.getState().userLocale).toEqual('en')
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateUserSlippageTolerance', () => {
|
||||
it('updates the userSlippageTolerance', () => {
|
||||
store.dispatch(updateUserSlippageTolerance({ userSlippageTolerance: '0.5' }))
|
||||
expect(store.getState().userSlippageTolerance).toEqual('0.5')
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateUserDeadline', () => {
|
||||
it('updates the userDeadline', () => {
|
||||
store.dispatch(updateUserDeadline({ userDeadline: 5 }))
|
||||
expect(store.getState().userDeadline).toEqual(5)
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateUserClientSideRouter', () => {
|
||||
it('updates the userClientSideRouter', () => {
|
||||
store.dispatch(updateUserClientSideRouter({ userClientSideRouter: true }))
|
||||
expect(store.getState().userClientSideRouter).toEqual(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateHideClosedPositions', () => {
|
||||
it('updates the userHideClosedPositions', () => {
|
||||
store.dispatch(updateHideClosedPositions({ userHideClosedPositions: true }))
|
||||
expect(store.getState().userHideClosedPositions).toEqual(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateHideUniswapWalletBanner', () => {
|
||||
it('updates the hideUniswapWalletBanner', () => {
|
||||
store.dispatch(updateHideUniswapWalletBanner({ hideUniswapWalletBanner: true }))
|
||||
expect(store.getState().hideUniswapWalletBanner).toEqual(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('addSerializedToken', () => {
|
||||
it('adds a token to the uninitialized list', () => {
|
||||
store = createStore(reducer, {
|
||||
...initialState,
|
||||
tokens: undefined as any,
|
||||
})
|
||||
store.dispatch(
|
||||
addSerializedToken({
|
||||
serializedToken: {
|
||||
chainId: 1,
|
||||
address: '0x123',
|
||||
},
|
||||
})
|
||||
)
|
||||
expect(store.getState().tokens).toEqual({ 1: { '0x123': { address: '0x123', chainId: 1 } } })
|
||||
})
|
||||
it('adds a token to the initialized list, no duplicates', () => {
|
||||
store.dispatch(addSerializedToken({ serializedToken: { chainId: 1, address: '0x123' } }))
|
||||
store.dispatch(addSerializedToken({ serializedToken: { chainId: 1, address: '0x123' } }))
|
||||
expect(store.getState().tokens).toEqual({ 1: { '0x123': { address: '0x123', chainId: 1 } } })
|
||||
})
|
||||
|
||||
it('adds a new token to the initialized list', () => {
|
||||
store.dispatch(addSerializedToken({ serializedToken: { chainId: 1, address: '0x123' } }))
|
||||
store.dispatch(addSerializedToken({ serializedToken: { chainId: 1, address: '0x456' } }))
|
||||
expect(store.getState().tokens).toEqual({
|
||||
1: {
|
||||
'0x123': { address: '0x123', chainId: 1 },
|
||||
'0x456': { address: '0x456', chainId: 1 },
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('addSerializedPair', () => {
|
||||
it('adds a pair to the uninitialized list', () => {
|
||||
store = createStore(reducer, {
|
||||
...initialState,
|
||||
})
|
||||
store.dispatch(
|
||||
addSerializedPair({
|
||||
serializedPair: buildSerializedPair('0x123', '0x456', 1),
|
||||
})
|
||||
)
|
||||
expect(store.getState().pairs).toEqual({
|
||||
1: { '0x123;0x456': buildSerializedPair('0x123', '0x456', 1) },
|
||||
})
|
||||
})
|
||||
|
||||
it('adds two pair to the initialized list, no duplicates', () => {
|
||||
store.dispatch(
|
||||
addSerializedPair({
|
||||
serializedPair: buildSerializedPair('0x123', '0x456', 1),
|
||||
})
|
||||
)
|
||||
store.dispatch(
|
||||
addSerializedPair({
|
||||
serializedPair: buildSerializedPair('0x123', '0x456', 1),
|
||||
})
|
||||
)
|
||||
expect(store.getState().pairs).toEqual({
|
||||
1: { '0x123;0x456': buildSerializedPair('0x123', '0x456', 1) },
|
||||
})
|
||||
})
|
||||
|
||||
it('adds two new pairs to the initialized list, same chain', () => {
|
||||
store.dispatch(
|
||||
addSerializedPair({
|
||||
serializedPair: buildSerializedPair('0x123', '0x456', 1),
|
||||
})
|
||||
)
|
||||
store.dispatch(
|
||||
addSerializedPair({
|
||||
serializedPair: buildSerializedPair('0x123', '0x789', 1),
|
||||
})
|
||||
)
|
||||
expect(store.getState().pairs).toEqual({
|
||||
1: {
|
||||
'0x123;0x456': buildSerializedPair('0x123', '0x456', 1),
|
||||
'0x123;0x789': buildSerializedPair('0x123', '0x789', 1),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('adds two new pairs to the initialized list, different chains', () => {
|
||||
store.dispatch(
|
||||
addSerializedPair({
|
||||
serializedPair: buildSerializedPair('0x123', '0x456', 1),
|
||||
})
|
||||
)
|
||||
store.dispatch(
|
||||
addSerializedPair({
|
||||
serializedPair: buildSerializedPair('0x123', '0x456', 5),
|
||||
})
|
||||
)
|
||||
expect(store.getState().pairs).toEqual({
|
||||
1: {
|
||||
'0x123;0x456': buildSerializedPair('0x123', '0x456', 1),
|
||||
},
|
||||
5: {
|
||||
'0x123;0x456': buildSerializedPair('0x123', '0x456', 5),
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
22
src/state/wallets/hooks.test.tsx
Normal file
22
src/state/wallets/hooks.test.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { act, renderHook } from 'test-utils'
|
||||
|
||||
import { useConnectedWallets } from './hooks'
|
||||
import { Wallet } from './types'
|
||||
|
||||
describe('useConnectedWallets', () => {
|
||||
it('should return the connected wallets', () => {
|
||||
const { result } = renderHook(() => useConnectedWallets())
|
||||
expect(result.current[0]).toEqual([])
|
||||
})
|
||||
it('should add a wallet', () => {
|
||||
const { result } = renderHook(() => useConnectedWallets())
|
||||
const wallet: Wallet = {
|
||||
walletType: 'injected',
|
||||
account: '0x123',
|
||||
}
|
||||
act(() => {
|
||||
result.current[1](wallet)
|
||||
})
|
||||
expect(result.current[0]).toEqual([wallet])
|
||||
})
|
||||
})
|
||||
40
src/state/wallets/reducer.test.ts
Normal file
40
src/state/wallets/reducer.test.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import walletsReducer from './reducer'
|
||||
import { Wallet } from './types'
|
||||
|
||||
describe('walletsSlice reducers', () => {
|
||||
it('should add a connected wallet', () => {
|
||||
const initialState = {
|
||||
connectedWallets: [],
|
||||
}
|
||||
const wallet = {
|
||||
address: '0x123',
|
||||
chainId: 1,
|
||||
}
|
||||
const action = {
|
||||
type: 'wallets/addConnectedWallet',
|
||||
payload: wallet,
|
||||
}
|
||||
const expectedState = {
|
||||
connectedWallets: [wallet],
|
||||
}
|
||||
expect(walletsReducer(initialState, action)).toEqual(expectedState)
|
||||
})
|
||||
|
||||
it('should remove a connected wallet', () => {
|
||||
const wallet: Wallet = {
|
||||
walletType: 'metamask',
|
||||
account: '0x123',
|
||||
}
|
||||
const initialState = {
|
||||
connectedWallets: [wallet],
|
||||
}
|
||||
const action = {
|
||||
type: 'wallets/removeConnectedWallet',
|
||||
payload: wallet,
|
||||
}
|
||||
const expectedState = {
|
||||
connectedWallets: [],
|
||||
}
|
||||
expect(walletsReducer(initialState, action)).toEqual(expectedState)
|
||||
})
|
||||
})
|
||||
22
src/tracing/errors.test.ts
Normal file
22
src/tracing/errors.test.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { ErrorEvent } from '@sentry/types'
|
||||
|
||||
import { filterKnownErrors } from './errors'
|
||||
|
||||
describe('filterKnownErrors', () => {
|
||||
const ERROR = {} as ErrorEvent
|
||||
it('propagates an error', () => {
|
||||
expect(filterKnownErrors(ERROR, {})).toBe(ERROR)
|
||||
})
|
||||
|
||||
it('filters block number polling errors', () => {
|
||||
const originalException = new (class extends Error {
|
||||
requestBody = JSON.stringify({ method: 'eth_blockNumber' })
|
||||
})()
|
||||
expect(filterKnownErrors(ERROR, { originalException })).toBe(null)
|
||||
})
|
||||
|
||||
it('filters network change errors', () => {
|
||||
const originalException = new Error('underlying network changed')
|
||||
expect(filterKnownErrors(ERROR, { originalException })).toBe(null)
|
||||
})
|
||||
})
|
||||
27
src/tracing/errors.ts
Normal file
27
src/tracing/errors.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { ClientOptions, ErrorEvent, EventHint } from '@sentry/types'
|
||||
|
||||
/** Identifies ethers request errors (as thrown by {@type import(@ethersproject/web).fetchJson}). */
|
||||
function isEthersRequestError(error: Error): error is Error & { requestBody: string } {
|
||||
return 'requestBody' in error && typeof (error as unknown as Record<'requestBody', unknown>).requestBody === 'string'
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters known (ignorable) errors out before sending them to Sentry.
|
||||
* Intended as a {@link ClientOptions.beforeSend} callback. Returning null filters the error from Sentry.
|
||||
*/
|
||||
export const filterKnownErrors: Required<ClientOptions>['beforeSend'] = (event: ErrorEvent, hint: EventHint) => {
|
||||
const error = hint.originalException
|
||||
if (error instanceof Error) {
|
||||
// ethers aggressively polls for block number, and it sometimes fails (whether spuriously or through rate-limiting).
|
||||
// If block number polling, it should not be considered an exception.
|
||||
if (isEthersRequestError(error)) {
|
||||
const method = JSON.parse(error.requestBody).method
|
||||
if (method === 'eth_blockNumber') return null
|
||||
}
|
||||
|
||||
// If the error is a network change, it should not be considered an exception.
|
||||
if (error.message.match(/underlying network changed/)) return null
|
||||
}
|
||||
|
||||
return event
|
||||
}
|
||||
@@ -7,6 +7,8 @@ import { SharedEventName } from '@uniswap/analytics-events'
|
||||
import { isSentryEnabled } from 'utils/env'
|
||||
import { getEnvName, isProductionEnv } from 'utils/env'
|
||||
|
||||
import { filterKnownErrors } from './errors'
|
||||
|
||||
export { trace } from './trace'
|
||||
|
||||
// Dump some metadata into the window to allow client verification.
|
||||
@@ -17,13 +19,10 @@ const AMPLITUDE_DUMMY_KEY = '00000000000000000000000000000000'
|
||||
export const STATSIG_DUMMY_KEY = 'client-0000000000000000000000000000000000000000000'
|
||||
|
||||
Sentry.init({
|
||||
// General configuration:
|
||||
dsn: process.env.REACT_APP_SENTRY_DSN,
|
||||
release: process.env.REACT_APP_GIT_COMMIT_HASH,
|
||||
environment: getEnvName(),
|
||||
// Exception reporting configuration:
|
||||
enabled: isSentryEnabled(),
|
||||
// Performance tracing configuration:
|
||||
tracesSampleRate: Number(process.env.REACT_APP_SENTRY_TRACES_SAMPLE_RATE ?? 0),
|
||||
integrations: [
|
||||
new BrowserTracing({
|
||||
@@ -31,6 +30,7 @@ Sentry.init({
|
||||
startTransactionOnPageLoad: true,
|
||||
}),
|
||||
],
|
||||
beforeSend: filterKnownErrors,
|
||||
})
|
||||
|
||||
initializeAnalytics(AMPLITUDE_DUMMY_KEY, OriginApplication.INTERFACE, {
|
||||
|
||||
@@ -122,6 +122,9 @@ describe('prices', () => {
|
||||
it('0 for undefined', () => {
|
||||
expect(warningSeverity(undefined)).toEqual(0)
|
||||
})
|
||||
it('0 for negative', () => {
|
||||
expect(warningSeverity(new Percent(-1))).toEqual(0)
|
||||
})
|
||||
it('correct for 0', () => {
|
||||
expect(warningSeverity(new Percent(0))).toEqual(0)
|
||||
})
|
||||
|
||||
@@ -85,6 +85,12 @@ const IMPACT_TIERS = [
|
||||
type WarningSeverity = 0 | 1 | 2 | 3 | 4
|
||||
export function warningSeverity(priceImpact: Percent | undefined): WarningSeverity {
|
||||
if (!priceImpact) return 0
|
||||
// This function is used to calculate the Severity level for % changes in USD value and Price Impact.
|
||||
// Price Impact is always an absolute value (conceptually always negative, but represented in code with a positive value)
|
||||
// The USD value change can be positive or negative, and it follows the same standard as Price Impact (positive value is the typical case of a loss due to slippage).
|
||||
// We don't want to return a warning level for a favorable/profitable change, so when the USD value change is negative we return 0.
|
||||
// TODO (WEB-3133): Disambiguate Price Impact and USD value change, and flip the sign of USD Value change.
|
||||
if (priceImpact.lessThan(0)) return 0
|
||||
let impact: WarningSeverity = IMPACT_TIERS.length as WarningSeverity
|
||||
for (const impactLevel of IMPACT_TIERS) {
|
||||
if (impactLevel.lessThan(priceImpact)) return impact
|
||||
|
||||
Reference in New Issue
Block a user