diff --git a/src/app/components/ErrorFormatter/index.tsx b/src/app/components/ErrorFormatter/index.tsx index 21bd0052cf..8f64941594 100644 --- a/src/app/components/ErrorFormatter/index.tsx +++ b/src/app/components/ErrorFormatter/index.tsx @@ -81,6 +81,7 @@ export function ErrorFormatter(props: Props) { indexerName: backendToLabel[backend()], }, ), + [WalletErrors.DisconnectedError]: t('errors.disconnectedError', 'Lost connection.'), } const error = errorMap[props.code] diff --git a/src/app/pages/AccountPage/Features/TransactionHistory/index.tsx b/src/app/pages/AccountPage/Features/TransactionHistory/index.tsx index 23026b0822..ab89955f1f 100644 --- a/src/app/pages/AccountPage/Features/TransactionHistory/index.tsx +++ b/src/app/pages/AccountPage/Features/TransactionHistory/index.tsx @@ -15,6 +15,7 @@ import { selectTransactionsError, } from '../../../../state/account/selectors' import { selectSelectedNetwork } from 'app/state/network/selectors' +import { ErrorFormatter } from 'app/components/ErrorFormatter' interface Props {} @@ -35,11 +36,8 @@ export function TransactionHistory(props: Props) { {transactionsError && (

- {t( - 'account.transaction.loadingError', - "Couldn't load transactions. List may be empty or out of date.", - )}{' '} - {transactionsError} + {t('account.transaction.loadingError', "Couldn't load transactions.")}{' '} +

)} {allTransactions.length ? ( diff --git a/src/app/pages/AccountPage/index.tsx b/src/app/pages/AccountPage/index.tsx index e15ecbaeee..87f9983082 100644 --- a/src/app/pages/AccountPage/index.tsx +++ b/src/app/pages/AccountPage/index.tsx @@ -3,6 +3,7 @@ * AccountPage * */ +import { AlertBox } from 'app/components/AlertBox' import { ErrorFormatter } from 'app/components/ErrorFormatter' import { TransactionModal } from 'app/components/TransactionModal' import { TransitionRoute } from 'app/components/TransitionRoute' @@ -116,19 +117,19 @@ export function AccountPage(props: Props) { )} {account.accountError && ( -

- {t('account.loadingError', "Couldn't load account. Information may be missing or out of date.")}{' '} - {account.accountError} -

+ + {t('account.loadingError', "Couldn't load account.")}{' '} + + )} {stake.updateDelegationsError && ( -

+ {t('delegations.loadingError', "Couldn't load delegations.")}{' '} -

+ )} {address && address !== '' && ( <> diff --git a/src/app/pages/StakingPage/Features/ValidatorList/index.tsx b/src/app/pages/StakingPage/Features/ValidatorList/index.tsx index f900a9b648..551cb015b7 100644 --- a/src/app/pages/StakingPage/Features/ValidatorList/index.tsx +++ b/src/app/pages/StakingPage/Features/ValidatorList/index.tsx @@ -4,6 +4,7 @@ * */ import { AmountFormatter } from 'app/components/AmountFormatter' +import { ErrorFormatter } from 'app/components/ErrorFormatter' import { ShortAddress } from 'app/components/ShortAddress' import { ValidatorStatus } from 'app/pages/StakingPage/Features/ValidatorList/ValidatorStatus' import { stakingActions } from 'app/state/staking' @@ -112,7 +113,9 @@ export const ValidatorList = memo((props: Props) => { staleTimestamp: new Date(validatorsTimestamp!).toLocaleString(), })}
- {updateValidatorsError} + {validators.length <= 0 && ( + + )}

)} ) {}, accountLoaded(state, action: PayloadAction) { - state.accountError = null + state.accountError = undefined Object.assign(state, action.payload) }, - accountError(state, action: PayloadAction) { + accountError(state, action: PayloadAction) { state.accountError = action.payload }, transactionsLoaded(state, action: PayloadAction) { - state.transactionsError = null + state.transactionsError = undefined state.transactions = action.payload }, - transactionsError(state, action: PayloadAction) { + transactionsError(state, action: PayloadAction) { state.transactionsError = action.payload }, setLoading(state, action: PayloadAction) { diff --git a/src/app/state/account/saga.ts b/src/app/state/account/saga.ts index d9b7f06b93..d4116c4bc2 100644 --- a/src/app/state/account/saga.ts +++ b/src/app/state/account/saga.ts @@ -1,6 +1,7 @@ import { PayloadAction } from '@reduxjs/toolkit' import { addressToPublicKey, parseRpcBalance } from 'app/lib/helpers' import { all, call, fork, join, put, select, take, takeLatest } from 'typed-redux-saga' +import { WalletError, WalletErrors } from 'types/errors' import { accountActions as actions } from '.' import { getExplorerAPIs, getOasisNic } from '../network/saga' @@ -27,7 +28,7 @@ export function* loadAccount(action: PayloadAction) { try { const account = yield* call(getAccount, address) yield put(actions.accountLoaded(account)) - } catch (apiError) { + } catch (apiError: any) { console.error('get account failed, continuing to RPC fallback.', apiError) try { const account = yield* call([nic, nic.stakingAccount], { owner: publicKey, height: 0 }) @@ -35,8 +36,16 @@ export function* loadAccount(action: PayloadAction) { yield put(actions.accountLoaded({ address, liquid_balance: parseFloat(balance.available) })) } catch (rpcError) { console.error('get account with RPC failed, continuing without updated account.', rpcError) - yield put(actions.accountError('' + apiError)) - return + if (apiError instanceof WalletError) { + yield* put(actions.accountError({ code: apiError.type, message: apiError.message })) + } else { + yield* put( + actions.accountError({ + code: WalletErrors.UnknownError, + message: apiError.message, + }), + ) + } } } }), @@ -49,10 +58,13 @@ export function* loadAccount(action: PayloadAction) { limit: 20, }) yield put(actions.transactionsLoaded(transactions)) - } catch (e) { + } catch (e: any) { console.error('get transactions list failed, continuing without updated list.', e) - yield put(actions.transactionsError('' + e)) - return + if (e instanceof WalletError) { + yield* put(actions.transactionsError({ code: e.type, message: e.message })) + } else { + yield* put(actions.transactionsError({ code: WalletErrors.UnknownError, message: e.message })) + } } }), ), diff --git a/src/app/state/account/types.ts b/src/app/state/account/types.ts index bc578f19a3..793e81a91a 100644 --- a/src/app/state/account/types.ts +++ b/src/app/state/account/types.ts @@ -1,4 +1,5 @@ import { Transaction } from 'app/state/transaction/types' +import { ErrorPayload } from 'types/errors' export interface BalanceDetails { total: number @@ -15,7 +16,7 @@ export interface Account { /* --- STATE --- */ export interface AccountState extends Account { loading: boolean - accountError: string | null + accountError?: ErrorPayload transactions: Transaction[] - transactionsError: string | null + transactionsError?: ErrorPayload } diff --git a/src/app/state/staking/index.ts b/src/app/state/staking/index.ts index ad0f28dd2a..8ff4fdc5e8 100644 --- a/src/app/state/staking/index.ts +++ b/src/app/state/staking/index.ts @@ -9,7 +9,7 @@ export const initialState: StakingState = { delegations: [], updateDelegationsError: undefined, validators: null, - updateValidatorsError: null, + updateValidatorsError: undefined, selectedValidatorDetails: null, selectedValidator: null, loading: false, @@ -24,10 +24,10 @@ const slice = createSlice({ }, fetchAccount(state, action: PayloadAction) {}, updateValidators(state, action: PayloadAction) { - state.updateValidatorsError = null + state.updateValidatorsError = undefined state.validators = action.payload }, - updateValidatorsError(state, action: PayloadAction<{ error: string; validators: Validators }>) { + updateValidatorsError(state, action: PayloadAction<{ error: ErrorPayload; validators: Validators }>) { state.updateValidatorsError = action.payload.error state.validators = action.payload.validators }, diff --git a/src/app/state/staking/saga.test.ts b/src/app/state/staking/saga.test.ts index 08046fa6e9..c8a866dc82 100644 --- a/src/app/state/staking/saga.test.ts +++ b/src/app/state/staking/saga.test.ts @@ -4,6 +4,7 @@ import * as matchers from 'redux-saga-test-plan/matchers' import { EffectProviders, StaticProvider } from 'redux-saga-test-plan/providers' import { select } from 'redux-saga/effects' import { RootState } from 'types' +import { WalletError, WalletErrors } from 'types/errors' import { initialState, stakingActions, stakingReducer } from '.' import { getExplorerAPIs, getOasisNic } from '../network/saga' @@ -144,7 +145,7 @@ describe('Staking Sagas', () => { }) it('should use fallback on mainnet', () => { - getAllValidators.mockRejectedValue('apiFailed') + getAllValidators.mockRejectedValue(new WalletError(WalletErrors.IndexerAPIError, 'Request failed')) const getMainnetDumpValidatorsMock = { dump_timestamp: 1647996761337, dump_timestamp_iso: '2022-03-23T00:52:41.337Z', @@ -185,7 +186,10 @@ describe('Staking Sagas', () => { .provide([...providers, [matchers.call.fn(getMainnetDumpValidators), getMainnetDumpValidatorsMock]]) .put( stakingActions.updateValidatorsError({ - error: 'apiFailed', + error: { + code: WalletErrors.IndexerAPIError, + message: 'Request failed', + }, validators: { timestamp: getMainnetDumpValidatorsMock.dump_timestamp, network: 'mainnet', diff --git a/src/app/state/staking/saga.ts b/src/app/state/staking/saga.ts index 55d1a5ecbb..11995b982e 100644 --- a/src/app/state/staking/saga.ts +++ b/src/app/state/staking/saga.ts @@ -62,20 +62,23 @@ export function* refreshValidators() { list: validators, }), ) - } catch (errorApi) { + } catch (errorApi: any) { console.error('get validators list failed', errorApi) - const fallback = yield* call(getFallbackValidators, network, '' + errorApi) + const fallback = yield* call(getFallbackValidators, network, errorApi) yield* put( stakingActions.updateValidatorsError({ - error: fallback.error, + error: { + code: fallback.error instanceof WalletError ? fallback.error.type : WalletErrors.UnknownError, + message: fallback.error.message, + }, validators: fallback.validators, }), ) } } -function* getFallbackValidators(network: NetworkType, errorApi: string) { +function* getFallbackValidators(network: NetworkType, errorApi: Error) { let fallbackValidators: Validators = { timestamp: yield* call(now), network: network, @@ -104,7 +107,7 @@ function* getFallbackValidators(network: NetworkType, errorApi: string) { } catch (errorDumpValidators) { // If fetching dump_validators fails, fall back to empty list return { - error: 'Lost connection', + error: new WalletError(WalletErrors.DisconnectedError, 'Lost connection'), validators: fallbackValidators, } } diff --git a/src/app/state/staking/types.ts b/src/app/state/staking/types.ts index cea487c27e..512e26f49a 100644 --- a/src/app/state/staking/types.ts +++ b/src/app/state/staking/types.ts @@ -60,7 +60,7 @@ export interface StakingState { validators: Validators | null /** Error from last attempt to update our list of validators */ - updateValidatorsError: string | null + updateValidatorsError?: ErrorPayload /** List of active delegations for the selected account */ delegations: Delegation[] diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 0317c7b658..a23c6e5c81 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -12,7 +12,7 @@ "delegate": "Delegate" }, "loading": "Loading account", - "loadingError": "Couldn't load account. Information may be missing or out of date.", + "loadingError": "Couldn't load account.", "otherTransaction": { "designation": "Other address", "header": "Unrecognized transaction, method '{{method}}'" @@ -54,7 +54,7 @@ "received": "Received <0> delegation in escrow", "sent": "Delegated <0> to validator" }, - "loadingError": "Couldn't load transactions. List may be empty or out of date.", + "loadingError": "Couldn't load transactions.", "reclaimEscrow": { "received": "<0> reclaimed by delegator", "sent": "Reclaimed <0> from validator" @@ -106,6 +106,7 @@ "errors": { "LedgerOasisAppIsNotOpen": "Oasis App on Ledger is closed.", "cannotSendToSelf": "Cannot send to yourself", + "disconnectedError": "Lost connection.", "duplicateTransaction": "Duplicate transaction", "indexerAPIError": "{{indexerName}} appears to be down, some information may be missing or out of date. Please, come back later.", "insufficientBalance": "Insufficient balance", diff --git a/src/types/errors.ts b/src/types/errors.ts index c39f29f0fc..5e51ed8df4 100644 --- a/src/types/errors.ts +++ b/src/types/errors.ts @@ -23,6 +23,7 @@ export enum WalletErrors { LedgerTransactionRejected = 'transaction_rejected', LedgerAppVersionNotSupported = 'ledger_version_not_supported', IndexerAPIError = 'indexer_api_error', + DisconnectedError = 'disconnected_error', } export interface ErrorPayload {