Skip to content

Commit

Permalink
Merge pull request #54 from oasisprotocol/ml/network-errors
Browse files Browse the repository at this point in the history
Improve error handling
  • Loading branch information
lubej authored Mar 20, 2024
2 parents 7020d5e + 6c98fa0 commit f0bfd1c
Show file tree
Hide file tree
Showing 8 changed files with 125 additions and 43 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@ module.exports = {
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
indent: ['error', 2],
indent: ['error', 2, { SwitchCase: 1 }],
},
}
3 changes: 2 additions & 1 deletion frontend/src/components/ErrorBoundaryLayout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { LayoutBase } from '../LayoutBase'
import { StringUtils } from '../../utils/string.utils.ts'
import { Alert } from '../Alert'
import classes from './index.module.css'
import { toErrorString } from '../../utils/errors.ts'

interface Props {
error: unknown
Expand All @@ -11,7 +12,7 @@ interface Props {
export const ErrorBoundaryLayout: FC<Props> = ({ error }) => (
<LayoutBase>
<Alert className={classes.errorAlert} type="error">
{StringUtils.truncate((error as Error).message ?? JSON.stringify(error))}
{StringUtils.truncate(toErrorString(error as Error))}
</Alert>
</LayoutBase>
)
26 changes: 19 additions & 7 deletions frontend/src/pages/HomePage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { DateUtils } from '../../utils/date.utils.ts'
import { MascotChoices } from '../../types'
import { NumberUtils } from '../../utils/number.utils.ts'
import { CheckCircleIcon } from '../../components/icons/CheckCircleIcon.tsx'
import { toErrorString } from '../../utils/errors.ts'

export const HomePage: FC = () => {
const {
Expand Down Expand Up @@ -70,28 +71,39 @@ export const HomePage: FC = () => {
setSelectedChoice(choice)
}

/**
* Returns null in case user is not eligible to vote
*/
const handleCanVoteOnPoll = async () => {
const canVote = await canVoteOnPoll()

if (!canVote) {
setPageStatus('insufficient-balance')

return null
}
}

const handleVote = async () => {
if (selectedChoice === null) return

setIsLoading(true)

try {
const canVote = await canVoteOnPoll()
setPageStatus('loading')

if (!canVote) {
setPageStatus('insufficient-balance')
const canVote = await handleCanVoteOnPoll()
if (canVote === null) {
setIsLoading(false)
return
}

setPageStatus('loading')

await vote(selectedChoice)
setPreviousVoteForCurrentWallet(selectedChoice)

setPageStatus('success')
} catch (ex) {
console.error(ex)
setError((ex as Error).message ?? JSON.stringify(ex))
setError(toErrorString(ex as Error))

setPageStatus('error')
} finally {
Expand Down
12 changes: 9 additions & 3 deletions frontend/src/pages/ResultsPage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { useAppState } from '../../hooks/useAppState.ts'
import { DateUtils } from '../../utils/date.utils.ts'
import { useWeb3 } from '../../hooks/useWeb3.ts'
import { PollChoice } from '../../types'
import { toErrorString } from '../../utils/errors.ts'

interface PollChoiceWithValue extends PollChoice {
value: bigint
Expand All @@ -25,6 +26,7 @@ export const ResultsPage: FC = () => {
const { getVoteCounts } = useWeb3()
const {
state: { poll, isDesktopScreen, isMobileScreen },
setAppError,
} = useAppState()

const [voteCount, setVoteCount] = useState<bigint[]>([])
Expand All @@ -35,9 +37,13 @@ export const ResultsPage: FC = () => {
let shouldUpdate = true

const init = async () => {
const voteCountsResponse = await getVoteCounts()
if (shouldUpdate) {
setVoteCount(voteCountsResponse)
try {
const voteCountsResponse = (await getVoteCounts())!
if (shouldUpdate) {
setVoteCount(voteCountsResponse)
}
} catch (ex) {
setAppError(toErrorString(ex as Error))
}
}

Expand Down
46 changes: 22 additions & 24 deletions frontend/src/providers/AppStateProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { StorageKeys } from '../constants/storage-keys.ts'
import { MascotChoices } from '../types'
import { NumberUtils } from '../utils/number.utils.ts'
import { useMediaQuery } from 'react-responsive'
import { toErrorString } from '../utils/errors.ts'

const localStorageStore = storage()

Expand Down Expand Up @@ -62,20 +63,27 @@ export const AppStateContextProvider: FC<PropsWithChildren> = ({ children }) =>
if (!isVoidSignerConnected) return

const init = async () => {
const poll = await getPoll()
const {
params: { numChoices },
} = poll

if (numChoices !== 3n) {
console.warn('[numChoices] Unexpected number of poll choices, this dApp may not behave as expected!')
try {
const poll = (await getPoll())!

const {
params: { numChoices },
} = poll

if (numChoices !== 3n) {
console.warn(
'[numChoices] Unexpected number of poll choices, this dApp may not behave as expected!'
)
}

setState(prevState => ({
...prevState,
isInitialLoading: false,
poll,
}))
} catch (ex) {
setAppError(toErrorString(ex as Error))
}

setState(prevState => ({
...prevState,
isInitialLoading: false,
poll,
}))
}

init()
Expand All @@ -98,19 +106,9 @@ export const AppStateContextProvider: FC<PropsWithChildren> = ({ children }) =>
const setAppError = (error: Error | object | string) => {
if (error === undefined || error === null) return

let appError = ''

if (Object.prototype.hasOwnProperty.call(error, 'message')) {
appError = (error as Error).message
} else if (typeof error === 'object') {
appError = JSON.stringify(appError)
} else {
appError = error
}

setState(prevState => ({
...prevState,
appError,
appError: toErrorString(error as Error),
}))
}

Expand Down
4 changes: 2 additions & 2 deletions frontend/src/providers/Web3Context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ export interface Web3ProviderContext {
getBalance: () => Promise<bigint>
getTransaction: (txHash: string) => Promise<TransactionResponse | null>
isProviderAvailable: () => Promise<boolean>
getPoll: () => Promise<DefaultReturnType<[Poll]>>
getPoll: () => Promise<DefaultReturnType<[Poll]> | void>
canVoteOnPoll: () => Promise<boolean>
vote: (choiceId: BigNumberish) => Promise<TransactionResponse | null>
getVoteCounts: () => Promise<bigint[]>
getVoteCounts: () => Promise<bigint[] | void>
}

export const Web3Context = createContext<Web3ProviderContext>({} as Web3ProviderContext)
20 changes: 15 additions & 5 deletions frontend/src/providers/Web3Provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ import {
VITE_PROPOSAL_ID,
VITE_WEB3_GATEWAY,
} from '../constants/config'
import { UnknownNetworkError } from '../utils/errors'
import {
handleKnownContractCallExceptionErrors,
handleKnownErrors,
handleKnownEthersErrors,
UnknownNetworkError,
} from '../utils/errors'
import { Web3Context, Web3ProviderContext, Web3ProviderState } from './Web3Context'
import { useEIP1193 } from '../hooks/useEIP1193.ts'
import { BigNumberish, BrowserProvider, JsonRpcProvider, toBeHex } from 'ethers'
Expand Down Expand Up @@ -210,7 +215,7 @@ export const Web3ContextProvider: FC<PropsWithChildren> = ({ children }) => {
throw new Error('[pollManagerWithoutSigner] not initialized!')
}

return await pollManagerVoidSigner.PROPOSALS(toBeHex(VITE_PROPOSAL_ID))
return await pollManagerVoidSigner.PROPOSALS(toBeHex(VITE_PROPOSAL_ID)).catch(handleKnownErrors)
}

const canVoteOnPoll = async () => {
Expand All @@ -227,7 +232,12 @@ export const Web3ContextProvider: FC<PropsWithChildren> = ({ children }) => {
return await pollManagerVoidSigner
.canVoteOnPoll(VITE_PROPOSAL_ID, account, EMPTY_IN_DATA)
.then(canVoteBigint => Promise.resolve(canVoteBigint === 1n))
.catch(() => Promise.resolve(false))
.catch(ex => {
handleKnownErrors(ex)
handleKnownContractCallExceptionErrors(ex, Promise.resolve(false))

return Promise.resolve(false)
})
}

const vote = async (choiceId: BigNumberish) => {
Expand All @@ -244,7 +254,7 @@ export const Web3ContextProvider: FC<PropsWithChildren> = ({ children }) => {
unsignedTx.gasLimit = MAX_GAS_LIMIT
unsignedTx.value = 0n

const txResponse = await signer.sendTransaction(unsignedTx)
const txResponse = await signer.sendTransaction(unsignedTx).catch(handleKnownEthersErrors)

return await getTransaction(txResponse.hash)
}
Expand All @@ -256,7 +266,7 @@ export const Web3ContextProvider: FC<PropsWithChildren> = ({ children }) => {
throw new Error('[pollManagerVoidSigner] not initialized!')
}

return await pollManagerVoidSigner.getVoteCounts(VITE_PROPOSAL_ID)
return await pollManagerVoidSigner.getVoteCounts(VITE_PROPOSAL_ID).catch(handleKnownErrors)
}

const providerState: Web3ProviderContext = {
Expand Down
55 changes: 55 additions & 0 deletions frontend/src/utils/errors.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import { CallExceptionError, EthersError } from 'ethers'

const NETWORK_ERROR_MESSAGE = 'Unable to connect to RPC node! Please check your internet connection.'

export class UnknownNetworkError extends Error {
constructor(message: string) {
super(message)
Expand All @@ -7,3 +11,54 @@ export class UnknownNetworkError extends Error {
export interface EIP1193Error extends Error {
code: number
}

export const handleKnownErrors = (error: Error): void => {
const errorMessage = (error?.message ?? '').toLowerCase()

switch (errorMessage) {
case 'failed to fetch':
throw new Error(NETWORK_ERROR_MESSAGE)
}
}

export const handleKnownContractCallExceptionErrors = <T = unknown>(
callExceptionError: CallExceptionError,
defaultReturn: Promise<T>
) => {
const reason = callExceptionError?.reason ?? ''

switch (reason) {
// Contract call reverted
case 'require(false)': {
return defaultReturn
}
}
}

export const handleKnownEthersErrors = (error: EthersError) => {
const errorCode = error?.code ?? ''

switch (errorCode) {
case 'ACTION_REJECTED':
throw new Error('User rejected action, please try again.')
case 'NETWORK_ERROR':
case 'TIMEOUT':
throw new Error(NETWORK_ERROR_MESSAGE)
}
// Default to short message
throw new Error(error.shortMessage)
}

export const toErrorString = (error: Error = new Error('Unknown error')) => {
let errorString = ''

if (Object.prototype.hasOwnProperty.call(error, 'message')) {
errorString = (error as Error).message
} else if (typeof error === 'object') {
errorString = JSON.stringify(errorString)
} else {
errorString = error
}

return errorString
}

0 comments on commit f0bfd1c

Please sign in to comment.