vega-frontend-monorepo/libs/web3/src/lib/use-ethereum-transaction.tsx
2022-11-04 10:47:53 +00:00

184 lines
4.9 KiB
TypeScript

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 {
EthereumTransactionDialog,
getTransactionContent,
} from './ethereum-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<EthTxState>(initialState);
const setTransaction = useCallback((update: Partial<EthTxState>) => {
_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<TContract[TMethod]>) => {
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 () => (
<EthereumTransactionDialog
title={formatLabel(methodName as string)}
onChange={() => {
reset();
}}
transaction={transaction}
requiredConfirmations={requiredConfirmations}
/>
);
}, [methodName, transaction, requiredConfirmations, reset]);
const TxContent = useMemo(
() =>
getTransactionContent({
title: formatLabel(methodName as string),
transaction,
requiredConfirmations,
reset,
}),
[methodName, requiredConfirmations, reset, transaction]
);
return { perform, transaction, reset, setConfirmed, Dialog, TxContent };
};
export type EthTransaction = ReturnType<typeof useEthereumTransaction>;