diff --git a/packages/react-components/src/Status/Queue.tsx b/packages/react-components/src/Status/Queue.tsx index b1ca322da1..29edcb5a0a 100644 --- a/packages/react-components/src/Status/Queue.tsx +++ b/packages/react-components/src/Status/Queue.tsx @@ -13,6 +13,7 @@ import { SubmittableResult } from '@polkadot/api'; import jsonrpc from '@polkadot/types/interfaces/jsonrpc'; import { getContractAbi } from '../util'; +import { isIncompleteEvent } from './checks'; import { STATUS_COMPLETE } from './constants'; import { QueueProvider } from './Context'; @@ -36,7 +37,10 @@ function mergeStatus (status: ActionStatusPartial[]): ActionStatus[] { const initial = status .reduce((result: StatusCount[], status): StatusCount[] => { - const prev = result.find(({ status: prev }) => prev.action === status.action && prev.status === status.status); + const prev = result.find(({ status: prev }) => + prev.action === status.action && + prev.status === status.status + ); if (prev) { prev.count++; @@ -82,8 +86,13 @@ function extractEvents (result?: SubmittableResult): ActionStatus[] { ((result && result.events) || []) // filter events handled globally, or those we are not interested in, these are // handled by the global overview, so don't add them here - .filter((record): boolean => !!record.event && record.event.section !== 'democracy') - .map(({ event: { data, method, section } }): ActionStatusPartial => { + .filter((record) => + !!record.event && + record.event.section !== 'democracy' + ) + .map((record): ActionStatusPartial => { + const { event: { data, method, section } } = record; + if (section === 'system' && method === 'ExtrinsicFailed') { const [dispatchError] = data as unknown as ITuple<[DispatchError]>; let message = dispatchError.type; @@ -133,6 +142,12 @@ function extractEvents (result?: SubmittableResult): ActionStatus[] { message: 'contract evicted', status: 'error' }; + } else if (isIncompleteEvent(record)) { + return { + action: `${section}.${method}`, + message: 'imcomplete execution', + status: 'eventWarn' + }; } } @@ -198,7 +213,7 @@ function Queue ({ children }: Props): React.ReactElement { ...(status.map((item): QueueStatus => { const id = ++nextId; const removeItem = (): void => - setStQueue([...stRef.current.filter((item): boolean => item.id !== id)]); + setStQueue([...stRef.current.filter((item) => item.id !== id)]); setTimeout(removeItem, REMOVE_TIMEOUT); @@ -265,7 +280,7 @@ function Queue ({ children }: Props): React.ReactElement { if (STATUS_COMPLETE.includes(status)) { setTimeout((): void => { - const item = txRef.current.find((item): boolean => item.id === id); + const item = txRef.current.find((item) => item.id === id); item && item.removeItem(); }, REMOVE_TIMEOUT); diff --git a/packages/react-components/src/Status/checks.ts b/packages/react-components/src/Status/checks.ts new file mode 100644 index 0000000000..7c920cd6ef --- /dev/null +++ b/packages/react-components/src/Status/checks.ts @@ -0,0 +1,60 @@ +// Copyright 2017-2022 @polkadot/react-components authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { Result } from '@polkadot/types'; +import type { Event, EventRecord } from '@polkadot/types/interfaces'; +import type { XcmV2TraitsOutcome } from '@polkadot/types/lookup'; +import type { Codec } from '@polkadot/types/types'; + +type EventCheck = (event: Event) => boolean; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +function acceptAll (_: Event): boolean { + return true; +} + +function dispatchResult ({ data: [result] }: Event): boolean { + return !result || (result as Result).isErr; +} + +// [approving, timepoint, multisig, callHash, result] +function dispatchResultMulti ({ data: [,,,, result] }: Event): boolean { + return !result || (result as Result).isErr; +} + +function xcmAttempted ({ data: [outcome] }: Event): boolean { + return !outcome || (outcome as XcmV2TraitsOutcome).isIncomplete; +} + +const xcmPallet: Record = { + Attempted: xcmAttempted +}; + +const CHECKS: Record> = { + multisig: { + MultisigExecuted: dispatchResultMulti + }, + polkadotXcm: xcmPallet, + proxy: { + ProxyExecuted: dispatchResult + }, + sudo: { + Sudid: dispatchResult, + SudoAsDone: dispatchResult + }, + utility: { + BatchInterrupted: acceptAll, + DispatchedAs: dispatchResult + }, + xcmPallet +}; + +export function isIncompleteEvent ({ event }: EventRecord): boolean { + const { method, section } = event; + + return ( + !!CHECKS[section] && + !!CHECKS[section][method] && + CHECKS[section][method](event) + ); +} diff --git a/packages/react-components/src/Status/index.tsx b/packages/react-components/src/Status/index.tsx index 28dd5a7c09..4c6f36f82b 100644 --- a/packages/react-components/src/Status/index.tsx +++ b/packages/react-components/src/Status/index.tsx @@ -25,6 +25,7 @@ function iconName (status: string): IconName { return 'ban'; case 'event': + case 'eventWarn': return 'assistive-listening-systems'; case 'received': @@ -284,6 +285,10 @@ export default React.memo(styled(Status)` background: teal; } + &.eventWarn > .wraper > .container { + background: orange; + } + &.completed, &.finalized, &.inblock, diff --git a/packages/react-components/src/Status/types.ts b/packages/react-components/src/Status/types.ts index 00da3ef72f..71f71536d5 100644 --- a/packages/react-components/src/Status/types.ts +++ b/packages/react-components/src/Status/types.ts @@ -12,7 +12,7 @@ export type Actions = 'create' | 'edit' | 'restore' | 'forget' | 'backup' | 'cha export interface ActionStatusBase { account?: AccountId | Address | string; message?: string; - status: 'error' | 'event' | 'queued' | 'received' | 'success'; + status: 'error' | 'event' | 'eventWarn' | 'queued' | 'received' | 'success'; } export interface ActionStatusPartial extends ActionStatusBase {