import { formatLabel } from '@vegaprotocol/react-helpers'; import type { ethers } from 'ethers'; import { useCallback, useMemo, useState } from 'react'; import type { EthereumError } from './ethereum-error'; import { isExpectedEthereumError } from './ethereum-error'; import { isEthereumError } from './ethereum-error'; import { TransactionDialog } from './transaction-dialog'; export enum EthTxStatus { Default = 'Default', Requested = 'Requested', Pending = 'Pending', Complete = 'Complete', Confirmed = 'Confirmed', Error = 'Error', } export type TxError = Error | EthereumError; export interface EthTxState { status: EthTxStatus; error: TxError | null; txHash: string | null; receipt: ethers.ContractReceipt | null; confirmations: number; dialogOpen: boolean; } export const initialState = { status: EthTxStatus.Default, error: null, txHash: null, receipt: null, confirmations: 0, dialogOpen: false, }; type DefaultContract = { contract: ethers.Contract; }; export const useEthereumTransaction = < TContract extends DefaultContract, TMethod extends string >( contract: TContract | null, methodName: keyof TContract, requiredConfirmations = 1, requiresConfirmation = false ) => { const [transaction, _setTransaction] = useState(initialState); const setTransaction = useCallback((update: Partial) => { _setTransaction((curr) => ({ ...curr, ...update, })); }, []); const perform = useCallback( // @ts-ignore TS errors here as TMethod doesn't satisfy the constraints on TContract // its a tricky one to fix but does enforce the correct types when calling perform async (...args: Parameters) => { setTransaction({ status: EthTxStatus.Requested, error: null, confirmations: 0, dialogOpen: true, }); try { if ( !contract || typeof contract[methodName] !== 'function' || typeof contract.contract.callStatic[methodName as string] !== 'function' ) { throw new Error('method not found on contract'); } await contract.contract.callStatic[methodName as string](...args); } catch (err) { setTransaction({ status: EthTxStatus.Error, error: err as EthereumError, }); return; } try { const method = contract[methodName]; if (!method || typeof method !== 'function') { throw new Error('method not found on contract'); } const tx = await method.call(contract, ...args); let receipt: ethers.ContractReceipt | null = null; setTransaction({ status: EthTxStatus.Pending, txHash: tx.hash }); for (let i = 1; i <= requiredConfirmations; i++) { receipt = await tx.wait(i); setTransaction({ confirmations: receipt ? receipt.confirmations : requiredConfirmations, }); } if (!receipt) { throw new Error('no receipt after confirmations are met'); } if (requiresConfirmation) { setTransaction({ status: EthTxStatus.Complete, receipt }); } else { setTransaction({ status: EthTxStatus.Confirmed, receipt }); } } catch (err) { if (err instanceof Error || isEthereumError(err)) { if (isExpectedEthereumError(err)) { setTransaction({ dialogOpen: false }); } else { setTransaction({ status: EthTxStatus.Error, error: err }); } } else { setTransaction({ status: EthTxStatus.Error, error: new Error('Something went wrong'), }); } return; } }, [ contract, methodName, requiredConfirmations, requiresConfirmation, setTransaction, ] ); const reset = useCallback(() => { setTransaction(initialState); }, [setTransaction]); const setConfirmed = useCallback(() => { setTransaction({ status: EthTxStatus.Confirmed }); }, [setTransaction]); const Dialog = useMemo(() => { return () => ( { reset(); }} transaction={transaction} requiredConfirmations={requiredConfirmations} /> ); }, [methodName, transaction, requiredConfirmations, reset]); return { perform, transaction, reset, setConfirmed, Dialog }; };