// Copyright 2017-2026 @pezkuwi/react-hooks authors & contributors // SPDX-License-Identifier: Apache-2.0 import type { SubmittableResult } from '@pezkuwi/api'; import type { SubmittableExtrinsic } from '@pezkuwi/api/promise/types'; import type { ActionStatus, ActionStatusPartial, PartialQueueTxExtrinsic, PartialQueueTxRpc, QueueProps, QueueStatus, QueueTx, QueueTxExtrinsic, QueueTxRpc, QueueTxStatus, SignerCallback } from '@pezkuwi/react-components/Status/types'; import type { DispatchError, EventRecord } from '@pezkuwi/types/interfaces'; import type { ITuple, Registry, SignerPayloadJSON } from '@pezkuwi/types/types'; import React, { useCallback, useMemo, useRef, useState } from 'react'; import { getDispatchError, getIncompleteMessage } from '@pezkuwi/react-components/Status/checks'; import { STATUS_COMPLETE } from '@pezkuwi/react-components/Status/constants'; import { getContractAbi } from '@pezkuwi/react-components/util'; import jsonrpc from '@pezkuwi/types/interfaces/jsonrpc'; export interface Props { children: React.ReactNode; } interface StatusCount { count: number; status: ActionStatusPartial; } const EMPTY_STATE: Partial = { stqueue: [] as QueueStatus[], txqueue: [] as QueueTx[] }; let nextId = 0; const EVENT_MESSAGE = 'extrinsic event'; const REMOVE_TIMEOUT = 7500; const SUBMIT_RPC = jsonrpc.author.submitAndWatchExtrinsic; export const QueueCtx = React.createContext(EMPTY_STATE as QueueProps); function mergeStatus (status: ActionStatusPartial[]): ActionStatus[] { let others: ActionStatus | null = null; const initial = status .reduce((result: StatusCount[], status): StatusCount[] => { const prev = result.find(({ status: prev }) => prev.action === status.action && prev.status === status.status ); if (prev) { prev.count++; } else { result.push({ count: 1, status }); } return result; }, []) .map(({ count, status }): ActionStatusPartial => count === 1 ? status : { ...status, action: `${status.action} (x${count})` } ) .filter((status): boolean => { if (status.message !== EVENT_MESSAGE) { return true; } if (others) { if (status.action.startsWith('system.ExtrinsicSuccess')) { (others.action as string[]).unshift(status.action); } else { (others.action as string[]).push(status.action); } } else { others = { ...status, action: [status.action] }; } return false; }); return others ? initial.concat(others) : initial; } function extractEvents (result?: SubmittableResult): ActionStatus[] { return mergeStatus( (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) => !!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]>; return { action: `${section}.${method}`, message: getDispatchError(dispatchError), status: 'error' }; } const incomplete = getIncompleteMessage(record); if (incomplete) { return { action: `${section}.${method}`, message: incomplete, status: 'eventWarn' }; } else if (section === 'contracts') { if (method === 'ContractExecution' && data.length === 2) { // see if we have info for this contract const [accountId, encoded] = data; try { const abi = getContractAbi(accountId.toString()); if (abi) { const decoded = abi.decodeEvent(encoded as EventRecord); return { action: decoded.event.identifier, message: 'contract event', status: 'event' }; } } catch (error) { // ABI mismatch? console.error(error); } } else if (method === 'Evicted') { return { action: `${section}.${method}`, message: 'contract evicted', status: 'error' }; } } return { action: `${section}.${method}`, message: EVENT_MESSAGE, status: 'event' }; }) ); } const EMPTY_QUEUE_ST: QueueStatus[] = []; const EMPTY_QUEUE_TX: QueueTx[] = []; export function QueueCtxRoot ({ children }: Props): React.ReactElement { const [stqueue, _setStQueue] = useState(EMPTY_QUEUE_ST); const [txqueue, _setTxQueue] = useState(EMPTY_QUEUE_TX); const stRef = useRef(stqueue); const txRef = useRef(txqueue); const setStQueue = useCallback( (st: QueueStatus[]): void => { stRef.current = st; _setStQueue(st); }, [] ); const setTxQueue = useCallback( (tx: QueueTx[]): void => { txRef.current = tx; _setTxQueue(tx); }, [] ); const addToTxQueue = useCallback( (value: QueueTxExtrinsic | QueueTxRpc | QueueTx): void => { const id = ++nextId; const removeItem = () => setTxQueue([ ...txRef.current.map((item): QueueTx => item.id === id ? { ...item, status: 'completed' } : item ) ]); setTxQueue([...txRef.current, { ...value, id, removeItem, rpc: (value as QueueTxRpc).rpc || SUBMIT_RPC, status: 'queued' }]); }, [setTxQueue] ); const queueAction = useCallback( (_status: ActionStatus | ActionStatus[]): void => { const status = Array.isArray(_status) ? _status : [_status]; status.length && setStQueue([ ...stRef.current, ...(status.map((item): QueueStatus => { const id = ++nextId; const removeItem = (): void => setStQueue([...stRef.current.filter((item) => item.id !== id)]); setTimeout(removeItem, REMOVE_TIMEOUT); return { ...item, id, isCompleted: false, removeItem }; })) ]); }, [setStQueue] ); const queueExtrinsic = useCallback( (value: PartialQueueTxExtrinsic) => addToTxQueue({ ...value }), [addToTxQueue] ); const queuePayload = useCallback( (registry: Registry, payload: SignerPayloadJSON, signerCb: SignerCallback): void => { addToTxQueue({ accountId: payload.address, // this is not great, but the Extrinsic doesn't need a submittable extrinsic: registry.createType('Extrinsic', { method: registry.createType('Call', payload.method) }, { version: payload.version } ) as unknown as SubmittableExtrinsic, payload, signerCb }); }, [addToTxQueue] ); const queueRpc = useCallback( (value: PartialQueueTxRpc) => addToTxQueue({ ...value }), [addToTxQueue] ); const queueSetTxStatus = useCallback( (id: number, status: QueueTxStatus, result?: SubmittableResult, error?: Error): void => { setTxQueue([ ...txRef.current.map((item): QueueTx => item.id === id ? { ...item, error: error === undefined ? item.error : error, result: result === undefined ? item.result as SubmittableResult : result, status: item.status === 'completed' ? item.status : status } : item ) ]); queueAction(extractEvents(result)); if (STATUS_COMPLETE.includes(status)) { setTimeout((): void => { const item = txRef.current.find((item) => item.id === id); item && item.removeItem(); }, REMOVE_TIMEOUT); } }, [queueAction, setTxQueue] ); const value = useMemo( () => ({ queueAction, queueExtrinsic, queuePayload, queueRpc, queueSetTxStatus, stqueue, txqueue }), [queueAction, queueExtrinsic, queuePayload, queueRpc, queueSetTxStatus, stqueue, txqueue] ); return ( {children} ); }