From 0eeeadfcda45b4b4979cf0886972ff9bf57a7610 Mon Sep 17 00:00:00 2001 From: Noah Zinsmeister Date: Thu, 13 Jan 2022 14:50:15 -0500 Subject: [PATCH] playing around with useHighestPriorityConnector (#355) * first pass at useHighestPriorityConnector * prefix increment * some cleanup * add some tests --- .gitignore | 1 + packages/core/src/index.spec.ts | 92 +++++++++++++++++++- packages/core/src/index.ts | 59 +++++++++++-- packages/example/.env | 3 + packages/example/App.tsx | 40 ++++----- packages/example/connectors/frame.ts | 2 +- packages/example/connectors/index.ts | 38 +++++--- packages/example/connectors/magic.ts | 2 +- packages/example/connectors/metaMask.ts | 2 +- packages/example/connectors/network.ts | 2 +- packages/example/connectors/walletConnect.ts | 2 +- packages/example/connectors/walletLink.ts | 2 +- packages/example/next.config.js | 6 +- 13 files changed, 198 insertions(+), 53 deletions(-) create mode 100644 packages/example/.env diff --git a/.gitignore b/.gitignore index b5d70d86e..0bf82f55d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules/ packages/*/dist/ packages/example/.next/ +.env.local diff --git a/packages/core/src/index.spec.ts b/packages/core/src/index.spec.ts index f470f5fa2..04650c68b 100644 --- a/packages/core/src/index.spec.ts +++ b/packages/core/src/index.spec.ts @@ -1,7 +1,7 @@ import { act, renderHook } from '@testing-library/react-hooks' -import type { Actions } from '@web3-react/types' +import type { Actions, Web3ReactStore } from '@web3-react/types' import { Connector } from '@web3-react/types' -import { initializeConnector, Web3ReactHooks } from '.' +import { initializeConnector, useHighestPriorityConnector, Web3ReactHooks } from '.' class MockConnector extends Connector { constructor(actions: Actions) { @@ -18,6 +18,8 @@ class MockConnector extends Connector { } } +class MockConnector2 extends MockConnector {} + describe('#initializeConnector', () => { let connector: MockConnector let hooks: Web3ReactHooks @@ -113,3 +115,89 @@ describe('#initializeConnector', () => { expect(error).toBeInstanceOf(Error) }) }) + +describe('#useHighestPriorityConnector', () => { + let connector: MockConnector + let hooks: Web3ReactHooks + let store: Web3ReactStore + + let connector2: MockConnector + let hooks2: Web3ReactHooks + let store2: Web3ReactStore + + beforeEach(() => { + ;[connector, hooks, store] = initializeConnector((actions) => new MockConnector(actions)) + ;[connector2, hooks2, store2] = initializeConnector((actions) => new MockConnector2(actions)) + }) + + test('returns first connector if both are uninitialized', () => { + const { + result: { current: highestPriorityConnector }, + } = renderHook(() => + useHighestPriorityConnector([ + [connector, hooks, store], + [connector2, hooks2, store2], + ]) + ) + + expect(highestPriorityConnector[0]).toBeInstanceOf(MockConnector) + expect(highestPriorityConnector[0]).not.toBeInstanceOf(MockConnector2) + }) + + test('returns first connector if it is initialized', () => { + let { + result: { current: highestPriorityConnector }, + } = renderHook(() => + useHighestPriorityConnector([ + [connector, hooks, store], + [connector2, hooks2, store2], + ]) + ) + + act(() => connector.update({ chainId: 1, accounts: [] })) + ;({ + result: { current: highestPriorityConnector }, + } = renderHook(() => + useHighestPriorityConnector([ + [connector, hooks, store], + [connector2, hooks2, store2], + ]) + )) + + const { + result: { current: isActive }, + } = renderHook(() => highestPriorityConnector[1].useIsActive()) + + expect(highestPriorityConnector[0]).toBeInstanceOf(MockConnector) + expect(highestPriorityConnector[0]).not.toBeInstanceOf(MockConnector2) + expect(isActive).toEqual(true) + }) + + test('returns second connector if it is initialized', () => { + let { + result: { current: highestPriorityConnector }, + } = renderHook(() => + useHighestPriorityConnector([ + [connector, hooks, store], + [connector2, hooks2, store2], + ]) + ) + + act(() => connector2.update({ chainId: 1, accounts: [] })) + ;({ + result: { current: highestPriorityConnector }, + } = renderHook(() => + useHighestPriorityConnector([ + [connector, hooks, store], + [connector2, hooks2, store2], + ]) + )) + + const { + result: { current: isActive }, + } = renderHook(() => highestPriorityConnector[1].useIsActive()) + + expect(highestPriorityConnector[0]).toBeInstanceOf(MockConnector2) + expect(isActive).toEqual(true) + }) +}) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index d21575f19..257b46bb6 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,8 +1,8 @@ import type { Networkish } from '@ethersproject/networks' import { Web3Provider } from '@ethersproject/providers' import { createWeb3ReactStoreAndActions } from '@web3-react/store' -import type { Actions, Connector, Web3ReactState } from '@web3-react/types' -import { useEffect, useMemo, useState } from 'react' +import type { Actions, Connector, Web3ReactState, Web3ReactStore } from '@web3-react/types' +import { useEffect, useMemo, useRef, useState } from 'react' import type { EqualityChecker, UseBoundStore } from 'zustand' import create from 'zustand' @@ -10,10 +10,14 @@ export type Web3ReactHooks = ReturnType & ReturnType & ReturnType +function computeIsActive({ chainId, accounts, activating, error }: Web3ReactState) { + return Boolean(chainId && accounts && !activating && !error) +} + export function initializeConnector( f: (actions: Actions) => T, allowedChainIds?: number[] -): [T, Web3ReactHooks] { +): [T, Web3ReactHooks, Web3ReactStore] { const [store, actions] = createWeb3ReactStoreAndActions(allowedChainIds) const connector = f(actions) @@ -23,7 +27,44 @@ export function initializeConnector( const derivedHooks = getDerivedHooks(stateHooks) const augmentedHooks = getAugmentedHooks(connector, stateHooks, derivedHooks) - return [connector, { ...stateHooks, ...derivedHooks, ...augmentedHooks }] + return [connector, { ...stateHooks, ...derivedHooks, ...augmentedHooks }, store] +} + +export function useHighestPriorityConnector( + connectorHooksAndStores: ReturnType[] +): ReturnType { + // used to force re-renders + const [, setCounter] = useState(0) + const areActive = useRef(connectorHooksAndStores.map(([, , store]) => computeIsActive(store.getState()))) + + useEffect(() => { + const areActiveNew = connectorHooksAndStores.map(([, , store]) => computeIsActive(store.getState())) + // only re-render if necessary + if ( + areActiveNew.length !== areActive.current.length || + areActiveNew.some((isActive, i) => isActive !== areActive.current[i]) + ) { + areActive.current = areActiveNew + setCounter((counter) => ++counter) + } + + const unsubscribes = connectorHooksAndStores.map(([, , store], i) => + store.subscribe((state) => { + const isActive = computeIsActive(state) + if (isActive !== areActive.current[i]) { + areActive.current.splice(i, 1, isActive) + setCounter((counter) => ++counter) + } + }) + ) + + return () => { + unsubscribes.forEach((unsubscribe) => unsubscribe()) + } + }, [connectorHooksAndStores]) + + const firstActiveIndex = areActive.current.findIndex((isActive) => isActive) + return connectorHooksAndStores[firstActiveIndex === -1 ? 0 : firstActiveIndex] } const CHAIN_ID = (state: Web3ReactState) => state.chainId @@ -31,8 +72,7 @@ const ACCOUNTS = (state: Web3ReactState) => state.accounts const ACCOUNTS_EQUALITY_CHECKER: EqualityChecker = (oldAccounts, newAccounts) => (oldAccounts === undefined && newAccounts === undefined) || (oldAccounts !== undefined && - newAccounts !== undefined && - oldAccounts.length === newAccounts.length && + oldAccounts.length === newAccounts?.length && oldAccounts.every((oldAccount, i) => oldAccount === newAccounts[i])) const ACTIVATING = (state: Web3ReactState) => state.activating const ERROR = (state: Web3ReactState) => state.error @@ -68,7 +108,12 @@ function getDerivedHooks({ useChainId, useAccounts, useIsActivating, useError }: const activating = useIsActivating() const error = useError() - return Boolean(chainId && accounts && !activating && !error) + return computeIsActive({ + chainId, + accounts, + activating, + error, + }) } return { useAccount, useIsActive } diff --git a/packages/example/.env b/packages/example/.env new file mode 100644 index 000000000..6b54f787c --- /dev/null +++ b/packages/example/.env @@ -0,0 +1,3 @@ +INFURA_KEY=84842078b09946638c03157f83405213 +ALCHEMY_KEY=_gg7wSSi0KMBsdKnGVfHDueq6xMB9EkC +MAGIC_KEY=pk_live_1F99B3C570C9B08F diff --git a/packages/example/App.tsx b/packages/example/App.tsx index 17c817313..8e3c1c665 100644 --- a/packages/example/App.tsx +++ b/packages/example/App.tsx @@ -1,11 +1,11 @@ import { BigNumber } from '@ethersproject/bignumber' import { formatEther } from '@ethersproject/units' -import { Web3ReactHooks } from '@web3-react/core' +import type { Web3ReactHooks } from '@web3-react/core' import { Frame } from '@web3-react/frame' import { Magic } from '@web3-react/magic' import { MetaMask } from '@web3-react/metamask' import { Network } from '@web3-react/network' -import { Connector } from '@web3-react/types' +import type { Connector } from '@web3-react/types' import { WalletConnect } from '@web3-react/walletconnect' import { WalletLink } from '@web3-react/walletlink' import { useCallback, useEffect, useState } from 'react' @@ -92,14 +92,8 @@ function useBalances( return balances } -function Accounts({ - useAnyNetwork, - hooks: { useAccounts, useProvider, useENSNames }, -}: { - useAnyNetwork: boolean - hooks: Web3ReactHooks -}) { - const provider = useProvider(useAnyNetwork ? 'any' : undefined) +function Accounts({ hooks: { useAccounts, useProvider, useENSNames } }: { hooks: Web3ReactHooks }) { + const provider = useProvider() const accounts = useAccounts() const ENSNames = useENSNames(provider) @@ -340,7 +334,7 @@ function NetworkConnect({ } } -function Connect({ +function GenericConnect({ connector, hooks: { useIsActivating, useError, useIsActive }, }: { @@ -372,6 +366,18 @@ function Connect({ } } +function Connect({ connector, hooks }: { connector: Connector; hooks: Web3ReactHooks }) { + return connector instanceof Magic ? ( + + ) : connector instanceof MetaMask ? ( + + ) : connector instanceof Network ? ( + + ) : ( + + ) +} + export default function App() { return (
@@ -394,18 +400,10 @@ export default function App() {
- +
- {connector instanceof Magic ? ( - - ) : connector instanceof MetaMask ? ( - - ) : connector instanceof Network ? ( - - ) : ( - - )} + ))} diff --git a/packages/example/connectors/frame.ts b/packages/example/connectors/frame.ts index 4e447e724..53683eb1a 100644 --- a/packages/example/connectors/frame.ts +++ b/packages/example/connectors/frame.ts @@ -1,4 +1,4 @@ import { initializeConnector } from '@web3-react/core' import { Frame } from '@web3-react/frame' -export const [frame, hooks] = initializeConnector((actions) => new Frame(actions, undefined, false)) +export const [frame, hooks, store] = initializeConnector((actions) => new Frame(actions, undefined, false)) diff --git a/packages/example/connectors/index.ts b/packages/example/connectors/index.ts index 6b545d7b2..fe54aadc6 100644 --- a/packages/example/connectors/index.ts +++ b/packages/example/connectors/index.ts @@ -1,17 +1,27 @@ import type { Web3ReactHooks } from '@web3-react/core' -import type { Connector } from '@web3-react/types' -import { frame, hooks as frameHooks } from './frame' -import { hooks as magicHooks, magic } from './magic' -import { hooks as metaMaskHooks, metaMask } from './metaMask' -import { hooks as networkHooks, network } from './network' -import { hooks as walletConnectHooks, walletConnect } from './walletConnect' -import { hooks as walletLinkHooks, walletLink } from './walletLink' +import type { Frame } from '@web3-react/frame' +import type { Magic } from '@web3-react/magic' +import type { MetaMask } from '@web3-react/metamask' +import type { Network } from '@web3-react/network' +import type { Web3ReactStore } from '@web3-react/types' +import type { WalletConnect } from '@web3-react/walletconnect' +import type { WalletLink } from '@web3-react/walletlink' +import { frame, hooks as frameHooks, store as frameStore } from './frame' +import { hooks as magicHooks, magic, store as magicStore } from './magic' +import { hooks as metaMaskHooks, metaMask, store as metaMaskStore } from './metaMask' +import { hooks as networkHooks, network, store as networkStore } from './network' +import { hooks as walletConnectHooks, store as walletConnectStore, walletConnect } from './walletConnect' +import { hooks as walletLinkHooks, store as walletLinkStore, walletLink } from './walletLink' -export const connectors: [Connector, Web3ReactHooks][] = [ - [network, networkHooks], - [metaMask, metaMaskHooks], - [walletConnect, walletConnectHooks], - [walletLink, walletLinkHooks], - [frame, frameHooks], - [magic, magicHooks], +export const connectors: [ + MetaMask | WalletConnect | WalletLink | Network | Frame | Magic, + Web3ReactHooks, + Web3ReactStore +][] = [ + [metaMask, metaMaskHooks, metaMaskStore], + [walletConnect, walletConnectHooks, walletConnectStore], + [walletLink, walletLinkHooks, walletLinkStore], + [network, networkHooks, networkStore], + [frame, frameHooks, frameStore], + [magic, magicHooks, magicStore], ] diff --git a/packages/example/connectors/magic.ts b/packages/example/connectors/magic.ts index ad884cb34..3dac6c890 100644 --- a/packages/example/connectors/magic.ts +++ b/packages/example/connectors/magic.ts @@ -1,7 +1,7 @@ import { initializeConnector } from '@web3-react/core' import { Magic } from '@web3-react/magic' -export const [magic, hooks] = initializeConnector( +export const [magic, hooks, store] = initializeConnector( (actions) => new Magic(actions, { apiKey: process.env.magicKey, diff --git a/packages/example/connectors/metaMask.ts b/packages/example/connectors/metaMask.ts index d1fcb6d66..1e8d3cc46 100644 --- a/packages/example/connectors/metaMask.ts +++ b/packages/example/connectors/metaMask.ts @@ -1,4 +1,4 @@ import { initializeConnector } from '@web3-react/core' import { MetaMask } from '@web3-react/metamask' -export const [metaMask, hooks] = initializeConnector((actions) => new MetaMask(actions)) +export const [metaMask, hooks, store] = initializeConnector((actions) => new MetaMask(actions)) diff --git a/packages/example/connectors/network.ts b/packages/example/connectors/network.ts index d2c5dbca4..bb5e1df6e 100644 --- a/packages/example/connectors/network.ts +++ b/packages/example/connectors/network.ts @@ -2,7 +2,7 @@ import { initializeConnector } from '@web3-react/core' import { Network } from '@web3-react/network' import { URLS } from '../chains' -export const [network, hooks] = initializeConnector( +export const [network, hooks, store] = initializeConnector( (actions) => new Network(actions, URLS), Object.keys(URLS).map((chainId) => Number(chainId)) ) diff --git a/packages/example/connectors/walletConnect.ts b/packages/example/connectors/walletConnect.ts index 669b239e7..87697fdd9 100644 --- a/packages/example/connectors/walletConnect.ts +++ b/packages/example/connectors/walletConnect.ts @@ -2,7 +2,7 @@ import { initializeConnector } from '@web3-react/core' import { WalletConnect } from '@web3-react/walletconnect' import { URLS } from '../chains' -export const [walletConnect, hooks] = initializeConnector( +export const [walletConnect, hooks, store] = initializeConnector( (actions) => new WalletConnect(actions, { rpc: Object.keys(URLS).reduce((accumulator, chainId) => { diff --git a/packages/example/connectors/walletLink.ts b/packages/example/connectors/walletLink.ts index 4862a7a4c..f3e4f698b 100644 --- a/packages/example/connectors/walletLink.ts +++ b/packages/example/connectors/walletLink.ts @@ -2,7 +2,7 @@ import { initializeConnector } from '@web3-react/core' import { WalletLink } from '@web3-react/walletlink' import { URLS } from '../chains' -export const [walletLink, hooks] = initializeConnector( +export const [walletLink, hooks, store] = initializeConnector( (actions) => new WalletLink(actions, { url: URLS[1][0], diff --git a/packages/example/next.config.js b/packages/example/next.config.js index 556376e90..794804935 100644 --- a/packages/example/next.config.js +++ b/packages/example/next.config.js @@ -3,9 +3,9 @@ */ const nextConfig = { env: { - infuraKey: '84842078b09946638c03157f83405213', - alchemyKey: '_gg7wSSi0KMBsdKnGVfHDueq6xMB9EkC', - magicKey: 'pk_live_1F99B3C570C9B08F', + infuraKey: process.env.INFURA_KEY, + alchemyKey: process.env.ALCHEMY_KEY, + magicKey: process.env.MAGIC_KEY, }, webpack: (config) => { config.resolve.fallback = {