From 00fd3af74620a4a0ae0e317723880bf0fc51856e Mon Sep 17 00:00:00 2001 From: Leonardo Zizzamia Date: Tue, 13 Aug 2024 23:46:19 -0700 Subject: [PATCH] feat: simplified setErrorMessage within Transaction component (#1047) --- .../components/TransactionProvider.tsx | 29 +++------- src/transaction/constants.ts | 6 --- src/transaction/hooks/useCallsStatus.test.ts | 3 +- src/transaction/hooks/useCallsStatus.ts | 8 +-- .../hooks/useWriteContract.test.ts | 19 ++----- src/transaction/hooks/useWriteContract.ts | 31 +++++------ .../hooks/useWriteContracts.test.ts | 53 ++++++++++++++----- src/transaction/hooks/useWriteContracts.ts | 27 ++++------ src/transaction/types.ts | 4 +- .../utils/isUserRejectedRequestError.test.ts | 21 ++++++++ .../utils/isUserRejectedRequestError.ts | 8 +++ vitest.config.ts | 8 +-- 12 files changed, 118 insertions(+), 99 deletions(-) create mode 100644 src/transaction/utils/isUserRejectedRequestError.test.ts create mode 100644 src/transaction/utils/isUserRejectedRequestError.ts diff --git a/src/transaction/components/TransactionProvider.tsx b/src/transaction/components/TransactionProvider.tsx index acb3003308..086b1fd6e0 100644 --- a/src/transaction/components/TransactionProvider.tsx +++ b/src/transaction/components/TransactionProvider.tsx @@ -5,11 +5,7 @@ import { useEffect, useState, } from 'react'; -import type { - Address, - TransactionExecutionError, - TransactionReceipt, -} from 'viem'; +import type { Address, TransactionReceipt } from 'viem'; import { useAccount, useConfig, @@ -30,6 +26,7 @@ import type { TransactionContextType, TransactionProviderReact, } from '../types'; +import { isUserRejectedRequestError } from '../utils/isUserRejectedRequestError'; const emptyContext = {} as TransactionContextType; export const TransactionContext = @@ -74,7 +71,6 @@ export function TransactionProvider({ // Hooks that depend from Core Hooks const { status: statusWriteContracts, writeContractsAsync } = useWriteContracts({ - setErrorMessage, setLifeCycleStatus, setTransactionId, }); @@ -83,7 +79,6 @@ export function TransactionProvider({ writeContractAsync, data: writeContractTransactionHash, } = useWriteContract({ - setErrorMessage, setLifeCycleStatus, setTransactionHashArray, transactionHashArray, @@ -100,6 +95,7 @@ export function TransactionProvider({ useEffect(() => { // Emit Error if (lifeCycleStatus.statusName === 'error') { + setErrorMessage(lifeCycleStatus.statusData.message); onError?.(lifeCycleStatus.statusData); } // Emit State @@ -145,15 +141,10 @@ export function TransactionProvider({ try { await writeContractAsync?.(contract); } catch (err) { - // if user rejected request - if ( - (err as TransactionExecutionError)?.cause?.name === - 'UserRejectedRequestError' - ) { - setErrorMessage('Request denied.'); - } else { - setErrorMessage(GENERIC_ERROR_MESSAGE); - } + const errorMessage = isUserRejectedRequestError(err) + ? 'Request denied.' + : GENERIC_ERROR_MESSAGE; + setErrorMessage(errorMessage); } } }, [contracts, writeContractAsync]); @@ -188,10 +179,7 @@ export function TransactionProvider({ setErrorMessage(GENERIC_ERROR_MESSAGE); } // handles user rejected request error - } else if ( - (err as TransactionExecutionError)?.cause?.name === - 'UserRejectedRequestError' - ) { + } else if (isUserRejectedRequestError(err)) { setErrorMessage('Request denied.'); // handles generic error } else { @@ -230,7 +218,6 @@ export function TransactionProvider({ isToastVisible, onSubmit: handleSubmit, receipt, - setErrorMessage, setIsToastVisible, setLifeCycleStatus, setTransactionId, diff --git a/src/transaction/constants.ts b/src/transaction/constants.ts index b2933810c9..dcbefc7272 100644 --- a/src/transaction/constants.ts +++ b/src/transaction/constants.ts @@ -3,9 +3,3 @@ export const GENERIC_ERROR_MESSAGE = 'Something went wrong. Please try again.'; export const METHOD_NOT_SUPPORTED_ERROR_SUBSTRING = 'this request method is not supported'; export const SEND_CALLS_NOT_SUPPORTED_ERROR = 'SEND_CALLS_NOT_SUPPORTED_ERROR'; -export const UNCAUGHT_WRITE_CONTRACT_ERROR_CODE = - 'UNCAUGHT_WRITE_CONTRACT_ERROR'; -export const UNCAUGHT_WRITE_CONTRACTS_ERROR_CODE = - 'UNCAUGHT_WRITE_WRITE_CONTRACTS_ERROR'; -export const WRITE_CONTRACT_ERROR_CODE = 'WRITE_CONTRACT_ERROR'; -export const WRITE_CONTRACTS_ERROR_CODE = 'WRITE_CONTRACTS_ERROR'; diff --git a/src/transaction/hooks/useCallsStatus.test.ts b/src/transaction/hooks/useCallsStatus.test.ts index b1674958d9..60b65cb294 100644 --- a/src/transaction/hooks/useCallsStatus.test.ts +++ b/src/transaction/hooks/useCallsStatus.test.ts @@ -40,8 +40,9 @@ describe('useCallsStatus', () => { expect(mockSetLifeCycleStatus).toHaveBeenCalledWith({ statusName: 'error', statusData: { - code: 'UNCAUGHT_CALL_STATUS_ERROR', + code: 'TmUCSh01', error: JSON.stringify(mockError), + message: '', }, }); }); diff --git a/src/transaction/hooks/useCallsStatus.ts b/src/transaction/hooks/useCallsStatus.ts index 705566befa..ff6162f278 100644 --- a/src/transaction/hooks/useCallsStatus.ts +++ b/src/transaction/hooks/useCallsStatus.ts @@ -1,8 +1,6 @@ import { useCallsStatus as useCallsStatusWagmi } from 'wagmi/experimental'; import type { UseCallsStatusParams } from '../types'; -const uncaughtErrorCode = 'UNCAUGHT_CALL_STATUS_ERROR'; - export function useCallsStatus({ setLifeCycleStatus, transactionId, @@ -22,7 +20,11 @@ export function useCallsStatus({ } catch (err) { setLifeCycleStatus({ statusName: 'error', - statusData: { code: uncaughtErrorCode, error: JSON.stringify(err) }, + statusData: { + code: 'TmUCSh01', + error: JSON.stringify(err), + message: '', + }, }); return { status: 'error', transactionHash: undefined }; } diff --git a/src/transaction/hooks/useWriteContract.test.ts b/src/transaction/hooks/useWriteContract.test.ts index 4d1f549bbf..12b13bed4a 100644 --- a/src/transaction/hooks/useWriteContract.test.ts +++ b/src/transaction/hooks/useWriteContract.test.ts @@ -21,7 +21,6 @@ type MockUseWriteContractReturn = { }; describe('useWriteContract', () => { - const mockSetErrorMessage = vi.fn(); const mockSetLifeCycleStatus = vi.fn(); const mockSetTransactionHashArray = vi.fn(); @@ -39,7 +38,6 @@ describe('useWriteContract', () => { } as MockUseWriteContractReturn); const { result } = renderHook(() => useWriteContract({ - setErrorMessage: mockSetErrorMessage, setLifeCycleStatus: mockSetLifeCycleStatus, setTransactionHashArray: mockSetTransactionHashArray, }), @@ -64,23 +62,18 @@ describe('useWriteContract', () => { ); renderHook(() => useWriteContract({ - setErrorMessage: mockSetErrorMessage, setLifeCycleStatus: mockSetLifeCycleStatus, setTransactionHashArray: mockSetTransactionHashArray, }), ); - expect(onErrorCallback).toBeDefined(); onErrorCallback?.(genericError); - - expect(mockSetErrorMessage).toHaveBeenCalledWith( - 'Something went wrong. Please try again.', - ); expect(mockSetLifeCycleStatus).toHaveBeenCalledWith({ statusName: 'error', statusData: { - code: 'WRITE_CONTRACT_ERROR', + code: 'TmUWCh01', error: 'Something went wrong. Please try again.', + message: 'Something went wrong. Please try again.', }, }); }); @@ -100,7 +93,6 @@ describe('useWriteContract', () => { ); renderHook(() => useWriteContract({ - setErrorMessage: mockSetErrorMessage, setLifeCycleStatus: mockSetLifeCycleStatus, setTransactionHashArray: mockSetTransactionHashArray, }), @@ -119,21 +111,18 @@ describe('useWriteContract', () => { ); const { result } = renderHook(() => useWriteContract({ - setErrorMessage: mockSetErrorMessage, setLifeCycleStatus: mockSetLifeCycleStatus, setTransactionHashArray: mockSetTransactionHashArray, }), ); expect(result.current.status).toBe('error'); expect(result.current.writeContractAsync).toBeInstanceOf(Function); - expect(mockSetErrorMessage).toHaveBeenCalledWith( - 'Something went wrong. Please try again.', - ); expect(mockSetLifeCycleStatus).toHaveBeenCalledWith({ statusName: 'error', statusData: { - code: 'UNCAUGHT_WRITE_CONTRACT_ERROR', + code: 'TmUWCh02', error: JSON.stringify(uncaughtError), + message: 'Something went wrong. Please try again.', }, }); }); diff --git a/src/transaction/hooks/useWriteContract.ts b/src/transaction/hooks/useWriteContract.ts index 3bb051f6d6..385aee697e 100644 --- a/src/transaction/hooks/useWriteContract.ts +++ b/src/transaction/hooks/useWriteContract.ts @@ -1,11 +1,8 @@ -import type { Address, TransactionExecutionError } from 'viem'; +import type { Address } from 'viem'; import { useWriteContract as useWriteContractWagmi } from 'wagmi'; -import { - GENERIC_ERROR_MESSAGE, - UNCAUGHT_WRITE_CONTRACT_ERROR_CODE, - WRITE_CONTRACT_ERROR_CODE, -} from '../constants'; +import { GENERIC_ERROR_MESSAGE } from '../constants'; import type { UseWriteContractParams } from '../types'; +import { isUserRejectedRequestError } from '../utils/isUserRejectedRequestError'; /** * Wagmi hook for single contract transactions. @@ -13,7 +10,6 @@ import type { UseWriteContractParams } from '../types'; * Does not support transaction batching or paymasters. */ export function useWriteContract({ - setErrorMessage, setLifeCycleStatus, setTransactionHashArray, transactionHashArray, @@ -22,17 +18,16 @@ export function useWriteContract({ const { status, writeContractAsync, data } = useWriteContractWagmi({ mutation: { onError: (e) => { - if ( - (e as TransactionExecutionError)?.cause?.name === - 'UserRejectedRequestError' - ) { - setErrorMessage('Request denied.'); - } else { - setErrorMessage(GENERIC_ERROR_MESSAGE); - } + const errorMessage = isUserRejectedRequestError(e) + ? 'Request denied.' + : GENERIC_ERROR_MESSAGE; setLifeCycleStatus({ statusName: 'error', - statusData: { code: WRITE_CONTRACT_ERROR_CODE, error: e.message }, + statusData: { + code: 'TmUWCh01', // Transaction module UseWriteContract hook 01 error + error: e.message, + message: errorMessage, + }, }); }, onSuccess: (hash: Address) => { @@ -47,11 +42,11 @@ export function useWriteContract({ setLifeCycleStatus({ statusName: 'error', statusData: { - code: UNCAUGHT_WRITE_CONTRACT_ERROR_CODE, + code: 'TmUWCh02', error: JSON.stringify(err), + message: GENERIC_ERROR_MESSAGE, }, }); - setErrorMessage(GENERIC_ERROR_MESSAGE); return { status: 'error', writeContractAsync: () => {} }; } } diff --git a/src/transaction/hooks/useWriteContracts.test.ts b/src/transaction/hooks/useWriteContracts.test.ts index d441f379c5..ab07fb4b1a 100644 --- a/src/transaction/hooks/useWriteContracts.test.ts +++ b/src/transaction/hooks/useWriteContracts.test.ts @@ -1,4 +1,5 @@ import { renderHook } from '@testing-library/react'; +import type { TransactionExecutionError } from 'viem'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { useWriteContracts as useWriteContractsWagmi } from 'wagmi/experimental'; import { useWriteContracts } from './useWriteContracts'; @@ -8,7 +9,6 @@ vi.mock('wagmi/experimental', () => ({ })); describe('useWriteContracts', () => { - const mockSetErrorMessage = vi.fn(); const mockSetLifeCycleStatus = vi.fn(); const mockSetTransactionId = vi.fn(); @@ -30,21 +30,54 @@ describe('useWriteContracts', () => { ); renderHook(() => useWriteContracts({ - setErrorMessage: mockSetErrorMessage, setLifeCycleStatus: mockSetLifeCycleStatus, setTransactionId: mockSetTransactionId, }), ); expect(onErrorCallback).toBeDefined(); onErrorCallback?.(genericError); - expect(mockSetErrorMessage).toHaveBeenCalledWith( - 'Something went wrong. Please try again.', - ); expect(mockSetLifeCycleStatus).toHaveBeenCalledWith({ statusName: 'error', statusData: { - code: 'WRITE_CONTRACTS_ERROR', + code: 'TmUWCSh01', error: 'Something went wrong. Please try again.', + message: 'Something went wrong. Please try again.', + }, + }); + }); + + it('should handle userRejectedRequestError', () => { + let onErrorCallback: + | ((error: TransactionExecutionError) => void) + | undefined; + (useWriteContractsWagmi as ReturnType).mockImplementation( + ({ mutation }: UseWriteContractsConfig) => { + onErrorCallback = mutation.onError; + return { + writeContracts: vi.fn(), + status: 'error', + }; + }, + ); + renderHook(() => + useWriteContracts({ + setLifeCycleStatus: mockSetLifeCycleStatus, + setTransactionId: mockSetTransactionId, + }), + ); + expect(onErrorCallback).toBeDefined(); + onErrorCallback?.({ + cause: { + name: 'UserRejectedRequestError', + }, + message: 'Request denied.', + }); + expect(mockSetLifeCycleStatus).toHaveBeenCalledWith({ + statusName: 'error', + statusData: { + code: 'TmUWCSh01', + error: 'Request denied.', + message: 'Request denied.', }, }); }); @@ -63,7 +96,6 @@ describe('useWriteContracts', () => { ); renderHook(() => useWriteContracts({ - setErrorMessage: mockSetErrorMessage, setLifeCycleStatus: mockSetLifeCycleStatus, setTransactionId: mockSetTransactionId, }), @@ -82,21 +114,18 @@ describe('useWriteContracts', () => { ); const { result } = renderHook(() => useWriteContracts({ - setErrorMessage: mockSetErrorMessage, setLifeCycleStatus: mockSetLifeCycleStatus, setTransactionId: mockSetTransactionId, }), ); expect(result.current.status).toBe('error'); expect(result.current.writeContracts).toBeInstanceOf(Function); - expect(mockSetErrorMessage).toHaveBeenCalledWith( - 'Something went wrong. Please try again.', - ); expect(mockSetLifeCycleStatus).toHaveBeenCalledWith({ statusName: 'error', statusData: { - code: 'UNCAUGHT_WRITE_WRITE_CONTRACTS_ERROR', + code: 'TmUWCSh02', error: JSON.stringify(uncaughtError), + message: 'Something went wrong. Please try again.', }, }); }); diff --git a/src/transaction/hooks/useWriteContracts.ts b/src/transaction/hooks/useWriteContracts.ts index ac256a1ab5..2a28b13ed2 100644 --- a/src/transaction/hooks/useWriteContracts.ts +++ b/src/transaction/hooks/useWriteContracts.ts @@ -1,12 +1,10 @@ -import type { TransactionExecutionError } from 'viem'; import { useWriteContracts as useWriteContractsWagmi } from 'wagmi/experimental'; import { GENERIC_ERROR_MESSAGE, METHOD_NOT_SUPPORTED_ERROR_SUBSTRING, - UNCAUGHT_WRITE_CONTRACTS_ERROR_CODE, - WRITE_CONTRACTS_ERROR_CODE, } from '../constants'; import type { UseWriteContractsParams } from '../types'; +import { isUserRejectedRequestError } from '../utils/isUserRejectedRequestError'; /** * useWriteContracts: Experimental Wagmi hook for batching transactions. @@ -15,7 +13,6 @@ import type { UseWriteContractsParams } from '../types'; * Does not support EOAs. */ export function useWriteContracts({ - setErrorMessage, setLifeCycleStatus, setTransactionId, }: UseWriteContractsParams) { @@ -30,18 +27,16 @@ export function useWriteContracts({ if (e.message.includes(METHOD_NOT_SUPPORTED_ERROR_SUBSTRING)) { return; } - - if ( - (e as TransactionExecutionError)?.cause?.name === - 'UserRejectedRequestError' - ) { - setErrorMessage('Request denied.'); - } else { - setErrorMessage(GENERIC_ERROR_MESSAGE); - } + const errorMessage = isUserRejectedRequestError(e) + ? 'Request denied.' + : GENERIC_ERROR_MESSAGE; setLifeCycleStatus({ statusName: 'error', - statusData: { code: WRITE_CONTRACTS_ERROR_CODE, error: e.message }, + statusData: { + code: 'TmUWCSh01', // Transaction module UseWriteContracts hook 01 error + error: e.message, + message: errorMessage, + }, }); }, onSuccess: (id) => { @@ -54,11 +49,11 @@ export function useWriteContracts({ setLifeCycleStatus({ statusName: 'error', statusData: { - code: UNCAUGHT_WRITE_CONTRACTS_ERROR_CODE, + code: 'TmUWCSh02', error: JSON.stringify(err), + message: GENERIC_ERROR_MESSAGE, }, }); - setErrorMessage(GENERIC_ERROR_MESSAGE); return { status: 'error', writeContracts: () => {}, diff --git a/src/transaction/types.ts b/src/transaction/types.ts index 84f281f7d9..589adf37ee 100644 --- a/src/transaction/types.ts +++ b/src/transaction/types.ts @@ -48,7 +48,6 @@ export type TransactionContextType = { isToastVisible: boolean; // A boolean indicating if the transaction toast notification is visible. onSubmit: () => void; // A function called when the transaction is submitted. receipt?: TransactionReceipt; // The receipt of the transaction - setErrorMessage: (error: string) => void; // A function to set the error message for the transaction. setIsToastVisible: (isVisible: boolean) => void; // A function to set the visibility of the transaction toast. setLifeCycleStatus: (state: LifeCycleStatus) => void; // A function to set the lifecycle status of the component setTransactionId: (id: string) => void; // A function to set the transaction ID. @@ -71,6 +70,7 @@ type PaymasterService = { export type TransactionError = { code: string; // The error code representing the type of transaction error. error: string; // The error message providing details about the transaction error. + message: string; // The error message providing details about the transaction error. }; export type TransactionProviderReact = { @@ -172,14 +172,12 @@ export type UseCallsStatusParams = { }; export type UseWriteContractParams = { - setErrorMessage: (error: string) => void; setLifeCycleStatus: (state: LifeCycleStatus) => void; setTransactionHashArray: (ids: Address[]) => void; transactionHashArray?: Address[]; }; export type UseWriteContractsParams = { - setErrorMessage: (error: string) => void; setLifeCycleStatus: (state: LifeCycleStatus) => void; setTransactionId: (id: string) => void; }; diff --git a/src/transaction/utils/isUserRejectedRequestError.test.ts b/src/transaction/utils/isUserRejectedRequestError.test.ts new file mode 100644 index 0000000000..ce05b640bf --- /dev/null +++ b/src/transaction/utils/isUserRejectedRequestError.test.ts @@ -0,0 +1,21 @@ +import { isUserRejectedRequestError } from './isUserRejectedRequestError'; + +describe('isUserRejectedRequestError', () => { + it('should return true if error is UserRejectedRequestError', () => { + const error = { + cause: { + name: 'UserRejectedRequestError', + }, + }; + expect(isUserRejectedRequestError(error)).toBe(true); + }); + + it('should return false if error is not UserRejectedRequestError', () => { + const error = { + cause: { + name: 'Error', + }, + }; + expect(isUserRejectedRequestError(error)).toBe(false); + }); +}); diff --git a/src/transaction/utils/isUserRejectedRequestError.ts b/src/transaction/utils/isUserRejectedRequestError.ts new file mode 100644 index 0000000000..1fbc185484 --- /dev/null +++ b/src/transaction/utils/isUserRejectedRequestError.ts @@ -0,0 +1,8 @@ +import type { TransactionExecutionError } from 'viem'; + +export function isUserRejectedRequestError(err: unknown) { + return ( + (err as TransactionExecutionError)?.cause?.name === + 'UserRejectedRequestError' + ); +} diff --git a/vitest.config.ts b/vitest.config.ts index 03347a424a..e694fafe59 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -22,10 +22,10 @@ export default defineConfig({ ], reportOnFailure: true, thresholds: { - statements: 99.43, - branches: 98.63, - functions: 96.72, - lines: 99.43, + statements: 99.47, + branches: 98.79, + functions: 96.74, + lines: 99.47, }, }, environment: 'jsdom',