From 0781460bb45271aec8fa42c30d3ee6ba6735d553 Mon Sep 17 00:00:00 2001 From: Andreea Eftene Date: Sun, 9 Jan 2022 17:54:08 +0100 Subject: [PATCH 1/9] fix dependency cycle --- src/ui/components/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ui/components/index.ts b/src/ui/components/index.ts index 88c4faa5..3c6bef60 100644 --- a/src/ui/components/index.ts +++ b/src/ui/components/index.ts @@ -9,4 +9,3 @@ export * from './homepage'; export * from './instantiate'; export * from './message'; export * from './sidebar'; -export * as default from './App'; From 62b9fc55d03cf96ebc4416e964364a5718d5afae Mon Sep 17 00:00:00 2001 From: Andreea Eftene Date: Sun, 9 Jan 2022 18:05:35 +0100 Subject: [PATCH 2/9] use transaction context in interact component --- src/api/contract/call.ts | 92 +---------- src/types/ui/contexts.ts | 2 +- src/ui/components/contract/Interact.tsx | 197 +++++++++++++++++++----- src/ui/components/form/index.ts | 1 + src/ui/contexts/InstantiateContext.tsx | 2 +- src/ui/contexts/TransactionsContext.tsx | 15 +- src/ui/hooks/index.ts | 5 + src/ui/hooks/useWeight.ts | 1 - 8 files changed, 181 insertions(+), 134 deletions(-) diff --git a/src/api/contract/call.ts b/src/api/contract/call.ts index 40fa4305..a000e670 100644 --- a/src/api/contract/call.ts +++ b/src/api/contract/call.ts @@ -1,24 +1,22 @@ // Copyright 2021 @paritytech/substrate-contracts-explorer authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { BN_ZERO } from '@polkadot/util'; -import { encodeSalt, transformUserInput } from 'api/util'; +import { transformUserInput } from 'api/util'; import { BlueprintOptions, ContractQuery, ContractOptions, ContractTx, - ContractCallParams, - CallResult, - RegistryError, KeyringPair, ContractDryRunParams, } from 'types'; -let nextId = 0; - -function prepareContractTx(tx: ContractTx<'promise'>, options: BlueprintOptions, args: unknown[]) { +export function prepareContractTx( + tx: ContractTx<'promise'>, + options: BlueprintOptions, + args: unknown[] +) { return args.length > 0 ? tx(options, ...args) : tx(options); } @@ -50,81 +48,3 @@ export function dryRun({ transformed ); } - -export async function call({ - contract, - message, - payment: value, - gasLimit, - sender, - argValues, - dispatch, -}: ContractCallParams) { - const salt = encodeSalt(); - const transformed = transformUserInput(contract.registry, message.args, argValues); - - const callResult: CallResult = { - id: ++nextId, - isComplete: false, - data: '', - log: [], - message, - time: Date.now(), - }; - - dispatch({ type: 'CALL_INIT', payload: callResult }); - if (message.isMutating || message.isPayable) { - const tx = prepareContractTx( - contract.tx[message.method], - { gasLimit: gasLimit.addn(1), value: message.isPayable ? value || BN_ZERO : undefined, salt }, - transformed - ); - - const unsub = await tx.signAndSend(sender, result => { - const { status, events, dispatchError, dispatchInfo } = result; - - const log = events.map(({ event }) => { - return `${event.section}:${event.method}`; - }); - - let error: RegistryError | undefined; - - if (status.isFinalized) { - if (dispatchError) { - error = contract.registry.findMetaError(dispatchError.asModule); - } - - dispatch({ - type: 'CALL_FINALISED', - payload: { - ...callResult, - log: log, - error, - blockHash: status.asFinalized.toString(), - info: dispatchInfo?.toHuman(), - }, - }); - - unsub(); - } - }); - } else { - const { result, output } = await sendContractQuery( - contract.query[message.method], - sender, - { gasLimit, value: message.isPayable ? value || BN_ZERO : undefined }, - transformed - ); - - let error: RegistryError | undefined; - - if (result.isErr && result.asErr.isModule) { - error = contract.registry.findMetaError(result.asErr.asModule); - } - - dispatch({ - type: 'CALL_FINALISED', - payload: { ...callResult, data: output?.toHuman(), error }, - }); - } -} diff --git a/src/types/ui/contexts.ts b/src/types/ui/contexts.ts index b38cd735..8d003df1 100644 --- a/src/types/ui/contexts.ts +++ b/src/types/ui/contexts.ts @@ -90,7 +90,7 @@ export interface Transaction { accountId: string; events: Record; isValid: (_: SubmittableResult) => boolean; - onSuccess?: (_: SubmittableResult) => Promise; + onSuccess?: ((_: SubmittableResult) => void) | ((_: SubmittableResult) => Promise); onError?: () => void; } diff --git a/src/ui/components/contract/Interact.tsx b/src/ui/components/contract/Interact.tsx index a7da9d9f..823987a4 100644 --- a/src/ui/components/contract/Interact.tsx +++ b/src/ui/components/contract/Interact.tsx @@ -1,29 +1,38 @@ // Copyright 2021 @paritytech/substrate-contracts-explorer authors & contributors // SPDX-License-Identifier: Apache-2.0 -import React, { useReducer, useEffect, useState } from 'react'; -import { Dropdown } from '../common/Dropdown'; -import { ArgumentForm } from '../form/ArgumentForm'; -import { Button, Buttons } from '../common/Button'; -import { AccountSelect } from '../account/AccountSelect'; -import { Form, FormField, getValidation } from '../form/FormField'; -import { InputBalance } from '../form/InputBalance'; -import { InputGas } from '../form/InputGas'; +import React, { useReducer, useEffect, useState, useRef } from 'react'; +import { BN_ZERO } from '@polkadot/util'; import { ResultsOutput } from './ResultsOutput'; -import { call, createMessageOptions, dryRun } from 'api'; -import { useApi } from 'ui/contexts'; +import { AccountSelect } from 'ui/components/account'; +import { Dropdown, Button, Buttons } from 'ui/components/common'; +import { + ArgumentForm, + InputGas, + InputBalance, + Form, + FormField, + getValidation, +} from 'ui/components/form'; +import { + createMessageOptions, + dryRun, + NOOP, + prepareContractTx, + sendContractQuery, + transformUserInput, +} from 'api'; +import { useApi, useTransactions } from 'ui/contexts'; import { contractCallReducer, initialState } from 'ui/reducers'; -import { BN, ContractPromise } from 'types'; -import { useAccountId } from 'ui/hooks/useAccountId'; -import { useFormField } from 'ui/hooks/useFormField'; -import { useArgValues } from 'ui/hooks/useArgValues'; -import { useBalance } from 'ui/hooks/useBalance'; -import { useWeight } from 'ui/hooks'; +import { BN, ContractPromise, RegistryError, SubmittableResult } from 'types'; +import { useWeight, useBalance, useArgValues, useFormField, useAccountId } from 'ui/hooks'; interface Props { contract: ContractPromise; } +let nextId = 1; + export const InteractTab = ({ contract }: Props) => { const { api, keyring } = useApi(); const message = useFormField(contract.abi.messages[0]); @@ -32,6 +41,7 @@ export const InteractTab = ({ contract }: Props) => { const payment = useBalance(100); const { value: accountId, onChange: setAccountId, ...accountIdValidation } = useAccountId(); const [estimatedWeight, setEstimatedWeight] = useState(null); + const [txId, setTxId] = useState(0); useEffect(() => { if (state.results.length > 0) { @@ -39,6 +49,7 @@ export const InteractTab = ({ contract }: Props) => { type: 'RESET', }); } + nextId = 1; message.value = contract.abi.messages[0]; // clears call results when navigating to another contract page // to do: storage for call results @@ -76,6 +87,117 @@ export const InteractTab = ({ contract }: Props) => { const weight = useWeight(); + const transformed = transformUserInput(contract.registry, message.value.args, argValues); + + const options = { + gasLimit: weight.weight.addn(1), + value: message.value.isPayable ? payment.value || BN_ZERO : undefined, + }; + + const { queue, process } = useTransactions(); + + const tx = prepareContractTx(contract.tx[message.value.method], options, transformed); + + const onSuccess = ({ status, dispatchInfo, dispatchError, events }: SubmittableResult) => { + const callResult = { + id: nextId, + isComplete: false, + data: '', + log: [], + message: message.value, + time: Date.now(), + }; + const log = events.map(({ event }) => { + return `${event.section}:${event.method}`; + }); + + dispatch({ + type: 'CALL_FINALISED', + payload: { + ...callResult, + isComplete: true, + log: log, + error: dispatchError ? contract.registry.findMetaError(dispatchError.asModule) : undefined, + blockHash: status.asFinalized.toString(), + info: dispatchInfo?.toHuman(), + }, + }); + nextId++; + }; + + const read = async () => { + const callResult = { + id: nextId, + isComplete: false, + data: '', + log: [], + message: message.value, + time: Date.now(), + }; + + dispatch({ + type: 'CALL_INIT', + payload: { ...callResult }, + }); + const { result, output } = await sendContractQuery( + contract.query[message.value.method], + keyring?.getPair(accountId), + options, + transformed + ); + + let error: RegistryError | undefined; + + if (result.isErr && result.asErr.isModule) { + error = contract.registry.findMetaError(result.asErr.asModule); + } + + dispatch({ + type: 'CALL_FINALISED', + payload: { + ...callResult, + isComplete: true, + data: output?.toHuman(), + error, + }, + }); + nextId++; + }; + + const isValid = () => true; + + const onError = NOOP; + + const newId = useRef(); + + const clickHandler = () => { + if (tx && accountId) { + const callResult = { + id: nextId, + isComplete: false, + data: '', + log: [], + message: message.value, + time: Date.now(), + }; + + dispatch({ + type: 'CALL_INIT', + payload: { ...callResult }, + }); + newId.current = queue({ extrinsic: tx, accountId, onSuccess, onError, isValid }); + setTxId(newId.current); + } + }; + + useEffect(() => { + async function processTx() { + txId && (await process(txId)); + } + processTx().catch(e => console.error(e)); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [txId]); + if (!contract) return null; return ( @@ -132,30 +254,25 @@ export const InteractTab = ({ contract }: Props) => { - + {message.value.isPayable || message.value.isMutating ? ( + + ) : ( + + )}
diff --git a/src/ui/components/form/index.ts b/src/ui/components/form/index.ts index a43adea0..ecae404b 100644 --- a/src/ui/components/form/index.ts +++ b/src/ui/components/form/index.ts @@ -7,6 +7,7 @@ export * from './InputBalance'; export * from './InputFile'; export * from './InputNumber'; export * from './InputSalt'; +export * from './InputGas'; export * from './ArgumentForm'; export * from './CodeHashField'; export * from './Bool'; diff --git a/src/ui/contexts/InstantiateContext.tsx b/src/ui/contexts/InstantiateContext.tsx index cfda91d8..1d64b576 100644 --- a/src/ui/contexts/InstantiateContext.tsx +++ b/src/ui/contexts/InstantiateContext.tsx @@ -87,7 +87,7 @@ export function InstantiateContextProvider({ setData(newData); stepForward(); } catch (e) { - console.log(e); + console.error(e); setTx([null, NOOP, 'Error creating transaction']); } diff --git a/src/ui/contexts/TransactionsContext.tsx b/src/ui/contexts/TransactionsContext.tsx index 0a877fcb..e628babc 100644 --- a/src/ui/contexts/TransactionsContext.tsx +++ b/src/ui/contexts/TransactionsContext.tsx @@ -12,12 +12,12 @@ import { } from 'types'; import { Transactions } from 'ui/components/Transactions'; -let nextId = 0; +let nextId = 1; export function createTx(options: TransactionOptions): Transaction { return { ...options, - id: ++nextId, + id: nextId, status: Status.Queued, events: {}, }; @@ -107,6 +107,8 @@ export function TransactionsContextProvider({ ]); unsub(); + + nextId++; } }); } @@ -124,9 +126,12 @@ export function TransactionsContextProvider({ let autoDismiss: NodeJS.Timeout; if (txs.length > 0) { - autoDismiss = setTimeout((): void => { - setTxs([...txs.filter(({ status }) => status === 'processing' || status === 'queued')]); - }, 5000); + const completed = txs.filter(({ status }) => status === 'error' || status === 'success'); + if (completed.length > 0) { + autoDismiss = setTimeout((): void => { + setTxs([...txs.filter(({ status }) => status === 'processing' || status === 'queued')]); + }, 5000); + } } return () => clearTimeout(autoDismiss); diff --git a/src/ui/hooks/index.ts b/src/ui/hooks/index.ts index 65417caf..1c5c1772 100644 --- a/src/ui/hooks/index.ts +++ b/src/ui/hooks/index.ts @@ -13,3 +13,8 @@ export * from './useTopContracts'; export * from './useDbQuery'; export * from './useWeight'; export * from './useBlockTime'; +export * from './useBalance'; +export * from './useQueueTx'; +export * from './useFormField'; +export * from './useArgValues'; +export * from './useAccountId'; diff --git a/src/ui/hooks/useWeight.ts b/src/ui/hooks/useWeight.ts index 595f840b..63b8fbf6 100644 --- a/src/ui/hooks/useWeight.ts +++ b/src/ui/hooks/useWeight.ts @@ -13,7 +13,6 @@ import type { UseWeight } from 'types'; export const useWeight = (): UseWeight => { const { api } = useApi(); const [blockTime] = useBlockTime(); - console.log(blockTime); const [megaGas, _setMegaGas] = useState( (api?.consts.system.blockWeights ? api.consts.system.blockWeights.maxBlock From 79c18d3d84f62a6ecfee6b62a7a0ba37c02c7e68 Mon Sep 17 00:00:00 2001 From: Andreea Eftene Date: Mon, 10 Jan 2022 00:05:18 +0100 Subject: [PATCH 3/9] refactor transaction queue --- src/types/ui/contexts.ts | 22 ++-- src/ui/components/Transactions.tsx | 116 +++++++----------- src/ui/components/common/NotificationIcon.tsx | 28 +++++ src/ui/contexts/TransactionsContext.tsx | 93 +++++--------- src/ui/hooks/useQueueTx.ts | 14 +-- 5 files changed, 119 insertions(+), 154 deletions(-) create mode 100644 src/ui/components/common/NotificationIcon.tsx diff --git a/src/types/ui/contexts.ts b/src/types/ui/contexts.ts index 8d003df1..404f3baa 100644 --- a/src/types/ui/contexts.ts +++ b/src/types/ui/contexts.ts @@ -82,27 +82,25 @@ export enum TxStatus { Processing = 'processing', Queued = 'queued', } - -export interface Transaction { - id: number; - status: TxStatus; +export interface TxOptions { extrinsic: SubmittableExtrinsic<'promise'>; accountId: string; - events: Record; isValid: (_: SubmittableResult) => boolean; onSuccess?: ((_: SubmittableResult) => void) | ((_: SubmittableResult) => Promise); onError?: () => void; } -export type TransactionOptions = Pick< - Transaction, - 'accountId' | 'extrinsic' | 'onSuccess' | 'onError' | 'isValid' ->; +export interface QueuedTxOptions extends TxOptions { + status: TxStatus; + events: Record; +} +export interface TransactionsQueue { + [id: number]: QueuedTxOptions; +} export interface TransactionsState { - txs: Transaction[]; + txs: TransactionsQueue; process: (_: number) => Promise; - queue: (_: TransactionOptions) => number; - unqueue: (id: number) => void; + queue: (_: TxOptions) => number; dismiss: (id: number) => void; } diff --git a/src/ui/components/Transactions.tsx b/src/ui/components/Transactions.tsx index 8afedc99..8ac60fda 100644 --- a/src/ui/components/Transactions.tsx +++ b/src/ui/components/Transactions.tsx @@ -1,81 +1,55 @@ // Copyright 2021 @paritytech/substrate-contracts-explorer authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { - BellIcon, - CheckIcon, - ClockIcon, - ExclamationCircleIcon, - XIcon, -} from '@heroicons/react/outline'; +import { BellIcon, XIcon } from '@heroicons/react/outline'; import React from 'react'; -import { Spinner } from './common/Spinner'; +import { NotificationIcon } from './common/NotificationIcon'; import type { TransactionsState } from 'types'; import { classes } from 'ui/util'; -type Props = React.HTMLAttributes & TransactionsState; +export function Transactions({ + dismiss, + txs, +}: React.HTMLAttributes & TransactionsState) { + const Notifications: JSX.Element[] = []; + for (const id in txs) { + const { extrinsic, status, events } = txs[id]; + const isComplete = status === 'error' || status === 'success'; -export function Transactions({ className, dismiss, txs }: Props) { - return ( -
- {txs.map(({ extrinsic, id, status, events }) => { - const [icon, text] = ((): [React.ReactNode, React.ReactNode] => { - switch (status) { - case 'success': - return [ - , - 'complete!', - ]; - case 'error': - return [ - , - 'error', - ]; - case 'processing': - return [, 'processing...']; - - default: - return [ - , - 'queued', - ]; - } - })(); - const isComplete = status === 'error' || status === 'success'; - return ( - <> -
- {icon} -
-
{extrinsic.registry.findMetaCall(extrinsic.callIndex).method}
-
{text}
-
- {isComplete && ( - dismiss(id)} /> - )} + Notifications.push( + <> +
+ +
+
{extrinsic.registry.findMetaCall(extrinsic.callIndex).method}
+
{status}
+
+ {isComplete && ( + dismiss(parseInt(id))} /> + )} +
+ {isComplete && ( +
+ +
+ {Object.keys(events).map(eventName => { + const times = events[eventName] > 1 ? ` (x${events[eventName]})` : ''; + return ( +
+ {`${eventName}${times}`} +
+ ); + })}
- {isComplete && ( -
- -
- {Object.keys(events).map(eventName => { - const times = events[eventName] > 1 ? ` (x${events[eventName]})` : ''; - return ( -
- {`${eventName}${times}`} -
- ); - })} -
- dismiss(id)} /> -
- )} - - ); - })} -
- ); + dismiss(parseInt(id))} /> +
+ )} + + ); + } + + return
{...Notifications}
; } diff --git a/src/ui/components/common/NotificationIcon.tsx b/src/ui/components/common/NotificationIcon.tsx new file mode 100644 index 00000000..23837dfd --- /dev/null +++ b/src/ui/components/common/NotificationIcon.tsx @@ -0,0 +1,28 @@ +// Copyright 2021 @paritytech/substrate-contracts-explorer authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { CheckIcon, ClockIcon, ExclamationCircleIcon } from '@heroicons/react/outline'; +import React from 'react'; +import { Spinner } from './Spinner'; +import { TxStatus } from 'types'; +import { classes } from 'ui/util'; + +interface Props { + status: TxStatus; +} + +export const NotificationIcon = ({ status }: Props) => { + switch (status) { + case 'success': + return ; + + case 'error': + return ; + + case 'processing': + return ; + + default: + return ; + } +}; diff --git a/src/ui/contexts/TransactionsContext.tsx b/src/ui/contexts/TransactionsContext.tsx index e628babc..4e71e2b5 100644 --- a/src/ui/contexts/TransactionsContext.tsx +++ b/src/ui/contexts/TransactionsContext.tsx @@ -3,58 +3,39 @@ import React, { useState, useContext, useEffect } from 'react'; import { useApi } from './ApiContext'; -import { - TransactionOptions, - Transaction as Tx, - TransactionsState, - Transaction, - TxStatus as Status, -} from 'types'; +import { TxOptions, TransactionsState, TxStatus as Status, TransactionsQueue } from 'types'; import { Transactions } from 'ui/components/Transactions'; let nextId = 1; -export function createTx(options: TransactionOptions): Transaction { - return { - ...options, - id: nextId, - status: Status.Queued, - events: {}, - }; -} - export const TransactionsContext = React.createContext({} as unknown as TransactionsState); export function TransactionsContextProvider({ children, }: React.PropsWithChildren>) { const { keyring, api } = useApi(); - const [txs, setTxs] = useState([]); + const [txs, setTxs] = useState({}); + + function queue(options: TxOptions): number { + setTxs({ + ...txs, + [nextId]: { + ...options, - function queue(options: TransactionOptions): number { - setTxs(txs => [ - ...txs.filter(({ id, status }) => id < nextId && status === 'queued'), - createTx(options), - ]); + status: Status.Queued, + events: {}, + }, + }); return nextId; } async function process(id: number) { - const tx = txs.find(tx => id === tx.id); + const tx = txs[id]; if (tx) { const { extrinsic, accountId, isValid, onSuccess } = tx; - setTxs(txs => [ - ...txs.map(tx => { - return tx.id === id - ? { - ...tx, - status: Status.Processing, - } - : tx; - }), - ]); + setTxs({ ...txs, [id]: { ...txs[id], status: Status.Processing } }); const unsub = await extrinsic.signAndSend(keyring.getPair(accountId), {}, async result => { if (result.isFinalized) { @@ -71,17 +52,7 @@ export function TransactionsContextProvider({ }); if (!isValid(result)) { - setTxs(txs => [ - ...txs.map(tx => { - return tx.id === id - ? { - ...tx, - status: Status.Error, - events, - } - : tx; - }), - ]); + setTxs({ ...txs, [id]: { ...txs[id], status: Status.Error, events } }); let message = 'Transaction failed'; @@ -94,17 +65,7 @@ export function TransactionsContextProvider({ onSuccess && (await onSuccess(result)); - setTxs(txs => [ - ...txs.map(tx => { - return tx.id === id - ? { - ...tx, - status: Status.Success, - events, - } - : tx; - }), - ]); + setTxs({ ...txs, [id]: { ...txs[id], status: Status.Success, events } }); unsub(); @@ -114,22 +75,27 @@ export function TransactionsContextProvider({ } } - function unqueue(id: number) { - setTxs([...txs.filter(tx => tx.id !== id || tx.status !== 'queued')]); - } - function dismiss(id: number) { - setTxs([...txs.filter(tx => tx.id !== id)]); + const newTxs = { ...txs }; + delete newTxs[id]; + setTxs(newTxs); } useEffect((): (() => void) => { let autoDismiss: NodeJS.Timeout; - if (txs.length > 0) { - const completed = txs.filter(({ status }) => status === 'error' || status === 'success'); + if (JSON.stringify(txs) !== '{}') { + const completed: number[] = []; + for (const id in txs) { + if (txs[id].status === 'error' || txs[id].status === 'success') { + completed.push(parseInt(id)); + } + } if (completed.length > 0) { autoDismiss = setTimeout((): void => { - setTxs([...txs.filter(({ status }) => status === 'processing' || status === 'queued')]); + const newTxs = { ...txs }; + completed.forEach(id => delete newTxs[id]); + setTxs(newTxs); }, 5000); } } @@ -142,7 +108,6 @@ export function TransactionsContextProvider({ dismiss, process, queue, - unqueue, }; return ( diff --git a/src/ui/hooks/useQueueTx.ts b/src/ui/hooks/useQueueTx.ts index 27763a3a..21cb7bd8 100644 --- a/src/ui/hooks/useQueueTx.ts +++ b/src/ui/hooks/useQueueTx.ts @@ -15,13 +15,13 @@ export function useQueueTx( onError: VoidFn, isValid: (_: SubmittableResult) => boolean ): [VoidFn, VoidFn, boolean, boolean] { - const { queue, unqueue, process, txs } = useTransactions(); - const [txId, setTxId] = useState(null); + const { queue, dismiss, process, txs } = useTransactions(); + const [txId, setTxId] = useState(0); const txIdRef = useRef(txId); const isProcessing = useMemo( - (): boolean => !!(txs.find(({ id }) => txId === id)?.status === 'processing'), + (): boolean => !!(txs[txId] && txs[txId].status === 'processing'), [txs, txId] ); @@ -30,12 +30,12 @@ export function useQueueTx( }, [process, txId]); const onCancel = useCallback((): void => { - txId && unqueue(txId); - setTxId(null); - }, [unqueue, txId]); + txId && dismiss(txId); + setTxId(0); + }, [dismiss, txId]); useEffect((): void => { - if (extrinsic && accountId && isNull(txId)) { + if (extrinsic && accountId && txId === 0) { const newId = queue({ extrinsic, accountId, onSuccess, onError, isValid }); setTxId(newId); From e3c906f635783a06402f8b126ccd14ff3dcd76e0 Mon Sep 17 00:00:00 2001 From: Andreea Eftene Date: Mon, 10 Jan 2022 01:19:05 +0100 Subject: [PATCH 4/9] prevent runtime errors --- src/types/ui/contexts.ts | 2 +- src/ui/components/Transactions.tsx | 6 +++--- src/ui/components/common/NotificationIcon.tsx | 7 +++++-- src/ui/components/contract/Interact.tsx | 9 ++++----- src/ui/contexts/TransactionsContext.tsx | 9 ++++----- src/ui/hooks/useQueueTx.ts | 2 +- 6 files changed, 18 insertions(+), 17 deletions(-) diff --git a/src/types/ui/contexts.ts b/src/types/ui/contexts.ts index 404f3baa..a1952914 100644 --- a/src/types/ui/contexts.ts +++ b/src/types/ui/contexts.ts @@ -95,7 +95,7 @@ export interface QueuedTxOptions extends TxOptions { events: Record; } export interface TransactionsQueue { - [id: number]: QueuedTxOptions; + [id: number]: QueuedTxOptions | undefined; } export interface TransactionsState { diff --git a/src/ui/components/Transactions.tsx b/src/ui/components/Transactions.tsx index 8ac60fda..7388ded8 100644 --- a/src/ui/components/Transactions.tsx +++ b/src/ui/components/Transactions.tsx @@ -13,7 +13,7 @@ export function Transactions({ }: React.HTMLAttributes & TransactionsState) { const Notifications: JSX.Element[] = []; for (const id in txs) { - const { extrinsic, status, events } = txs[id]; + const { extrinsic, status, events } = txs[id] || {}; const isComplete = status === 'error' || status === 'success'; Notifications.push( @@ -24,14 +24,14 @@ export function Transactions({ >
-
{extrinsic.registry.findMetaCall(extrinsic.callIndex).method}
+
{extrinsic?.registry.findMetaCall(extrinsic.callIndex).method}
{status}
{isComplete && ( dismiss(parseInt(id))} /> )}
- {isComplete && ( + {isComplete && events && (
diff --git a/src/ui/components/common/NotificationIcon.tsx b/src/ui/components/common/NotificationIcon.tsx index 23837dfd..2b404be9 100644 --- a/src/ui/components/common/NotificationIcon.tsx +++ b/src/ui/components/common/NotificationIcon.tsx @@ -8,7 +8,7 @@ import { TxStatus } from 'types'; import { classes } from 'ui/util'; interface Props { - status: TxStatus; + status?: TxStatus; } export const NotificationIcon = ({ status }: Props) => { @@ -22,7 +22,10 @@ export const NotificationIcon = ({ status }: Props) => { case 'processing': return ; - default: + case 'queued': return ; + + default: + return null; } }; diff --git a/src/ui/components/contract/Interact.tsx b/src/ui/components/contract/Interact.tsx index 823987a4..06770303 100644 --- a/src/ui/components/contract/Interact.tsx +++ b/src/ui/components/contract/Interact.tsx @@ -94,9 +94,7 @@ export const InteractTab = ({ contract }: Props) => { value: message.value.isPayable ? payment.value || BN_ZERO : undefined, }; - const { queue, process } = useTransactions(); - - const tx = prepareContractTx(contract.tx[message.value.method], options, transformed); + const { queue, process, txs } = useTransactions(); const onSuccess = ({ status, dispatchInfo, dispatchError, events }: SubmittableResult) => { const callResult = { @@ -171,6 +169,8 @@ export const InteractTab = ({ contract }: Props) => { const newId = useRef(); const clickHandler = () => { + const tx = prepareContractTx(contract.tx[message.value.method], options, transformed); + if (tx && accountId) { const callResult = { id: nextId, @@ -257,7 +257,7 @@ export const InteractTab = ({ contract }: Props) => { {message.value.isPayable || message.value.isMutating ? (
- +
); diff --git a/src/ui/contexts/TransactionsContext.tsx b/src/ui/contexts/TransactionsContext.tsx index c420cb6b..2f4a83e9 100644 --- a/src/ui/contexts/TransactionsContext.tsx +++ b/src/ui/contexts/TransactionsContext.tsx @@ -5,6 +5,7 @@ import React, { useState, useContext, useEffect } from 'react'; import { useApi } from './ApiContext'; import { TxOptions, TransactionsState, TxStatus as Status, TransactionsQueue } from 'types'; import { Transactions } from 'ui/components/Transactions'; +import { isEmptyObj } from 'ui/util'; let nextId = 1; @@ -83,7 +84,7 @@ export function TransactionsContextProvider({ useEffect((): (() => void) => { let autoDismiss: NodeJS.Timeout; - if (JSON.stringify(txs) !== '{}') { + if (!isEmptyObj(txs)) { const completed: number[] = []; for (const id in txs) { if (txs[id]?.status === 'error' || txs[id]?.status === 'success') { diff --git a/src/ui/reducers/contractCall.ts b/src/ui/reducers/contractCall.ts deleted file mode 100644 index 52485a4d..00000000 --- a/src/ui/reducers/contractCall.ts +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright 2021 @paritytech/substrate-contracts-explorer authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { Reducer } from 'react'; -import { ContractCallState, ContractCallAction } from 'types'; - -export const initialState: ContractCallState = { - isLoading: false, - isSuccess: false, - results: [], -}; - -export const contractCallReducer: Reducer = ( - state, - action -) => { - switch (action.type) { - case 'CALL_INIT': - return { - ...state, - isLoading: true, - results: [ - ...state.results, - { - ...action.payload, - isComplete: false, - }, - ], - }; - - case 'CALL_FINALISED': - return { - ...state, - isSuccess: true, - results: state.results.map(result => - action.payload.id === result.id - ? { - ...result, - ...action.payload, - isComplete: true, - } - : result - ), - isLoading: false, - }; - - case 'RESET': - return initialState; - - default: - throw new Error(); - } -}; diff --git a/src/ui/reducers/index.ts b/src/ui/reducers/index.ts index ff62c72a..8028cf66 100644 --- a/src/ui/reducers/index.ts +++ b/src/ui/reducers/index.ts @@ -1,5 +1,4 @@ // Copyright 2021 @paritytech/substrate-contracts-explorer authors & contributors // SPDX-License-Identifier: Apache-2.0 -export * from './contractCall'; export * from './api'; diff --git a/src/ui/util/index.ts b/src/ui/util/index.ts index fb9ea2eb..4a68d2b6 100644 --- a/src/ui/util/index.ts +++ b/src/ui/util/index.ts @@ -24,4 +24,8 @@ export function isValidCodeHash(value: string): boolean { return /^0x[0-9a-fA-F]{64}$/.test(value); } +export function isEmptyObj(value: unknown) { + return JSON.stringify(value) === '{}'; +} + export * from './initValue'; From 2d0a9f56b1955bbe1369fb1b74ec94c3e62d0069 Mon Sep 17 00:00:00 2001 From: Andreea Eftene Date: Tue, 11 Jan 2022 20:20:49 +0100 Subject: [PATCH 9/9] catch signing errors --- src/ui/components/Transactions.tsx | 4 +- src/ui/components/contract/Interact.tsx | 2 +- src/ui/contexts/TransactionsContext.tsx | 65 +++++++++++++------------ 3 files changed, 38 insertions(+), 33 deletions(-) diff --git a/src/ui/components/Transactions.tsx b/src/ui/components/Transactions.tsx index 7388ded8..65fa7288 100644 --- a/src/ui/components/Transactions.tsx +++ b/src/ui/components/Transactions.tsx @@ -5,7 +5,7 @@ import { BellIcon, XIcon } from '@heroicons/react/outline'; import React from 'react'; import { NotificationIcon } from './common/NotificationIcon'; import type { TransactionsState } from 'types'; -import { classes } from 'ui/util'; +import { classes, isEmptyObj } from 'ui/util'; export function Transactions({ dismiss, @@ -31,7 +31,7 @@ export function Transactions({ dismiss(parseInt(id))} /> )}
- {isComplete && events && ( + {isComplete && events && !isEmptyObj(events) && (
diff --git a/src/ui/components/contract/Interact.tsx b/src/ui/components/contract/Interact.tsx index 408aea84..bf037fc0 100644 --- a/src/ui/components/contract/Interact.tsx +++ b/src/ui/components/contract/Interact.tsx @@ -141,7 +141,7 @@ export const InteractTab = ({ contract }: Props) => { setNextResultId(nextResultId + 1); }; - const isValid = () => true; + const isValid = (result: SubmittableResult) => !result.isError; const onError = NOOP; diff --git a/src/ui/contexts/TransactionsContext.tsx b/src/ui/contexts/TransactionsContext.tsx index 2f4a83e9..53722b05 100644 --- a/src/ui/contexts/TransactionsContext.tsx +++ b/src/ui/contexts/TransactionsContext.tsx @@ -37,41 +37,46 @@ export function TransactionsContextProvider({ setTxs({ ...txs, [id]: { ...tx, status: Status.Processing } }); - const unsub = await extrinsic.signAndSend(keyring.getPair(accountId), {}, async result => { - if (result.isFinalized) { - const events: Record = {}; - - result.events.forEach(record => { - const { event } = record; - const key = `${event.section}:${event.method}`; - if (!events[key]) { - events[key] = 1; - } else { - events[key]++; + try { + const unsub = await extrinsic.signAndSend(keyring.getPair(accountId), {}, async result => { + if (result.isFinalized) { + const events: Record = {}; + + result.events.forEach(record => { + const { event } = record; + const key = `${event.section}:${event.method}`; + if (!events[key]) { + events[key] = 1; + } else { + events[key]++; + } + }); + + if (!isValid(result)) { + setTxs({ ...txs, [id]: { ...tx, status: Status.Error, events } }); + + let message = 'Transaction failed'; + + if (result.dispatchError?.isModule) { + const decoded = api.registry.findMetaError(result.dispatchError.asModule); + message = `${decoded.section.toUpperCase()}.${decoded.method}: ${decoded.docs}`; + } + throw new Error(message); } - }); - if (!isValid(result)) { - setTxs({ ...txs, [id]: { ...tx, status: Status.Error, events } }); + onSuccess && (await onSuccess(result)); - let message = 'Transaction failed'; + setTxs({ ...txs, [id]: { ...tx, status: Status.Success, events } }); - if (result.dispatchError?.isModule) { - const decoded = api.registry.findMetaError(result.dispatchError.asModule); - message = `${decoded.section.toUpperCase()}.${decoded.method}: ${decoded.docs}`; - } - throw new Error(message); - } - - onSuccess && (await onSuccess(result)); - - setTxs({ ...txs, [id]: { ...tx, status: Status.Success, events } }); + unsub(); - unsub(); - - nextId++; - } - }); + nextId++; + } + }); + } catch (error) { + setTxs({ ...txs, [id]: { ...tx, status: Status.Error } }); + console.error(error); + } } }