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"
>
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))
- }, [])
-}