diff --git a/src/components/Identicon/StatusIcon.test.tsx b/src/components/Identicon/StatusIcon.test.tsx index a8461143db..d6d52c9bc9 100644 --- a/src/components/Identicon/StatusIcon.test.tsx +++ b/src/components/Identicon/StatusIcon.test.tsx @@ -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() expect(component.getByTestId('StatusIconRoot')).toMatchSnapshot() }) it('renders without mini icons', () => { - const supportedConnections = getConnections() - const injectedConnection = supportedConnections[2] const component = render() 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() expect(component.getByTestId('StatusIconRoot')).toMatchSnapshot() }) it('renders without mini icons', () => { - const supportedConnections = getConnections() - const injectedConnection = supportedConnections[2] const component = render() expect(component.getByTestId('StatusIconRoot').children.length).toEqual(0) }) diff --git a/src/components/Identicon/__snapshots__/StatusIcon.test.tsx.snap b/src/components/Identicon/__snapshots__/StatusIcon.test.tsx.snap index f8f8635caf..9f9ed1c083 100644 --- a/src/components/Identicon/__snapshots__/StatusIcon.test.tsx.snap +++ b/src/components/Identicon/__snapshots__/StatusIcon.test.tsx.snap @@ -112,9 +112,9 @@ exports[`StatusIcon with account renders children in correct order 1`] = ` class="c1" > WalletConnect icon
WalletConnect icon
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. diff --git a/src/components/Web3Provider/index.test.tsx b/src/components/Web3Provider/index.test.tsx index 391f966999..1a91312231 100644 --- a/src/components/Web3Provider/index.test.tsx +++ b/src/components/Web3Provider/index.test.tsx @@ -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 diff --git a/src/components/Web3Provider/index.tsx b/src/components/Web3Provider/index.tsx index 64378e31d3..1e29cb286a 100644 --- a/src/components/Web3Provider/index.tsx +++ b/src/components/Web3Provider/index.tsx @@ -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 ( diff --git a/src/connection/index.test.tsx b/src/connection/index.test.tsx index 7bc3464d4d..d02e87a79f 100644 --- a/src/connection/index.test.tsx +++ b/src/connection/index.test.tsx @@ -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) diff --git a/src/connection/index.ts b/src/connection/index.ts index 6af0f2c591..4cbd608ce5 100644 --- a/src/connection/index.ts +++ b/src/connection/index.ts @@ -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((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(this.initializer) + private activeConnector = initializeConnector(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') } diff --git a/src/hooks/useOrderedConnections.ts b/src/hooks/useOrderedConnections.ts deleted file mode 100644 index 7bad56601a..0000000000 --- a/src/hooks/useOrderedConnections.ts +++ /dev/null @@ -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)) - }, []) -}