test(e2e): switching network (#6662)
* test(e2e): switching network Co-authored-by: Jordan Frankfurt <<jordan@CORN-Jordan-949.frankfurt> * fix: reconnect after failed chain switch * test(e2e): add forks to hardhat.config * test(e2e): wait on wallet_switchEthereumChain * build: upgrade cypress-hardhat * fix: do not disconnect whilst switching * fix: unit tests * fix: better chain selector check * test(e2e): better helper fn naming * test(e2e): better stub naming * fix: doc re-activation * fix: add back click * build: upgrade cypress-hardhat to include network caching * unwrap web3status --------- Co-authored-by: Jordan Frankfurt <<jordan@CORN-Jordan-949.frankfurt>
This commit is contained in:
parent
c0163767ed
commit
5caaaf1b1f
121
cypress/e2e/wallet-connection/switch-network.test.ts
Normal file
121
cypress/e2e/wallet-connection/switch-network.test.ts
Normal file
@ -0,0 +1,121 @@
|
||||
import { createDeferredPromise } from '../../../src/test-utils/promise'
|
||||
import { getTestSelector } from '../../utils'
|
||||
|
||||
function waitsForActiveChain(chain: string) {
|
||||
cy.get(getTestSelector('chain-selector-logo')).invoke('attr', 'alt').should('eq', chain)
|
||||
}
|
||||
|
||||
function switchChain(chain: string) {
|
||||
cy.get(getTestSelector('chain-selector')).eq(1).click()
|
||||
cy.contains(chain).click()
|
||||
}
|
||||
|
||||
describe('network switching', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/swap', { ethereum: 'hardhat' })
|
||||
cy.get(getTestSelector('web3-status-connected'))
|
||||
})
|
||||
|
||||
function rejectsNetworkSwitchWith(rejection: unknown) {
|
||||
cy.hardhat().then((hardhat) => {
|
||||
// Reject network switch
|
||||
const sendStub = cy.stub(hardhat.provider, 'send').log(false).as('switch')
|
||||
sendStub.withArgs('wallet_switchEthereumChain').rejects(rejection)
|
||||
sendStub.callThrough() // allows other calls to return non-stubbed values
|
||||
})
|
||||
|
||||
switchChain('Polygon')
|
||||
|
||||
// Verify rejected network switch
|
||||
cy.get('@switch').should('have.been.calledWith', 'wallet_switchEthereumChain')
|
||||
waitsForActiveChain('Ethereum')
|
||||
cy.get(getTestSelector('web3-status-connected'))
|
||||
}
|
||||
|
||||
it('should not display message on user rejection', () => {
|
||||
const USER_REJECTION = { code: 4001 }
|
||||
rejectsNetworkSwitchWith(USER_REJECTION)
|
||||
cy.get(getTestSelector('popups')).should('not.contain', 'Failed to switch networks')
|
||||
})
|
||||
|
||||
it('should display message on unknown error', () => {
|
||||
rejectsNetworkSwitchWith(new Error('Unknown error'))
|
||||
cy.get(getTestSelector('popups')).contains('Failed to switch networks')
|
||||
})
|
||||
|
||||
it('should add missing chain', () => {
|
||||
cy.hardhat().then((hardhat) => {
|
||||
// https://docs.metamask.io/guide/rpc-api.html#unrestricted-methods
|
||||
const CHAIN_NOT_ADDED = { code: 4902 } // missing message in useSelectChain
|
||||
|
||||
// Reject network switch with CHAIN_NOT_ADDED
|
||||
const sendStub = cy.stub(hardhat.provider, 'send').log(false).as('switch')
|
||||
let added = false
|
||||
sendStub
|
||||
.withArgs('wallet_switchEthereumChain')
|
||||
.callsFake(() => (added ? Promise.resolve(null) : Promise.reject(CHAIN_NOT_ADDED)))
|
||||
sendStub.withArgs('wallet_addEthereumChain').callsFake(() => {
|
||||
added = true
|
||||
return Promise.resolve(null)
|
||||
})
|
||||
sendStub.callThrough() // allows other calls to return non-stubbed values
|
||||
})
|
||||
|
||||
switchChain('Polygon')
|
||||
|
||||
// Verify the network was added
|
||||
cy.get('@switch').should('have.been.calledWith', 'wallet_switchEthereumChain')
|
||||
cy.get('@switch').should('have.been.calledWith', 'wallet_addEthereumChain', [
|
||||
{
|
||||
blockExplorerUrls: ['https://polygonscan.com/'],
|
||||
chainId: '0x89',
|
||||
chainName: 'Polygon',
|
||||
nativeCurrency: { name: 'Polygon Matic', symbol: 'MATIC', decimals: 18 },
|
||||
rpcUrls: ['https://polygon-rpc.com/'],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should not disconnect while switching', () => {
|
||||
const promise = createDeferredPromise()
|
||||
|
||||
cy.hardhat().then((hardhat) => {
|
||||
// Reject network switch with CHAIN_NOT_ADDED
|
||||
const sendStub = cy.stub(hardhat.provider, 'send').log(false).as('switch')
|
||||
sendStub.withArgs('wallet_switchEthereumChain').returns(promise)
|
||||
sendStub.callThrough() // allows other calls to return non-stubbed values
|
||||
})
|
||||
|
||||
switchChain('Polygon')
|
||||
|
||||
// Verify there is no disconnection
|
||||
cy.get('@switch').should('have.been.calledWith', 'wallet_switchEthereumChain')
|
||||
cy.contains('Connecting to Polygon')
|
||||
cy.get(getTestSelector('web3-status-connected')).should('be.disabled')
|
||||
promise.resolve()
|
||||
})
|
||||
|
||||
it('should switch networks', () => {
|
||||
// Select an output currency
|
||||
cy.get('#swap-currency-output .open-currency-select-button').click()
|
||||
cy.contains('USDC').click()
|
||||
|
||||
// Populate input/output fields
|
||||
cy.get('#swap-currency-input .token-amount-input').clear().type('1')
|
||||
cy.get('#swap-currency-output .token-amount-input').should('not.equal', '')
|
||||
|
||||
// Switch network
|
||||
switchChain('Polygon')
|
||||
|
||||
// Verify network switch
|
||||
cy.wait('@wallet_switchEthereumChain')
|
||||
waitsForActiveChain('Polygon')
|
||||
cy.get(getTestSelector('web3-status-connected'))
|
||||
|
||||
// Verify that the input/output fields were reset
|
||||
cy.get('#swap-currency-input .token-amount-input').should('have.value', '')
|
||||
cy.get(`#swap-currency-input .token-symbol-container`).should('contain.text', 'MATIC')
|
||||
cy.get(`#swap-currency-output .token-amount-input`).should('not.have.value')
|
||||
cy.get(`#swap-currency-output .token-symbol-container`).should('contain.text', 'Select token')
|
||||
})
|
||||
})
|
@ -1,3 +1,5 @@
|
||||
import { SupportedChainId } from '@uniswap/sdk-core'
|
||||
|
||||
/* eslint-env node */
|
||||
require('dotenv').config()
|
||||
|
||||
@ -5,20 +7,33 @@ require('dotenv').config()
|
||||
// The only requirement is that all infrastructure under test (eg Permit2 contracts) are already deployed.
|
||||
// TODO(WEB-2187): Make more dynamic to avoid manually updating
|
||||
const BLOCK_NUMBER = 17388567
|
||||
const POLYGON_BLOCK_NUMBER = 43600000
|
||||
|
||||
const mainnetFork = {
|
||||
url: `https://mainnet.infura.io/v3/${process.env.REACT_APP_INFURA_KEY}`,
|
||||
blockNumber: BLOCK_NUMBER,
|
||||
const forkingConfig = {
|
||||
httpHeaders: {
|
||||
Origin: 'localhost:3000', // infura allowlists requests by origin
|
||||
},
|
||||
}
|
||||
|
||||
const forks = {
|
||||
[SupportedChainId.MAINNET]: {
|
||||
url: `https://mainnet.infura.io/v3/${process.env.REACT_APP_INFURA_KEY}`,
|
||||
blockNumber: BLOCK_NUMBER,
|
||||
...forkingConfig,
|
||||
},
|
||||
[SupportedChainId.POLYGON]: {
|
||||
url: `https://polygon-mainnet.infura.io/v3/${process.env.REACT_APP_INFURA_KEY}`,
|
||||
blockNumber: POLYGON_BLOCK_NUMBER,
|
||||
...forkingConfig,
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
forks,
|
||||
networks: {
|
||||
hardhat: {
|
||||
chainId: 1,
|
||||
forking: mainnetFork,
|
||||
chainId: SupportedChainId.MAINNET,
|
||||
forking: forks[SupportedChainId.MAINNET],
|
||||
accounts: {
|
||||
count: 2,
|
||||
},
|
||||
|
@ -106,7 +106,7 @@
|
||||
"buffer": "^6.0.3",
|
||||
"concurrently": "^8.0.1",
|
||||
"cypress": "12.12.0",
|
||||
"cypress-hardhat": "^2.3.0",
|
||||
"cypress-hardhat": "^2.4.1",
|
||||
"env-cmd": "^10.1.0",
|
||||
"eslint": "^7.11.0",
|
||||
"eslint-plugin-import": "^2.27",
|
||||
|
@ -240,7 +240,7 @@ export default function AuthenticatedHeader({ account, openSettings }: { account
|
||||
<AuthenticatedHeaderWrapper>
|
||||
<HeaderWrapper>
|
||||
<StatusWrapper>
|
||||
<StatusIcon connection={connection} size={40} />
|
||||
<StatusIcon account={account} connection={connection} size={40} />
|
||||
{account && (
|
||||
<AccountNamesWrapper>
|
||||
<ThemedText.SubHeader>
|
||||
|
@ -8,12 +8,12 @@ import { useToggleAccountDrawer } from 'components/AccountDrawer'
|
||||
import Row from 'components/Row'
|
||||
import { MouseoverTooltip } from 'components/Tooltip'
|
||||
import { useFilterPossiblyMaliciousPositions } from 'hooks/useFilterPossiblyMaliciousPositions'
|
||||
import { useSwitchChain } from 'hooks/useSwitchChain'
|
||||
import { EmptyWalletModule } from 'nft/components/profile/view/EmptyWalletContent'
|
||||
import { useCallback, useMemo, useReducer } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
import { switchChain } from 'utils/switchChain'
|
||||
|
||||
import { ExpandoRow } from '../ExpandoRow'
|
||||
import { PortfolioLogo } from '../PortfolioLogo'
|
||||
@ -126,11 +126,12 @@ function PositionListItem({ positionInfo }: { positionInfo: PositionInfo }) {
|
||||
const navigate = useNavigate()
|
||||
const toggleWalletDrawer = useToggleAccountDrawer()
|
||||
const { chainId: walletChainId, connector } = useWeb3React()
|
||||
const switchChain = useSwitchChain()
|
||||
const onClick = useCallback(async () => {
|
||||
if (walletChainId !== chainId) await switchChain(connector, chainId)
|
||||
toggleWalletDrawer()
|
||||
navigate('/pool/' + details.tokenId)
|
||||
}, [walletChainId, chainId, connector, toggleWalletDrawer, navigate, details.tokenId])
|
||||
}, [walletChainId, chainId, switchChain, connector, toggleWalletDrawer, navigate, details.tokenId])
|
||||
const analyticsEventProperties = useMemo(
|
||||
() => ({
|
||||
chain_id: chainId,
|
||||
|
@ -5,6 +5,8 @@ import { render } from 'test-utils/render'
|
||||
|
||||
import StatusIcon from './StatusIcon'
|
||||
|
||||
const ACCOUNT = '0x0'
|
||||
|
||||
jest.mock('../../hooks/useSocksBalance', () => ({
|
||||
useHasSocks: () => true,
|
||||
}))
|
||||
@ -14,14 +16,14 @@ describe('StatusIcon', () => {
|
||||
it('renders children in correct order', () => {
|
||||
const supportedConnections = getConnections()
|
||||
const injectedConnection = supportedConnections[1]
|
||||
const component = render(<StatusIcon connection={injectedConnection} />)
|
||||
const component = render(<StatusIcon account={ACCOUNT} connection={injectedConnection} />)
|
||||
expect(component.getByTestId('StatusIconRoot')).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders without mini icons', () => {
|
||||
const supportedConnections = getConnections()
|
||||
const injectedConnection = supportedConnections[1]
|
||||
const component = render(<StatusIcon connection={injectedConnection} showMiniIcons={false} />)
|
||||
const component = render(<StatusIcon account={ACCOUNT} connection={injectedConnection} showMiniIcons={false} />)
|
||||
expect(component.getByTestId('StatusIconRoot').children.length).toEqual(0)
|
||||
})
|
||||
})
|
||||
@ -37,15 +39,15 @@ describe('StatusIcon', () => {
|
||||
it('renders children in correct order', () => {
|
||||
const supportedConnections = getConnections()
|
||||
const injectedConnection = supportedConnections[1]
|
||||
const component = render(<StatusIcon connection={injectedConnection} />)
|
||||
const component = render(<StatusIcon account={ACCOUNT} connection={injectedConnection} />)
|
||||
expect(component.getByTestId('StatusIconRoot')).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders without mini icons', () => {
|
||||
const supportedConnections = getConnections()
|
||||
const injectedConnection = supportedConnections[1]
|
||||
const component = render(<StatusIcon connection={injectedConnection} showMiniIcons={false} />)
|
||||
expect(component.getByTestId('StatusIconRoot').children.length).toEqual(1)
|
||||
const component = render(<StatusIcon account={ACCOUNT} connection={injectedConnection} showMiniIcons={false} />)
|
||||
expect(component.getByTestId('StatusIconRoot').children.length).toEqual(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { Unicon } from 'components/Unicon'
|
||||
import { Connection, ConnectionType } from 'connection/types'
|
||||
import useENSAvatar from 'hooks/useENSAvatar'
|
||||
@ -67,24 +66,25 @@ const MiniWalletIcon = ({ connection, side }: { connection: Connection; side: 'l
|
||||
)
|
||||
}
|
||||
|
||||
const MainWalletIcon = ({ connection, size }: { connection: Connection; size: number }) => {
|
||||
const { account } = useWeb3React()
|
||||
const MainWalletIcon = ({ account, connection, size }: { account: string; connection: Connection; size: number }) => {
|
||||
const { avatar } = useENSAvatar(account ?? undefined)
|
||||
|
||||
if (!account) {
|
||||
return null
|
||||
} else if (avatar || (connection.type === ConnectionType.INJECTED && connection.getName() === 'MetaMask')) {
|
||||
return <Identicon size={size} />
|
||||
return <Identicon account={account} size={size} />
|
||||
} else {
|
||||
return <Unicon address={account} size={size} />
|
||||
}
|
||||
}
|
||||
|
||||
export default function StatusIcon({
|
||||
account,
|
||||
connection,
|
||||
size = 16,
|
||||
showMiniIcons = true,
|
||||
}: {
|
||||
account: string
|
||||
connection: Connection
|
||||
size?: number
|
||||
showMiniIcons?: boolean
|
||||
@ -93,7 +93,7 @@ export default function StatusIcon({
|
||||
|
||||
return (
|
||||
<IconWrapper size={size} data-testid="StatusIconRoot">
|
||||
<MainWalletIcon connection={connection} size={size} />
|
||||
<MainWalletIcon account={account} connection={connection} size={size} />
|
||||
{showMiniIcons && <MiniWalletIcon connection={connection} side="right" />}
|
||||
{hasSocks && showMiniIcons && <Socks />}
|
||||
</IconWrapper>
|
||||
|
@ -108,148 +108,6 @@ exports[`StatusIcon with account renders children in correct order 1`] = `
|
||||
data-testid="StatusIconRoot"
|
||||
size="16"
|
||||
>
|
||||
<div
|
||||
style="height: 16px; width: 16px; position: relative;"
|
||||
>
|
||||
<div
|
||||
style="height: 16px; width: 16px; overflow: visible; position: absolute;"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 16 16"
|
||||
>
|
||||
<defs>
|
||||
<defs>
|
||||
<mask
|
||||
id="container-mask0x52270d8234b864dcAC9947f510CE9275A8a116Db16"
|
||||
>
|
||||
<rect
|
||||
fill="white"
|
||||
height="100%"
|
||||
width="100%"
|
||||
x="0"
|
||||
y="0"
|
||||
/>
|
||||
<g
|
||||
transform="scale(0.4444444444444444)
|
||||
translate(0, 0)"
|
||||
>
|
||||
<path
|
||||
d="M18.1309 3.25957C9.91898 3.40293 3.14567 10.1762 3.00231 18.3882C2.85896 26.6001 9.39985 33.141 17.6118 32.9977C25.8238 32.8543 32.5971 26.081 32.7404 17.8691L33 3L18.1309 3.25957Z"
|
||||
fill="black"
|
||||
/>
|
||||
</g>
|
||||
</mask>
|
||||
<mask
|
||||
id="shape-mask0x52270d8234b864dcAC9947f510CE9275A8a116Db16"
|
||||
>
|
||||
<rect
|
||||
fill="white"
|
||||
height="100%"
|
||||
width="100%"
|
||||
x="0"
|
||||
y="0"
|
||||
/>
|
||||
<g
|
||||
transform="scale(0.4444444444444444)
|
||||
translate(10, 10)"
|
||||
>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M13.6569 13.6568C12.0059 10.0663 12.0059 5.93368 13.6569 2.34314C10.0663 3.99414 5.93368 3.99414 2.34315 2.34314C3.99414 5.93368 3.99414 10.0663 2.34315 13.6568C5.93368 12.0059 10.0663 12.0059 13.6569 13.6568ZM8 11C9.65685 11 11 9.65686 11 8.00001C11 6.34315 9.65685 5.00001 8 5.00001C6.34315 5.00001 5 6.34315 5 8.00001C5 9.65686 6.34315 11 8 11Z"
|
||||
fill="black"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</g>
|
||||
</mask>
|
||||
<mask
|
||||
id="mask0x52270d8234b864dcAC9947f510CE9275A8a116Db16"
|
||||
>
|
||||
<g
|
||||
fill="white"
|
||||
>
|
||||
<g
|
||||
mask="url(#shape-mask0x52270d8234b864dcAC9947f510CE9275A8a116Db16)"
|
||||
>
|
||||
<g
|
||||
transform="scale(0.4444444444444444)"
|
||||
>
|
||||
<path
|
||||
d="M18.1309 3.25957C9.91898 3.40293 3.14567 10.1762 3.00231 18.3882C2.85896 26.6001 9.39985 33.141 17.6118 32.9977C25.8238 32.8543 32.5971 26.081 32.7404 17.8691L33 3L18.1309 3.25957Z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
mask="url(#container-mask0x52270d8234b864dcAC9947f510CE9275A8a116Db16)"
|
||||
>
|
||||
<g
|
||||
transform="scale(0.4444444444444444)
|
||||
translate(10, 10)"
|
||||
>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M13.6569 13.6568C12.0059 10.0663 12.0059 5.93368 13.6569 2.34314C10.0663 3.99414 5.93368 3.99414 2.34315 2.34314C3.99414 5.93368 3.99414 10.0663 2.34315 13.6568C5.93368 12.0059 10.0663 12.0059 13.6569 13.6568ZM8 11C9.65685 11 11 9.65686 11 8.00001C11 6.34315 9.65685 5.00001 8 5.00001C6.34315 5.00001 5 6.34315 5 8.00001C5 9.65686 6.34315 11 8 11Z"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</mask>
|
||||
</defs>
|
||||
<lineargradient
|
||||
id="gradient0x52270d8234b864dcAC9947f510CE9275A8a116Db16"
|
||||
>
|
||||
<stop
|
||||
offset="0%"
|
||||
stop-color="#36DBFF"
|
||||
/>
|
||||
<stop
|
||||
offset="100%"
|
||||
stop-color="#B8C3B7"
|
||||
/>
|
||||
</lineargradient>
|
||||
<filter
|
||||
height="200%"
|
||||
id="blur0x52270d8234b864dcAC9947f510CE9275A8a116Db16"
|
||||
width="200%"
|
||||
x="-50%"
|
||||
y="-50%"
|
||||
>
|
||||
<fegaussianblur
|
||||
in="SourceGraphic"
|
||||
stdDeviation="5.333333333333333"
|
||||
/>
|
||||
</filter>
|
||||
</defs>
|
||||
<g
|
||||
mask="url(#mask0x52270d8234b864dcAC9947f510CE9275A8a116Db16)"
|
||||
>
|
||||
<rect
|
||||
fill="url(#gradient0x52270d8234b864dcAC9947f510CE9275A8a116Db16)"
|
||||
height="100%"
|
||||
width="100%"
|
||||
x="0"
|
||||
y="0"
|
||||
/>
|
||||
<rect
|
||||
fill="black"
|
||||
height="100%"
|
||||
opacity="0.08"
|
||||
width="100%"
|
||||
x="0"
|
||||
y="0"
|
||||
/>
|
||||
<ellipse
|
||||
cx="8"
|
||||
cy="0"
|
||||
fill="#9D99F5"
|
||||
filter="url(#blur0x52270d8234b864dcAC9947f510CE9275A8a116Db16)"
|
||||
rx="8"
|
||||
ry="8"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c1"
|
||||
>
|
||||
|
@ -1,5 +1,4 @@
|
||||
import jazzicon from '@metamask/jazzicon'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import useENSAvatar from 'hooks/useENSAvatar'
|
||||
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
||||
import styled from 'styled-components/macro'
|
||||
@ -18,8 +17,7 @@ const StyledAvatar = styled.img`
|
||||
border-radius: inherit;
|
||||
`
|
||||
|
||||
export default function Identicon({ size }: { size?: number }) {
|
||||
const { account } = useWeb3React()
|
||||
export default function Identicon({ account, size }: { account: string; size?: number }) {
|
||||
const { avatar } = useENSAvatar(account ?? undefined)
|
||||
const [fetchable, setFetchable] = useState(true)
|
||||
const iconSize = size ?? 24
|
||||
|
@ -80,7 +80,6 @@ export default function ChainSelectorRow({ disabled, targetChain, onSelectChain,
|
||||
onClick={() => {
|
||||
if (!disabled) onSelectChain(targetChain)
|
||||
}}
|
||||
data-testid={`chain-selector-option-${label.toLowerCase()}`}
|
||||
>
|
||||
<Logo src={logoUrl} alt={label} />
|
||||
<Label>{label}</Label>
|
||||
|
@ -7,11 +7,13 @@ import PrefetchBalancesWrapper from 'components/AccountDrawer/PrefetchBalancesWr
|
||||
import Loader from 'components/Icons/LoadingSpinner'
|
||||
import { IconWrapper } from 'components/Identicon/StatusIcon'
|
||||
import { getConnection } from 'connection'
|
||||
import useLast from 'hooks/useLast'
|
||||
import { navSearchInputVisibleSize } from 'hooks/useScreenSize'
|
||||
import { Portal } from 'nft/components/common/Portal'
|
||||
import { useIsNftClaimAvailable } from 'nft/hooks/useIsNftClaimAvailable'
|
||||
import { darken } from 'polished'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useAppSelector } from 'state/hooks'
|
||||
import styled from 'styled-components/macro'
|
||||
import { colors } from 'theme/colors'
|
||||
import { flexRowNoWrap } from 'theme/styles'
|
||||
@ -42,7 +44,7 @@ const Web3StatusGeneric = styled(ButtonSecondary)`
|
||||
}
|
||||
`
|
||||
|
||||
const Web3StatusConnectWrapper = styled.div<{ faded?: boolean }>`
|
||||
const Web3StatusConnectWrapper = styled.div`
|
||||
${flexRowNoWrap};
|
||||
align-items: center;
|
||||
background-color: ${({ theme }) => theme.accentActionSoft};
|
||||
@ -130,8 +132,11 @@ const StyledConnectButton = styled.button`
|
||||
`
|
||||
|
||||
function Web3StatusInner() {
|
||||
const { account, connector, chainId, ENSName } = useWeb3React()
|
||||
const switchingChain = useAppSelector((state) => state.wallets.switchingChain)
|
||||
const ignoreWhileSwitchingChain = useCallback(() => !switchingChain, [switchingChain])
|
||||
const { account, connector, ENSName } = useLast(useWeb3React(), ignoreWhileSwitchingChain)
|
||||
const connection = getConnection(connector)
|
||||
|
||||
const [, toggleAccountDrawer] = useAccountDrawer()
|
||||
const handleWalletDropdownClick = useCallback(() => {
|
||||
sendAnalyticsEvent(InterfaceEventName.ACCOUNT_DROPDOWN_BUTTON_CLICKED)
|
||||
@ -150,9 +155,7 @@ function Web3StatusInner() {
|
||||
|
||||
const hasPendingTransactions = !!pending.length
|
||||
|
||||
if (!chainId) {
|
||||
return null
|
||||
} else if (account) {
|
||||
if (account) {
|
||||
return (
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
@ -160,12 +163,15 @@ function Web3StatusInner() {
|
||||
properties={{ type: 'open' }}
|
||||
>
|
||||
<Web3StatusConnected
|
||||
disabled={Boolean(switchingChain)}
|
||||
data-testid="web3-status-connected"
|
||||
onClick={handleWalletDropdownClick}
|
||||
pending={hasPendingTransactions}
|
||||
isClaimAvailable={isClaimAvailable}
|
||||
>
|
||||
{!hasPendingTransactions && <StatusIcon size={24} connection={connection} showMiniIcons={false} />}
|
||||
{!hasPendingTransactions && (
|
||||
<StatusIcon size={24} account={account} connection={connection} showMiniIcons={false} />
|
||||
)}
|
||||
{hasPendingTransactions ? (
|
||||
<RowBetween>
|
||||
<Text>
|
||||
@ -190,7 +196,6 @@ function Web3StatusInner() {
|
||||
>
|
||||
<Web3StatusConnectWrapper
|
||||
tabIndex={0}
|
||||
faded={!account}
|
||||
onKeyPress={(e) => e.key === 'Enter' && handleWalletDropdownClick()}
|
||||
onClick={handleWalletDropdownClick}
|
||||
>
|
||||
|
@ -5,11 +5,8 @@ import { useEffect, useState } from 'react'
|
||||
* @param value changing value
|
||||
* @param filterFn function that determines whether a given value should be considered for the last value
|
||||
*/
|
||||
export default function useLast<T>(
|
||||
value: T | undefined | null,
|
||||
filterFn?: (value: T | null | undefined) => boolean
|
||||
): T | null | undefined {
|
||||
const [last, setLast] = useState<T | null | undefined>(filterFn && filterFn(value) ? value : undefined)
|
||||
export default function useLast<T>(value: T, filterFn?: (value: T) => boolean): T {
|
||||
const [last, setLast] = useState<T>(value)
|
||||
useEffect(() => {
|
||||
setLast((last) => {
|
||||
const shouldUse: boolean = filterFn ? filterFn(value) : true
|
||||
|
@ -5,11 +5,13 @@ import { SupportedChainId } from 'constants/chains'
|
||||
import { useCallback } from 'react'
|
||||
import { addPopup } from 'state/application/reducer'
|
||||
import { useAppDispatch } from 'state/hooks'
|
||||
import { switchChain } from 'utils/switchChain'
|
||||
|
||||
import { useSwitchChain } from './useSwitchChain'
|
||||
|
||||
export default function useSelectChain() {
|
||||
const dispatch = useAppDispatch()
|
||||
const { connector } = useWeb3React()
|
||||
const switchChain = useSwitchChain()
|
||||
|
||||
return useCallback(
|
||||
async (targetChain: SupportedChainId) => {
|
||||
@ -20,15 +22,12 @@ export default function useSelectChain() {
|
||||
try {
|
||||
await switchChain(connector, targetChain)
|
||||
} catch (error) {
|
||||
if (didUserReject(connection, error)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!didUserReject(connection, error)) {
|
||||
console.error('Failed to switch networks', error)
|
||||
|
||||
dispatch(addPopup({ content: { failedSwitchNetwork: targetChain }, key: 'failed-network-switch' }))
|
||||
}
|
||||
}
|
||||
},
|
||||
[connector, dispatch]
|
||||
[connector, dispatch, switchChain]
|
||||
)
|
||||
}
|
||||
|
76
src/hooks/useSwitchChain.ts
Normal file
76
src/hooks/useSwitchChain.ts
Normal file
@ -0,0 +1,76 @@
|
||||
import { Connector } from '@web3-react/types'
|
||||
import {
|
||||
networkConnection,
|
||||
uniwalletConnectConnection,
|
||||
walletConnectV1Connection,
|
||||
walletConnectV2Connection,
|
||||
} from 'connection'
|
||||
import { getChainInfo } from 'constants/chainInfo'
|
||||
import { isSupportedChain, SupportedChainId } from 'constants/chains'
|
||||
import { FALLBACK_URLS, RPC_URLS } from 'constants/networks'
|
||||
import { useCallback } from 'react'
|
||||
import { useAppDispatch } from 'state/hooks'
|
||||
import { endSwitchingChain, startSwitchingChain } from 'state/wallets/reducer'
|
||||
|
||||
function getRpcUrl(chainId: SupportedChainId): string {
|
||||
switch (chainId) {
|
||||
case SupportedChainId.MAINNET:
|
||||
case SupportedChainId.GOERLI:
|
||||
case SupportedChainId.SEPOLIA:
|
||||
return RPC_URLS[chainId][0]
|
||||
// Attempting to add a chain using an infura URL will not work, as the URL will be unreachable from the MetaMask background page.
|
||||
// MetaMask allows switching to any publicly reachable URL, but for novel chains, it will display a warning if it is not on the "Safe" list.
|
||||
// See the definition of FALLBACK_URLS for more details.
|
||||
default:
|
||||
return FALLBACK_URLS[chainId][0]
|
||||
}
|
||||
}
|
||||
|
||||
export function useSwitchChain() {
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
return useCallback(
|
||||
async (connector: Connector, chainId: SupportedChainId) => {
|
||||
if (!isSupportedChain(chainId)) {
|
||||
throw new Error(`Chain ${chainId} not supported for connector (${typeof connector})`)
|
||||
} else {
|
||||
dispatch(startSwitchingChain(chainId))
|
||||
try {
|
||||
if (
|
||||
[
|
||||
walletConnectV1Connection.connector,
|
||||
walletConnectV2Connection.connector,
|
||||
uniwalletConnectConnection.connector,
|
||||
networkConnection.connector,
|
||||
].includes(connector)
|
||||
) {
|
||||
await connector.activate(chainId)
|
||||
} else {
|
||||
const info = getChainInfo(chainId)
|
||||
const addChainParameter = {
|
||||
chainId,
|
||||
chainName: info.label,
|
||||
rpcUrls: [getRpcUrl(chainId)],
|
||||
nativeCurrency: info.nativeCurrency,
|
||||
blockExplorerUrls: [info.explorer],
|
||||
}
|
||||
await connector.activate(addChainParameter)
|
||||
}
|
||||
} catch (error) {
|
||||
// In activating a new chain, the connector passes through a deactivated state.
|
||||
// If we fail to switch chains, it may remain in this state, and no longer be usable.
|
||||
// We defensively re-activate the connector to ensure the user does not notice any change.
|
||||
try {
|
||||
await connector.activate()
|
||||
} catch (error) {
|
||||
console.error('Failed to re-activate connector', error)
|
||||
}
|
||||
throw error
|
||||
} finally {
|
||||
dispatch(endSwitchingChain())
|
||||
}
|
||||
}
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
}
|
@ -18,6 +18,7 @@ import { useNftUniversalRouterAddress } from 'graphql/data/nft/NftUniversalRoute
|
||||
import { useCurrency } from 'hooks/Tokens'
|
||||
import { AllowanceState } from 'hooks/usePermit2Allowance'
|
||||
import { useStablecoinValue } from 'hooks/useStablecoinPrice'
|
||||
import { useSwitchChain } from 'hooks/useSwitchChain'
|
||||
import { useTokenBalance } from 'lib/hooks/useCurrencyBalance'
|
||||
import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
|
||||
import { useBag } from 'nft/hooks/useBag'
|
||||
@ -37,7 +38,6 @@ import { AlertTriangle, ChevronDown } from 'react-feather'
|
||||
import { InterfaceTrade, TradeState } from 'state/routing/types'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
import { switchChain } from 'utils/switchChain'
|
||||
import { shallow } from 'zustand/shallow'
|
||||
|
||||
import { BuyButtonStateData, BuyButtonStates, getBuyButtonStateData } from './ButtonStates'
|
||||
@ -348,6 +348,7 @@ export const BagFooter = ({ setModalIsOpen, eventProperties }: BagFooterProps) =
|
||||
setBagStatus(BagStatus.ADDING_TO_BAG)
|
||||
}, [inputCurrency, setBagStatus])
|
||||
|
||||
const switchChain = useSwitchChain()
|
||||
const {
|
||||
buttonText,
|
||||
buttonTextColor,
|
||||
@ -441,6 +442,7 @@ export const BagFooter = ({ setModalIsOpen, eventProperties }: BagFooterProps) =
|
||||
priceImpact,
|
||||
theme,
|
||||
fetchAssets,
|
||||
switchChain,
|
||||
connector,
|
||||
toggleWalletDrawer,
|
||||
setBagExpanded,
|
||||
|
@ -26,6 +26,7 @@ import { useMaxAmountIn } from 'hooks/useMaxAmountIn'
|
||||
import usePermit2Allowance, { AllowanceState } from 'hooks/usePermit2Allowance'
|
||||
import usePrevious from 'hooks/usePrevious'
|
||||
import { useSwapCallback } from 'hooks/useSwapCallback'
|
||||
import { useSwitchChain } from 'hooks/useSwitchChain'
|
||||
import { useUSDPrice } from 'hooks/useUSDPrice'
|
||||
import JSBI from 'jsbi'
|
||||
import { formatSwapQuoteReceivedEventProperties } from 'lib/utils/analytics'
|
||||
@ -33,11 +34,11 @@ import { ReactNode, useCallback, useEffect, useMemo, useReducer, useState } from
|
||||
import { ArrowDown } from 'react-feather'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import { Text } from 'rebass'
|
||||
import { useAppSelector } from 'state/hooks'
|
||||
import { InterfaceTrade, TradeState } from 'state/routing/types'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
import { currencyAmountToPreciseFloat, formatTransactionAmount } from 'utils/formatNumbers'
|
||||
import { didUserReject } from 'utils/swapErrorToUserReadableMessage'
|
||||
import { switchChain } from 'utils/switchChain'
|
||||
|
||||
import AddressInputPanel from '../../components/AddressInputPanel'
|
||||
import { ButtonError, ButtonLight, ButtonPrimary } from '../../components/Button'
|
||||
@ -536,6 +537,9 @@ export function Swap({
|
||||
!showWrap && userHasSpecifiedInputOutput && (trade || routeIsLoading || routeIsSyncing)
|
||||
)
|
||||
|
||||
const switchChain = useSwitchChain()
|
||||
const switchingChain = useAppSelector((state) => state.wallets.switchingChain)
|
||||
|
||||
return (
|
||||
<SwapWrapper chainId={chainId} className={className} id="swap-page">
|
||||
<TokenSafetyModal
|
||||
@ -669,6 +673,10 @@ export function Swap({
|
||||
<Trans>Unsupported Asset</Trans>
|
||||
</ThemedText.DeprecatedMain>
|
||||
</ButtonPrimary>
|
||||
) : switchingChain ? (
|
||||
<ButtonPrimary disabled={true}>
|
||||
<Trans>Connecting to {getChainInfo(switchingChain)?.label}</Trans>
|
||||
</ButtonPrimary>
|
||||
) : !account ? (
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
|
@ -3,24 +3,44 @@ import { Wallet } from './types'
|
||||
|
||||
const WALLET: Wallet = { account: '0x123', walletType: 'test' }
|
||||
|
||||
describe('walletsSlice reducers', () => {
|
||||
const INITIAL_STATE = { connectedWallets: [] as Wallet[], switchingChain: false as const }
|
||||
|
||||
describe('wallets reducer', () => {
|
||||
describe('connectedWallets', () => {
|
||||
it('should add a connected wallet', () => {
|
||||
const initialState = { connectedWallets: [] }
|
||||
const action = {
|
||||
type: 'wallets/addConnectedWallet',
|
||||
payload: WALLET,
|
||||
}
|
||||
const expectedState = { connectedWallets: [WALLET] }
|
||||
expect(walletsReducer(initialState, action)).toEqual(expectedState)
|
||||
const expectedState = { connectedWallets: [WALLET], switchingChain: false }
|
||||
expect(walletsReducer(INITIAL_STATE, action)).toEqual(expectedState)
|
||||
})
|
||||
|
||||
it('should not duplicate a connected wallet', () => {
|
||||
const initialState = { connectedWallets: [WALLET] }
|
||||
const action = {
|
||||
type: 'wallets/addConnectedWallet',
|
||||
payload: WALLET,
|
||||
}
|
||||
const expectedState = { connectedWallets: [WALLET] }
|
||||
expect(walletsReducer(initialState, action)).toEqual(expectedState)
|
||||
const expectedState = { connectedWallets: [WALLET], switchingChain: false }
|
||||
expect(walletsReducer({ ...INITIAL_STATE, connectedWallets: [WALLET] }, action)).toEqual(expectedState)
|
||||
})
|
||||
})
|
||||
|
||||
describe('switchingChain', () => {
|
||||
it('should start switching to chain', () => {
|
||||
const action = {
|
||||
type: 'wallets/startSwitchingChain',
|
||||
payload: 1,
|
||||
}
|
||||
const expectedState = { connectedWallets: [], switchingChain: 1 }
|
||||
expect(walletsReducer(INITIAL_STATE, action)).toEqual(expectedState)
|
||||
})
|
||||
|
||||
it('should stop switching to chain', () => {
|
||||
const action = {
|
||||
type: 'wallets/endSwitchingChain',
|
||||
}
|
||||
expect(walletsReducer({ ...INITIAL_STATE, switchingChain: 1 }, action)).toEqual(INITIAL_STATE)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -1,16 +1,19 @@
|
||||
import { createSlice } from '@reduxjs/toolkit'
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import { shallowEqual } from 'react-redux'
|
||||
|
||||
import { Wallet } from './types'
|
||||
|
||||
// Used to track wallets that have been connected by the user in current session, and remove them when deliberately disconnected.
|
||||
// Used to compute is_reconnect event property for analytics
|
||||
interface WalletState {
|
||||
// Used to track wallets that have been connected by the user in current session, and remove them when deliberately disconnected.
|
||||
// Used to compute is_reconnect event property for analytics
|
||||
connectedWallets: Wallet[]
|
||||
switchingChain: SupportedChainId | false
|
||||
}
|
||||
|
||||
const initialState: WalletState = {
|
||||
connectedWallets: [],
|
||||
switchingChain: false,
|
||||
}
|
||||
|
||||
const walletsSlice = createSlice({
|
||||
@ -21,8 +24,14 @@ const walletsSlice = createSlice({
|
||||
if (state.connectedWallets.some((wallet) => shallowEqual(payload, wallet))) return
|
||||
state.connectedWallets = [...state.connectedWallets, payload]
|
||||
},
|
||||
startSwitchingChain(state, { payload }) {
|
||||
state.switchingChain = payload
|
||||
},
|
||||
endSwitchingChain(state) {
|
||||
state.switchingChain = false
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const { addConnectedWallet } = walletsSlice.actions
|
||||
export const { addConnectedWallet, startSwitchingChain, endSwitchingChain } = walletsSlice.actions
|
||||
export default walletsSlice.reducer
|
||||
|
@ -1,49 +0,0 @@
|
||||
import { Connector } from '@web3-react/types'
|
||||
import {
|
||||
networkConnection,
|
||||
uniwalletConnectConnection,
|
||||
walletConnectV1Connection,
|
||||
walletConnectV2Connection,
|
||||
} from 'connection'
|
||||
import { getChainInfo } from 'constants/chainInfo'
|
||||
import { isSupportedChain, SupportedChainId } from 'constants/chains'
|
||||
import { FALLBACK_URLS, RPC_URLS } from 'constants/networks'
|
||||
|
||||
function getRpcUrl(chainId: SupportedChainId): string {
|
||||
switch (chainId) {
|
||||
case SupportedChainId.MAINNET:
|
||||
case SupportedChainId.GOERLI:
|
||||
case SupportedChainId.SEPOLIA:
|
||||
return RPC_URLS[chainId][0]
|
||||
// Attempting to add a chain using an infura URL will not work, as the URL will be unreachable from the MetaMask background page.
|
||||
// MetaMask allows switching to any publicly reachable URL, but for novel chains, it will display a warning if it is not on the "Safe" list.
|
||||
// See the definition of FALLBACK_URLS for more details.
|
||||
default:
|
||||
return FALLBACK_URLS[chainId][0]
|
||||
}
|
||||
}
|
||||
|
||||
export const switchChain = async (connector: Connector, chainId: SupportedChainId) => {
|
||||
if (!isSupportedChain(chainId)) {
|
||||
throw new Error(`Chain ${chainId} not supported for connector (${typeof connector})`)
|
||||
} else if (
|
||||
[
|
||||
walletConnectV1Connection.connector,
|
||||
walletConnectV2Connection.connector,
|
||||
uniwalletConnectConnection.connector,
|
||||
networkConnection.connector,
|
||||
].includes(connector)
|
||||
) {
|
||||
await connector.activate(chainId)
|
||||
} else {
|
||||
const info = getChainInfo(chainId)
|
||||
const addChainParameter = {
|
||||
chainId,
|
||||
chainName: info.label,
|
||||
rpcUrls: [getRpcUrl(chainId)],
|
||||
nativeCurrency: info.nativeCurrency,
|
||||
blockExplorerUrls: [info.explorer],
|
||||
}
|
||||
await connector.activate(addChainParameter)
|
||||
}
|
||||
}
|
@ -9109,10 +9109,10 @@ csstype@^3.0.2, csstype@^3.0.7:
|
||||
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.0.tgz#4ddcac3718d787cf9df0d1b7d15033925c8f29f2"
|
||||
integrity sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==
|
||||
|
||||
cypress-hardhat@^2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/cypress-hardhat/-/cypress-hardhat-2.3.0.tgz#646b35d57490d060e3fd4441e76e4d91b4ff4ec7"
|
||||
integrity sha512-Sj437lFrUZ9UJGXS5a+DLQPBoyaWUxJafEiydNqKKpViKswBiylHD3ZJu2mrtQ/fhp7lgOPpMP72IQX4Ncwzdg==
|
||||
cypress-hardhat@^2.4.1:
|
||||
version "2.4.1"
|
||||
resolved "https://registry.yarnpkg.com/cypress-hardhat/-/cypress-hardhat-2.4.1.tgz#dff41b06a85c4a572d43ae662f2a0cd03c29c4d7"
|
||||
integrity sha512-D9keayw+9C1YGPTXEfkXDGmPusMoA5Sg2fiRoaBgKHO53UUFQRKnwa6HCTkCxcd0t+Hh8UcwpFwkxtlXh5QtjA==
|
||||
dependencies:
|
||||
"@uniswap/permit2-sdk" "^1.2.0"
|
||||
"@uniswap/sdk-core" "^3.0.1"
|
||||
|
Loading…
Reference in New Issue
Block a user