fix: keep a referentially stable connectors list (#7090)

This commit is contained in:
Zach Pomerantz 2023-08-08 08:46:01 -07:00 committed by GitHub
parent a53e773e5d
commit 684258dc17
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 87 additions and 93 deletions

@ -1,5 +1,5 @@
import { useWeb3React } from '@web3-react/core' import { useWeb3React } from '@web3-react/core'
import { getConnections } from 'connection' import { injectedConnection } from 'connection'
import { mocked } from 'test-utils/mocked' import { mocked } from 'test-utils/mocked'
import { render } from 'test-utils/render' import { render } from 'test-utils/render'
@ -14,15 +14,11 @@ jest.mock('../../hooks/useSocksBalance', () => ({
describe('StatusIcon', () => { describe('StatusIcon', () => {
describe('with no account', () => { describe('with no account', () => {
it('renders children in correct order', () => { it('renders children in correct order', () => {
const supportedConnections = getConnections()
const injectedConnection = supportedConnections[2]
const component = render(<StatusIcon account={ACCOUNT} connection={injectedConnection} />) const component = render(<StatusIcon account={ACCOUNT} connection={injectedConnection} />)
expect(component.getByTestId('StatusIconRoot')).toMatchSnapshot() expect(component.getByTestId('StatusIconRoot')).toMatchSnapshot()
}) })
it('renders without mini icons', () => { it('renders without mini icons', () => {
const supportedConnections = getConnections()
const injectedConnection = supportedConnections[2]
const component = render(<StatusIcon account={ACCOUNT} connection={injectedConnection} showMiniIcons={false} />) const component = render(<StatusIcon account={ACCOUNT} connection={injectedConnection} showMiniIcons={false} />)
expect(component.getByTestId('StatusIconRoot').children.length).toEqual(0) expect(component.getByTestId('StatusIconRoot').children.length).toEqual(0)
}) })
@ -37,15 +33,11 @@ describe('StatusIcon', () => {
}) })
it('renders children in correct order', () => { it('renders children in correct order', () => {
const supportedConnections = getConnections()
const injectedConnection = supportedConnections[2]
const component = render(<StatusIcon account={ACCOUNT} connection={injectedConnection} />) const component = render(<StatusIcon account={ACCOUNT} connection={injectedConnection} />)
expect(component.getByTestId('StatusIconRoot')).toMatchSnapshot() expect(component.getByTestId('StatusIconRoot')).toMatchSnapshot()
}) })
it('renders without mini icons', () => { it('renders without mini icons', () => {
const supportedConnections = getConnections()
const injectedConnection = supportedConnections[2]
const component = render(<StatusIcon account={ACCOUNT} connection={injectedConnection} showMiniIcons={false} />) const component = render(<StatusIcon account={ACCOUNT} connection={injectedConnection} showMiniIcons={false} />)
expect(component.getByTestId('StatusIconRoot').children.length).toEqual(0) expect(component.getByTestId('StatusIconRoot').children.length).toEqual(0)
}) })

@ -112,9 +112,9 @@ exports[`StatusIcon with account renders children in correct order 1`] = `
class="c1" class="c1"
> >
<img <img
alt="WalletConnect icon" alt="Install MetaMask icon"
class="c2" class="c2"
src="walletconnect-icon.svg" src="metamask-icon.svg"
/> />
</div> </div>
<div <div
@ -240,9 +240,9 @@ exports[`StatusIcon with no account renders children in correct order 1`] = `
class="c1" class="c1"
> >
<img <img
alt="WalletConnect icon" alt="Install MetaMask icon"
class="c2" class="c2"
src="walletconnect-icon.svg" src="metamask-icon.svg"
/> />
</div> </div>
<div <div

@ -2,7 +2,7 @@ import { useWeb3React } from '@web3-react/core'
import IconButton from 'components/AccountDrawer/IconButton' import IconButton from 'components/AccountDrawer/IconButton'
import { AutoColumn } from 'components/Column' import { AutoColumn } from 'components/Column'
import { AutoRow } from 'components/Row' import { AutoRow } from 'components/Row'
import { getConnections, networkConnection } from 'connection' import { connections, networkConnection } from 'connection'
import { ActivationStatus, useActivationState } from 'connection/activate' import { ActivationStatus, useActivationState } from 'connection/activate'
import { isSupportedChain } from 'constants/chains' import { isSupportedChain } from 'constants/chains'
import { useEffect } from 'react' import { useEffect } from 'react'
@ -40,8 +40,6 @@ const PrivacyPolicyWrapper = styled.div`
export default function WalletModal({ openSettings }: { openSettings: () => void }) { export default function WalletModal({ openSettings }: { openSettings: () => void }) {
const { connector, chainId } = useWeb3React() const { connector, chainId } = useWeb3React()
const connections = getConnections()
const { activationState } = useActivationState() const { activationState } = useActivationState()
// Keep the network connector in sync with any active user connector to prevent chain-switching on wallet disconnection. // Keep the network connector in sync with any active user connector to prevent chain-switching on wallet disconnection.

@ -1,13 +1,11 @@
import { act, render } from '@testing-library/react' import { act, render } from '@testing-library/react'
import { InterfaceEventName, WalletConnectionResult } from '@uniswap/analytics-events' import { InterfaceEventName, WalletConnectionResult } from '@uniswap/analytics-events'
import { initializeConnector, MockEIP1193Provider } from '@web3-react/core' import { MockEIP1193Provider } from '@web3-react/core'
import { EIP1193 } from '@web3-react/eip1193'
import { Provider as EIP1193Provider } from '@web3-react/types' import { Provider as EIP1193Provider } from '@web3-react/types'
import { sendAnalyticsEvent, user } from 'analytics' import { sendAnalyticsEvent, user } from 'analytics'
import { getConnection } from 'connection' import { connections, getConnection } from 'connection'
import { Connection, ConnectionType } from 'connection/types' import { Connection, ConnectionType } from 'connection/types'
import useEagerlyConnect from 'hooks/useEagerlyConnect' import useEagerlyConnect from 'hooks/useEagerlyConnect'
import useOrderedConnections from 'hooks/useOrderedConnections'
import { Provider } from 'react-redux' import { Provider } from 'react-redux'
import { HashRouter } from 'react-router-dom' import { HashRouter } from 'react-router-dom'
import store from 'state' import store from 'state'
@ -20,11 +18,22 @@ jest.mock('analytics', () => ({
user: { set: jest.fn(), postInsert: jest.fn() }, user: { set: jest.fn(), postInsert: jest.fn() },
})) }))
jest.mock('connection', () => { jest.mock('connection', () => {
const { EIP1193 } = jest.requireActual('@web3-react/eip1193')
const { initializeConnector, MockEIP1193Provider } = jest.requireActual('@web3-react/core')
const { ConnectionType } = jest.requireActual('connection') const { ConnectionType } = jest.requireActual('connection')
return { ConnectionType, getConnection: jest.fn() } const provider: EIP1193Provider = new MockEIP1193Provider()
const [connector, hooks] = initializeConnector((actions: any) => new EIP1193({ actions, provider }))
const mockConnection: Connection = {
connector,
hooks,
getName: () => 'test',
type: 'INJECTED' as ConnectionType,
shouldDisplay: () => false,
}
return { ConnectionType, getConnection: jest.fn(), connections: [mockConnection] }
}) })
jest.mock('hooks/useEagerlyConnect', () => jest.fn()) jest.mock('hooks/useEagerlyConnect', () => jest.fn())
jest.mock('hooks/useOrderedConnections', () => jest.fn())
jest.unmock('@web3-react/core') jest.unmock('@web3-react/core')
@ -45,22 +54,6 @@ const UI = (
) )
describe('Web3Provider', () => { describe('Web3Provider', () => {
let provider: MockEIP1193Provider & EIP1193Provider
let connection: Connection
beforeEach(() => {
provider = new MockEIP1193Provider() as MockEIP1193Provider & EIP1193Provider
const [connector, hooks] = initializeConnector((actions) => new EIP1193({ actions, provider }))
connection = {
connector,
hooks,
getName: jest.fn().mockReturnValue('test'),
type: 'INJECTED' as ConnectionType,
shouldDisplay: () => false,
}
mocked(useOrderedConnections).mockReturnValue([connection])
})
it('renders and eagerly connects', async () => { it('renders and eagerly connects', async () => {
const result = render(UI) const result = render(UI)
await act(async () => { await act(async () => {
@ -71,8 +64,12 @@ describe('Web3Provider', () => {
}) })
describe('analytics', () => { describe('analytics', () => {
let mockProvider: MockEIP1193Provider
beforeEach(() => { beforeEach(() => {
mocked(getConnection).mockReturnValue(connection) const mockConnection = connections[0]
mockProvider = mockConnection.connector.provider as MockEIP1193Provider
mocked(getConnection).mockReturnValue(mockConnection)
}) })
it('sends event when the active account changes', async () => { it('sends event when the active account changes', async () => {
@ -84,8 +81,8 @@ describe('Web3Provider', () => {
// Act // Act
act(() => { act(() => {
provider.emitConnect('0x1') mockProvider.emitConnect('0x1')
provider.emitAccountsChanged(['0x0000000000000000000000000000000000000000']) mockProvider.emitAccountsChanged(['0x0000000000000000000000000000000000000000'])
}) })
// Assert // Assert
@ -114,14 +111,14 @@ describe('Web3Provider', () => {
// Act // Act
act(() => { act(() => {
provider.emitConnect('0x1') mockProvider.emitConnect('0x1')
provider.emitAccountsChanged(['0x0000000000000000000000000000000000000000']) mockProvider.emitAccountsChanged(['0x0000000000000000000000000000000000000000'])
}) })
act(() => { act(() => {
provider.emitAccountsChanged(['0x0000000000000000000000000000000000000001']) mockProvider.emitAccountsChanged(['0x0000000000000000000000000000000000000001'])
}) })
act(() => { act(() => {
provider.emitAccountsChanged(['0x0000000000000000000000000000000000000000']) mockProvider.emitAccountsChanged(['0x0000000000000000000000000000000000000000'])
}) })
// Assert // Assert

@ -3,12 +3,11 @@ import { getWalletMeta } from '@uniswap/conedison/provider/meta'
import { useWeb3React, Web3ReactHooks, Web3ReactProvider } from '@web3-react/core' import { useWeb3React, Web3ReactHooks, Web3ReactProvider } from '@web3-react/core'
import { Connector } from '@web3-react/types' import { Connector } from '@web3-react/types'
import { sendAnalyticsEvent, user } from 'analytics' import { sendAnalyticsEvent, user } from 'analytics'
import { getConnection } from 'connection' import { connections, getConnection } from 'connection'
import { isSupportedChain } from 'constants/chains' import { isSupportedChain } from 'constants/chains'
import { RPC_PROVIDERS } from 'constants/providers' import { RPC_PROVIDERS } from 'constants/providers'
import { TraceJsonRpcVariant, useTraceJsonRpcFlag } from 'featureFlags/flags/traceJsonRpc' import { TraceJsonRpcVariant, useTraceJsonRpcFlag } from 'featureFlags/flags/traceJsonRpc'
import useEagerlyConnect from 'hooks/useEagerlyConnect' import useEagerlyConnect from 'hooks/useEagerlyConnect'
import useOrderedConnections from 'hooks/useOrderedConnections'
import usePrevious from 'hooks/usePrevious' import usePrevious from 'hooks/usePrevious'
import { ReactNode, useEffect } from 'react' import { ReactNode, useEffect } from 'react'
import { useLocation } from 'react-router-dom' import { useLocation } from 'react-router-dom'
@ -17,8 +16,7 @@ import { getCurrentPageFromLocation } from 'utils/urlRoutes'
export default function Web3Provider({ children }: { children: ReactNode }) { export default function Web3Provider({ children }: { children: ReactNode }) {
useEagerlyConnect() useEagerlyConnect()
const connections = useOrderedConnections() const connectors = connections.map<[Connector, Web3ReactHooks]>(({ hooks, connector }) => [connector, hooks])
const connectors: [Connector, Web3ReactHooks][] = connections.map(({ hooks, connector }) => [connector, hooks])
return ( return (
<Web3ReactProvider connectors={connectors}> <Web3ReactProvider connectors={connectors}>

@ -1,6 +1,6 @@
import INJECTED_DARK_ICON from 'assets/wallets/browser-wallet-dark.svg' import INJECTED_DARK_ICON from 'assets/wallets/browser-wallet-dark.svg'
import INJECTED_LIGHT_ICON from 'assets/wallets/browser-wallet-light.svg' import INJECTED_LIGHT_ICON from 'assets/wallets/browser-wallet-light.svg'
import { getConnection, getConnections } from 'connection' import { connections, getConnection } from 'connection'
import { ConnectionType } from './types' import { ConnectionType } from './types'
@ -19,7 +19,7 @@ describe('connection utility/metadata tests', () => {
UserAgentMock.isMobile = isMobile UserAgentMock.isMobile = isMobile
global.window.ethereum = ethereum global.window.ethereum = ethereum
const displayed = getConnections().filter((c) => c.shouldDisplay()) const displayed = connections.filter((c) => c.shouldDisplay())
const injected = getConnection(ConnectionType.INJECTED) const injected = getConnection(ConnectionType.INJECTED)
const coinbase = getConnection(ConnectionType.COINBASE_WALLET) const coinbase = getConnection(ConnectionType.COINBASE_WALLET)
const uniswap = getConnection(ConnectionType.UNISWAP_WALLET_V2) const uniswap = getConnection(ConnectionType.UNISWAP_WALLET_V2)

@ -10,6 +10,7 @@ import UNISWAP_LOGO from 'assets/svg/logo.svg'
import COINBASE_ICON from 'assets/wallets/coinbase-icon.svg' import COINBASE_ICON from 'assets/wallets/coinbase-icon.svg'
import UNIWALLET_ICON from 'assets/wallets/uniswap-wallet-icon.png' import UNIWALLET_ICON from 'assets/wallets/uniswap-wallet-icon.png'
import WALLET_CONNECT_ICON from 'assets/wallets/walletconnect-icon.svg' import WALLET_CONNECT_ICON from 'assets/wallets/walletconnect-icon.svg'
import { useSyncExternalStore } from 'react'
import { isMobile, isNonIOSPhone } from 'utils/userAgent' import { isMobile, isNonIOSPhone } from 'utils/userAgent'
import { RPC_URLS } from '../constants/networks' import { RPC_URLS } from '../constants/networks'
@ -43,7 +44,7 @@ const getIsGenericInjector = () => getIsInjected() && !getIsMetaMaskWallet() &&
const [web3Injected, web3InjectedHooks] = initializeConnector<MetaMask>((actions) => new MetaMask({ actions, onError })) const [web3Injected, web3InjectedHooks] = initializeConnector<MetaMask>((actions) => new MetaMask({ actions, onError }))
const injectedConnection: Connection = { export const injectedConnection: Connection = {
getName: () => getInjection().name, getName: () => getInjection().name,
connector: web3Injected, connector: web3Injected,
hooks: web3InjectedHooks, hooks: web3InjectedHooks,
@ -78,17 +79,53 @@ export const walletConnectV2Connection: Connection = new (class implements Conne
getIcon = () => WALLET_CONNECT_ICON getIcon = () => WALLET_CONNECT_ICON
shouldDisplay = () => !getIsInjectedMobileBrowser() shouldDisplay = () => !getIsInjectedMobileBrowser()
private _connector = initializeConnector<WalletConnectV2>(this.initializer) private activeConnector = initializeConnector<WalletConnectV2>(this.initializer)
// The web3-react Provider requires referentially stable connectors, so we use proxies to allow lazy connections
// whilst maintaining referential equality.
private proxyConnector = new Proxy(
{},
{
get: (target, p, receiver) => Reflect.get(this.activeConnector[0], p, receiver),
getOwnPropertyDescriptor: (target, p) => Reflect.getOwnPropertyDescriptor(this.activeConnector[0], p),
getPrototypeOf: () => WalletConnectV2.prototype,
set: (target, p, receiver) => Reflect.set(this.activeConnector[0], p, receiver),
}
) as (typeof this.activeConnector)[0]
private proxyHooks = new Proxy(
{},
{
get: (target, p, receiver) => {
return () => {
// Because our connectors are referentially stable (through proxying), we need a way to trigger React renders
// from outside of the React lifecycle when our connector is re-initialized. This is done via 'change' events
// with `useSyncExternalStore`:
const hooks = useSyncExternalStore(
(onChange) => {
this.onActivate = onChange
return () => (this.onActivate = undefined)
},
() => this.activeConnector[1]
)
return Reflect.get(hooks, p, receiver)()
}
},
}
) as (typeof this.activeConnector)[1]
private onActivate?: () => void
overrideActivate = (chainId?: ChainId) => { overrideActivate = (chainId?: ChainId) => {
// Always re-create the connector, so that the chainId is updated. // Always re-create the connector, so that the chainId is updated.
this._connector = initializeConnector((actions) => this.initializer(actions, chainId)) this.activeConnector = initializeConnector((actions) => this.initializer(actions, chainId))
this.onActivate?.()
return false return false
} }
get connector() { get connector() {
return this._connector[0] return this.proxyConnector
} }
get hooks() { get hooks() {
return this._connector[1] return this.proxyHooks
} }
})() })()
@ -135,20 +172,18 @@ const coinbaseWalletConnection: Connection = {
}, },
} }
export function getConnections() { export const connections = [
return [ gnosisSafeConnection,
uniwalletWCV2ConnectConnection, uniwalletWCV2ConnectConnection,
injectedConnection, injectedConnection,
walletConnectV2Connection, walletConnectV2Connection,
coinbaseWalletConnection, coinbaseWalletConnection,
gnosisSafeConnection, networkConnection,
networkConnection, ]
]
}
export function getConnection(c: Connector | ConnectionType) { export function getConnection(c: Connector | ConnectionType) {
if (c instanceof Connector) { if (c instanceof Connector) {
const connection = getConnections().find((connection) => connection.connector === c) const connection = connections.find((connection) => connection.connector === c)
if (!connection) { if (!connection) {
throw Error('unsupported connector') throw Error('unsupported connector')
} }

@ -1,26 +0,0 @@
import { getConnection } from 'connection'
import { ConnectionType } from 'connection/types'
import { useMemo } from 'react'
const SELECTABLE_WALLETS = [
ConnectionType.UNISWAP_WALLET_V2,
ConnectionType.INJECTED,
ConnectionType.WALLET_CONNECT_V2,
ConnectionType.COINBASE_WALLET,
]
export default function useOrderedConnections() {
return useMemo(() => {
const orderedConnectionTypes: ConnectionType[] = []
// Always attempt to use to Gnosis Safe first, as we can't know if we're in a SafeContext.
orderedConnectionTypes.push(ConnectionType.GNOSIS_SAFE)
orderedConnectionTypes.push(...SELECTABLE_WALLETS)
// Add network connection last as it should be the fallback.
orderedConnectionTypes.push(ConnectionType.NETWORK)
return orderedConnectionTypes.map((connectionType) => getConnection(connectionType))
}, [])
}