fix: keep a referentially stable connectors list (#7090)
This commit is contained in:
parent
a53e773e5d
commit
684258dc17
@ -1,5 +1,5 @@
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { getConnections } from 'connection'
|
||||
import { injectedConnection } from 'connection'
|
||||
import { mocked } from 'test-utils/mocked'
|
||||
import { render } from 'test-utils/render'
|
||||
|
||||
@ -14,15 +14,11 @@ jest.mock('../../hooks/useSocksBalance', () => ({
|
||||
describe('StatusIcon', () => {
|
||||
describe('with no account', () => {
|
||||
it('renders children in correct order', () => {
|
||||
const supportedConnections = getConnections()
|
||||
const injectedConnection = supportedConnections[2]
|
||||
const component = render(<StatusIcon account={ACCOUNT} connection={injectedConnection} />)
|
||||
expect(component.getByTestId('StatusIconRoot')).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders without mini icons', () => {
|
||||
const supportedConnections = getConnections()
|
||||
const injectedConnection = supportedConnections[2]
|
||||
const component = render(<StatusIcon account={ACCOUNT} connection={injectedConnection} showMiniIcons={false} />)
|
||||
expect(component.getByTestId('StatusIconRoot').children.length).toEqual(0)
|
||||
})
|
||||
@ -37,15 +33,11 @@ describe('StatusIcon', () => {
|
||||
})
|
||||
|
||||
it('renders children in correct order', () => {
|
||||
const supportedConnections = getConnections()
|
||||
const injectedConnection = supportedConnections[2]
|
||||
const component = render(<StatusIcon account={ACCOUNT} connection={injectedConnection} />)
|
||||
expect(component.getByTestId('StatusIconRoot')).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders without mini icons', () => {
|
||||
const supportedConnections = getConnections()
|
||||
const injectedConnection = supportedConnections[2]
|
||||
const component = render(<StatusIcon account={ACCOUNT} connection={injectedConnection} showMiniIcons={false} />)
|
||||
expect(component.getByTestId('StatusIconRoot').children.length).toEqual(0)
|
||||
})
|
||||
|
@ -112,9 +112,9 @@ exports[`StatusIcon with account renders children in correct order 1`] = `
|
||||
class="c1"
|
||||
>
|
||||
<img
|
||||
alt="WalletConnect icon"
|
||||
alt="Install MetaMask icon"
|
||||
class="c2"
|
||||
src="walletconnect-icon.svg"
|
||||
src="metamask-icon.svg"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
@ -240,9 +240,9 @@ exports[`StatusIcon with no account renders children in correct order 1`] = `
|
||||
class="c1"
|
||||
>
|
||||
<img
|
||||
alt="WalletConnect icon"
|
||||
alt="Install MetaMask icon"
|
||||
class="c2"
|
||||
src="walletconnect-icon.svg"
|
||||
src="metamask-icon.svg"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
|
@ -2,7 +2,7 @@ import { useWeb3React } from '@web3-react/core'
|
||||
import IconButton from 'components/AccountDrawer/IconButton'
|
||||
import { AutoColumn } from 'components/Column'
|
||||
import { AutoRow } from 'components/Row'
|
||||
import { getConnections, networkConnection } from 'connection'
|
||||
import { connections, networkConnection } from 'connection'
|
||||
import { ActivationStatus, useActivationState } from 'connection/activate'
|
||||
import { isSupportedChain } from 'constants/chains'
|
||||
import { useEffect } from 'react'
|
||||
@ -40,8 +40,6 @@ const PrivacyPolicyWrapper = styled.div`
|
||||
export default function WalletModal({ openSettings }: { openSettings: () => void }) {
|
||||
const { connector, chainId } = useWeb3React()
|
||||
|
||||
const connections = getConnections()
|
||||
|
||||
const { activationState } = useActivationState()
|
||||
|
||||
// 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 { InterfaceEventName, WalletConnectionResult } from '@uniswap/analytics-events'
|
||||
import { initializeConnector, MockEIP1193Provider } from '@web3-react/core'
|
||||
import { EIP1193 } from '@web3-react/eip1193'
|
||||
import { MockEIP1193Provider } from '@web3-react/core'
|
||||
import { Provider as EIP1193Provider } from '@web3-react/types'
|
||||
import { sendAnalyticsEvent, user } from 'analytics'
|
||||
import { getConnection } from 'connection'
|
||||
import { connections, getConnection } from 'connection'
|
||||
import { Connection, ConnectionType } from 'connection/types'
|
||||
import useEagerlyConnect from 'hooks/useEagerlyConnect'
|
||||
import useOrderedConnections from 'hooks/useOrderedConnections'
|
||||
import { Provider } from 'react-redux'
|
||||
import { HashRouter } from 'react-router-dom'
|
||||
import store from 'state'
|
||||
@ -20,11 +18,22 @@ jest.mock('analytics', () => ({
|
||||
user: { set: jest.fn(), postInsert: jest.fn() },
|
||||
}))
|
||||
jest.mock('connection', () => {
|
||||
const { EIP1193 } = jest.requireActual('@web3-react/eip1193')
|
||||
const { initializeConnector, MockEIP1193Provider } = jest.requireActual('@web3-react/core')
|
||||
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/useOrderedConnections', () => jest.fn())
|
||||
|
||||
jest.unmock('@web3-react/core')
|
||||
|
||||
@ -45,22 +54,6 @@ const UI = (
|
||||
)
|
||||
|
||||
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 () => {
|
||||
const result = render(UI)
|
||||
await act(async () => {
|
||||
@ -71,8 +64,12 @@ describe('Web3Provider', () => {
|
||||
})
|
||||
|
||||
describe('analytics', () => {
|
||||
let mockProvider: MockEIP1193Provider
|
||||
|
||||
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 () => {
|
||||
@ -84,8 +81,8 @@ describe('Web3Provider', () => {
|
||||
|
||||
// Act
|
||||
act(() => {
|
||||
provider.emitConnect('0x1')
|
||||
provider.emitAccountsChanged(['0x0000000000000000000000000000000000000000'])
|
||||
mockProvider.emitConnect('0x1')
|
||||
mockProvider.emitAccountsChanged(['0x0000000000000000000000000000000000000000'])
|
||||
})
|
||||
|
||||
// Assert
|
||||
@ -114,14 +111,14 @@ describe('Web3Provider', () => {
|
||||
|
||||
// Act
|
||||
act(() => {
|
||||
provider.emitConnect('0x1')
|
||||
provider.emitAccountsChanged(['0x0000000000000000000000000000000000000000'])
|
||||
mockProvider.emitConnect('0x1')
|
||||
mockProvider.emitAccountsChanged(['0x0000000000000000000000000000000000000000'])
|
||||
})
|
||||
act(() => {
|
||||
provider.emitAccountsChanged(['0x0000000000000000000000000000000000000001'])
|
||||
mockProvider.emitAccountsChanged(['0x0000000000000000000000000000000000000001'])
|
||||
})
|
||||
act(() => {
|
||||
provider.emitAccountsChanged(['0x0000000000000000000000000000000000000000'])
|
||||
mockProvider.emitAccountsChanged(['0x0000000000000000000000000000000000000000'])
|
||||
})
|
||||
|
||||
// Assert
|
||||
|
@ -3,12 +3,11 @@ import { getWalletMeta } from '@uniswap/conedison/provider/meta'
|
||||
import { useWeb3React, Web3ReactHooks, Web3ReactProvider } from '@web3-react/core'
|
||||
import { Connector } from '@web3-react/types'
|
||||
import { sendAnalyticsEvent, user } from 'analytics'
|
||||
import { getConnection } from 'connection'
|
||||
import { connections, getConnection } from 'connection'
|
||||
import { isSupportedChain } from 'constants/chains'
|
||||
import { RPC_PROVIDERS } from 'constants/providers'
|
||||
import { TraceJsonRpcVariant, useTraceJsonRpcFlag } from 'featureFlags/flags/traceJsonRpc'
|
||||
import useEagerlyConnect from 'hooks/useEagerlyConnect'
|
||||
import useOrderedConnections from 'hooks/useOrderedConnections'
|
||||
import usePrevious from 'hooks/usePrevious'
|
||||
import { ReactNode, useEffect } from 'react'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
@ -17,8 +16,7 @@ import { getCurrentPageFromLocation } from 'utils/urlRoutes'
|
||||
|
||||
export default function Web3Provider({ children }: { children: ReactNode }) {
|
||||
useEagerlyConnect()
|
||||
const connections = useOrderedConnections()
|
||||
const connectors: [Connector, Web3ReactHooks][] = connections.map(({ hooks, connector }) => [connector, hooks])
|
||||
const connectors = connections.map<[Connector, Web3ReactHooks]>(({ hooks, connector }) => [connector, hooks])
|
||||
|
||||
return (
|
||||
<Web3ReactProvider connectors={connectors}>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import INJECTED_DARK_ICON from 'assets/wallets/browser-wallet-dark.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'
|
||||
|
||||
@ -19,7 +19,7 @@ describe('connection utility/metadata tests', () => {
|
||||
UserAgentMock.isMobile = isMobile
|
||||
global.window.ethereum = ethereum
|
||||
|
||||
const displayed = getConnections().filter((c) => c.shouldDisplay())
|
||||
const displayed = connections.filter((c) => c.shouldDisplay())
|
||||
const injected = getConnection(ConnectionType.INJECTED)
|
||||
const coinbase = getConnection(ConnectionType.COINBASE_WALLET)
|
||||
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 UNIWALLET_ICON from 'assets/wallets/uniswap-wallet-icon.png'
|
||||
import WALLET_CONNECT_ICON from 'assets/wallets/walletconnect-icon.svg'
|
||||
import { useSyncExternalStore } from 'react'
|
||||
import { isMobile, isNonIOSPhone } from 'utils/userAgent'
|
||||
|
||||
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 injectedConnection: Connection = {
|
||||
export const injectedConnection: Connection = {
|
||||
getName: () => getInjection().name,
|
||||
connector: web3Injected,
|
||||
hooks: web3InjectedHooks,
|
||||
@ -78,17 +79,53 @@ export const walletConnectV2Connection: Connection = new (class implements Conne
|
||||
getIcon = () => WALLET_CONNECT_ICON
|
||||
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) => {
|
||||
// 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
|
||||
}
|
||||
|
||||
get connector() {
|
||||
return this._connector[0]
|
||||
return this.proxyConnector
|
||||
}
|
||||
get hooks() {
|
||||
return this._connector[1]
|
||||
return this.proxyHooks
|
||||
}
|
||||
})()
|
||||
|
||||
@ -135,20 +172,18 @@ const coinbaseWalletConnection: Connection = {
|
||||
},
|
||||
}
|
||||
|
||||
export function getConnections() {
|
||||
return [
|
||||
uniwalletWCV2ConnectConnection,
|
||||
injectedConnection,
|
||||
walletConnectV2Connection,
|
||||
coinbaseWalletConnection,
|
||||
gnosisSafeConnection,
|
||||
networkConnection,
|
||||
]
|
||||
}
|
||||
export const connections = [
|
||||
gnosisSafeConnection,
|
||||
uniwalletWCV2ConnectConnection,
|
||||
injectedConnection,
|
||||
walletConnectV2Connection,
|
||||
coinbaseWalletConnection,
|
||||
networkConnection,
|
||||
]
|
||||
|
||||
export function getConnection(c: Connector | ConnectionType) {
|
||||
if (c instanceof Connector) {
|
||||
const connection = getConnections().find((connection) => connection.connector === c)
|
||||
const connection = connections.find((connection) => connection.connector === c)
|
||||
if (!connection) {
|
||||
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))
|
||||
}, [])
|
||||
}
|
Loading…
Reference in New Issue
Block a user