essefuga
Version:
A simple, maximally extensible, dependency minimized framework for building modern Ethereum dApps
250 lines (221 loc) • 8.29 kB
text/typescript
import { useReducer, useEffect, useCallback, useRef } from 'react'
import { ConnectorUpdate, ConnectorEvent } from '@web3-react/types'
import { AbstractConnector } from '@web3-react/abstract-connector'
import warning from 'tiny-warning'
import { Web3ReactManagerReturn } from './types'
import { normalizeChainId, normalizeAccount } from './normalizers'
class StaleConnectorError extends Error {
constructor() {
super()
this.name = this.constructor.name
}
}
export class UnsupportedChainIdError extends Error {
public constructor(unsupportedChainId: number, supportedChainIds?: readonly number[]) {
super()
this.name = this.constructor.name
this.message = `Unsupported chain id: ${unsupportedChainId}. Supported chain ids are: ${supportedChainIds}.`
}
}
interface Web3ReactManagerState {
connector?: AbstractConnector
provider?: any
chainId?: number
account?: null | string
onError?: (error: Error) => void
error?: Error
}
enum ActionType {
ACTIVATE_CONNECTOR,
UPDATE,
UPDATE_FROM_ERROR,
ERROR,
ERROR_FROM_ACTIVATION,
DEACTIVATE_CONNECTOR
}
interface Action {
type: ActionType
payload?: any
}
function reducer(state: Web3ReactManagerState, { type, payload }: Action): Web3ReactManagerState {
switch (type) {
case ActionType.ACTIVATE_CONNECTOR: {
const { connector, provider, chainId, account, onError } = payload
return { connector, provider, chainId, account, onError }
}
case ActionType.UPDATE: {
const { provider, chainId, account } = payload
return {
...state,
...(provider === undefined ? {} : { provider }),
...(chainId === undefined ? {} : { chainId }),
...(account === undefined ? {} : { account })
}
}
case ActionType.UPDATE_FROM_ERROR: {
const { provider, chainId, account } = payload
return {
...state,
...(provider === undefined ? {} : { provider }),
...(chainId === undefined ? {} : { chainId }),
...(account === undefined ? {} : { account }),
error: undefined
}
}
case ActionType.ERROR: {
const { error } = payload
const { connector, onError } = state
return {
connector,
error,
onError
}
}
case ActionType.ERROR_FROM_ACTIVATION: {
const { connector, error } = payload
return {
connector,
error
}
}
case ActionType.DEACTIVATE_CONNECTOR: {
return {}
}
}
}
async function augmentConnectorUpdate(
connector: AbstractConnector,
update: ConnectorUpdate
): Promise<ConnectorUpdate<number>> {
const provider = update.provider === undefined ? await connector.getProvider() : update.provider
const [_chainId, _account] = (await Promise.all([
update.chainId === undefined ? connector.getChainId() : update.chainId,
update.account === undefined ? connector.getAccount() : update.account
])) as [Required<ConnectorUpdate>['chainId'], Required<ConnectorUpdate>['account']]
const chainId = normalizeChainId(_chainId)
if (!!connector.supportedChainIds && !connector.supportedChainIds.includes(chainId)) {
throw new UnsupportedChainIdError(chainId, connector.supportedChainIds)
}
const account = _account === null ? _account : normalizeAccount(_account)
return { provider, chainId, account }
}
export function useWeb3ReactManager(): Web3ReactManagerReturn {
const [state, dispatch] = useReducer(reducer, {})
const { connector, provider, chainId, account, onError, error } = state
const updateBusterRef = useRef(-1)
updateBusterRef.current += 1
const activate = useCallback(
async (
connector: AbstractConnector,
onError?: (error: Error) => void,
throwErrors: boolean = false
): Promise<void> => {
const updateBusterInitial = updateBusterRef.current
let activated = false
try {
const update = await connector.activate().then(
(update): ConnectorUpdate => {
activated = true
return update
}
)
const augmentedUpdate = await augmentConnectorUpdate(connector, update)
if (updateBusterRef.current > updateBusterInitial) {
throw new StaleConnectorError()
}
dispatch({ type: ActionType.ACTIVATE_CONNECTOR, payload: { connector, ...augmentedUpdate, onError } })
} catch (error) {
if (error instanceof StaleConnectorError) {
activated && connector.deactivate()
warning(false, `Suppressed stale connector activation ${connector}`)
} else if (throwErrors) {
activated && connector.deactivate()
throw error
} else if (onError) {
activated && connector.deactivate()
onError(error)
} else {
// we don't call activated && connector.deactivate() here because it'll be handled in the useEffect
dispatch({ type: ActionType.ERROR_FROM_ACTIVATION, payload: { connector, error } })
}
}
},
[]
)
const setError = useCallback((error: Error): void => {
dispatch({ type: ActionType.ERROR, payload: { error } })
}, [])
const deactivate = useCallback((): void => {
dispatch({ type: ActionType.DEACTIVATE_CONNECTOR })
}, [])
const handleUpdate = useCallback(
async (update: ConnectorUpdate): Promise<void> => {
if (!connector) {
throw Error("This should never happen, it's just so Typescript stops complaining")
}
const updateBusterInitial = updateBusterRef.current
// updates are handled differently depending on whether the connector is active vs in an error state
if (!error) {
const chainId = update.chainId === undefined ? undefined : normalizeChainId(update.chainId)
if (chainId !== undefined && !!connector.supportedChainIds && !connector.supportedChainIds.includes(chainId)) {
const error = new UnsupportedChainIdError(chainId, connector.supportedChainIds)
onError ? onError(error) : dispatch({ type: ActionType.ERROR, payload: { error } })
} else {
const account = typeof update.account === 'string' ? normalizeAccount(update.account) : update.account
dispatch({ type: ActionType.UPDATE, payload: { provider: update.provider, chainId, account } })
}
} else {
try {
const augmentedUpdate = await augmentConnectorUpdate(connector, update)
if (updateBusterRef.current > updateBusterInitial) {
throw new StaleConnectorError()
}
dispatch({ type: ActionType.UPDATE_FROM_ERROR, payload: augmentedUpdate })
} catch (error) {
if (error instanceof StaleConnectorError) {
warning(false, `Suppressed stale connector update from error state ${connector} ${update}`)
} else {
// though we don't have to, we're re-circulating the new error
onError ? onError(error) : dispatch({ type: ActionType.ERROR, payload: { error } })
}
}
}
},
[connector, error, onError]
)
const handleError = useCallback(
(error: Error): void => {
onError ? onError(error) : dispatch({ type: ActionType.ERROR, payload: { error } })
},
[onError]
)
const handleDeactivate = useCallback((): void => {
dispatch({ type: ActionType.DEACTIVATE_CONNECTOR })
}, [])
// ensure that connectors which were set are deactivated
useEffect((): (() => void) => {
return () => {
if (connector) {
connector.deactivate()
}
}
}, [connector])
// ensure that events emitted from the set connector are handled appropriately
useEffect((): (() => void) => {
if (connector) {
connector
.on(ConnectorEvent.Update, handleUpdate)
.on(ConnectorEvent.Error, handleError)
.on(ConnectorEvent.Deactivate, handleDeactivate)
}
return () => {
if (connector) {
connector
.off(ConnectorEvent.Update, handleUpdate)
.off(ConnectorEvent.Error, handleError)
.off(ConnectorEvent.Deactivate, handleDeactivate)
}
}
}, [connector, handleUpdate, handleError, handleDeactivate])
return { connector, provider, chainId, account, activate, setError, deactivate, error }
}