Skip to content

Commit

Permalink
playing around with useHighestPriorityConnector (#355)
Browse files Browse the repository at this point in the history
* first pass at useHighestPriorityConnector

* prefix increment

* some cleanup

* add some tests
  • Loading branch information
NoahZinsmeister committed Jan 13, 2022
1 parent dfe6669 commit 0eeeadf
Show file tree
Hide file tree
Showing 13 changed files with 198 additions and 53 deletions.
1 change: 1 addition & 0 deletions .gitignore
@@ -1,3 +1,4 @@
node_modules/
packages/*/dist/
packages/example/.next/
.env.local
92 changes: 90 additions & 2 deletions 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) {
Expand All @@ -18,6 +18,8 @@ class MockConnector extends Connector {
}
}

class MockConnector2 extends MockConnector {}

describe('#initializeConnector', () => {
let connector: MockConnector
let hooks: Web3ReactHooks
Expand Down Expand Up @@ -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)
})
})
59 changes: 52 additions & 7 deletions packages/core/src/index.ts
@@ -1,19 +1,23 @@
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'

export type Web3ReactHooks = ReturnType<typeof getStateHooks> &
ReturnType<typeof getDerivedHooks> &
ReturnType<typeof getAugmentedHooks>

function computeIsActive({ chainId, accounts, activating, error }: Web3ReactState) {
return Boolean(chainId && accounts && !activating && !error)
}

export function initializeConnector<T extends Connector>(
f: (actions: Actions) => T,
allowedChainIds?: number[]
): [T, Web3ReactHooks] {
): [T, Web3ReactHooks, Web3ReactStore] {
const [store, actions] = createWeb3ReactStoreAndActions(allowedChainIds)

const connector = f(actions)
Expand All @@ -23,16 +27,52 @@ export function initializeConnector<T extends Connector>(
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<typeof initializeConnector>[]
): ReturnType<typeof initializeConnector> {
// used to force re-renders
const [, setCounter] = useState(0)
const areActive = useRef<boolean[]>(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
const ACCOUNTS = (state: Web3ReactState) => state.accounts
const ACCOUNTS_EQUALITY_CHECKER: EqualityChecker<Web3ReactState['accounts']> = (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
Expand Down Expand Up @@ -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 }
Expand Down
3 changes: 3 additions & 0 deletions packages/example/.env
@@ -0,0 +1,3 @@
INFURA_KEY=84842078b09946638c03157f83405213
ALCHEMY_KEY=_gg7wSSi0KMBsdKnGVfHDueq6xMB9EkC
MAGIC_KEY=pk_live_1F99B3C570C9B08F
40 changes: 19 additions & 21 deletions 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'
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -340,7 +334,7 @@ function NetworkConnect({
}
}

function Connect({
function GenericConnect({
connector,
hooks: { useIsActivating, useError, useIsActive },
}: {
Expand Down Expand Up @@ -372,6 +366,18 @@ function Connect({
}
}

function Connect({ connector, hooks }: { connector: Connector; hooks: Web3ReactHooks }) {
return connector instanceof Magic ? (
<MagicConnect connector={connector} hooks={hooks} />
) : connector instanceof MetaMask ? (
<MetaMaskConnect connector={connector} hooks={hooks} />
) : connector instanceof Network ? (
<NetworkConnect connector={connector} hooks={hooks} />
) : (
<GenericConnect connector={connector} hooks={hooks} />
)
}

export default function App() {
return (
<div style={{ display: 'flex', flexFlow: 'wrap', fontFamily: 'sans-serif' }}>
Expand All @@ -394,18 +400,10 @@ export default function App() {
<Status connector={connector} hooks={hooks} />
<br />
<ChainId hooks={hooks} />
<Accounts useAnyNetwork={connector instanceof WalletConnect} hooks={hooks} />
<Accounts hooks={hooks} />
<br />
</div>
{connector instanceof Magic ? (
<MagicConnect connector={connector} hooks={hooks} />
) : connector instanceof MetaMask ? (
<MetaMaskConnect connector={connector} hooks={hooks} />
) : connector instanceof Network ? (
<NetworkConnect connector={connector} hooks={hooks} />
) : (
<Connect connector={connector} hooks={hooks} />
)}
<Connect connector={connector} hooks={hooks} />
</div>
))}
</div>
Expand Down
2 changes: 1 addition & 1 deletion 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<Frame>((actions) => new Frame(actions, undefined, false))
export const [frame, hooks, store] = initializeConnector<Frame>((actions) => new Frame(actions, undefined, false))
38 changes: 24 additions & 14 deletions 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],
]
2 changes: 1 addition & 1 deletion 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<Magic>(
export const [magic, hooks, store] = initializeConnector<Magic>(
(actions) =>
new Magic(actions, {
apiKey: process.env.magicKey,
Expand Down
2 changes: 1 addition & 1 deletion 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<MetaMask>((actions) => new MetaMask(actions))
export const [metaMask, hooks, store] = initializeConnector<MetaMask>((actions) => new MetaMask(actions))
2 changes: 1 addition & 1 deletion packages/example/connectors/network.ts
Expand Up @@ -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<Network>(
export const [network, hooks, store] = initializeConnector<Network>(
(actions) => new Network(actions, URLS),
Object.keys(URLS).map((chainId) => Number(chainId))
)
2 changes: 1 addition & 1 deletion packages/example/connectors/walletConnect.ts
Expand Up @@ -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<WalletConnect>(
export const [walletConnect, hooks, store] = initializeConnector<WalletConnect>(
(actions) =>
new WalletConnect(actions, {
rpc: Object.keys(URLS).reduce((accumulator, chainId) => {
Expand Down
2 changes: 1 addition & 1 deletion packages/example/connectors/walletLink.ts
Expand Up @@ -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<WalletLink>(
export const [walletLink, hooks, store] = initializeConnector<WalletLink>(
(actions) =>
new WalletLink(actions, {
url: URLS[1][0],
Expand Down
6 changes: 3 additions & 3 deletions packages/example/next.config.js
Expand Up @@ -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 = {
Expand Down

1 comment on commit 0eeeadf

@vercel
Copy link

@vercel vercel bot commented on 0eeeadf Jan 13, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.