From 7eb02d572090748b1ce9225b093bf35d1a3f561c Mon Sep 17 00:00:00 2001 From: Leonardo Zizzamia Date: Wed, 14 Aug 2024 13:58:39 -0700 Subject: [PATCH 1/2] chore: more error cleanup --- .../components/TransactionProvider.test.tsx | 37 +++++++---- .../components/TransactionProvider.tsx | 65 ++++++++++--------- .../hooks/useTransactionReceipts.ts | 0 3 files changed, 62 insertions(+), 40 deletions(-) create mode 100644 src/transaction/hooks/useTransactionReceipts.ts diff --git a/src/transaction/components/TransactionProvider.test.tsx b/src/transaction/components/TransactionProvider.test.tsx index 7e0fc9e370..3c7cb18d60 100644 --- a/src/transaction/components/TransactionProvider.test.tsx +++ b/src/transaction/components/TransactionProvider.test.tsx @@ -5,6 +5,7 @@ import { useSwitchChain, useWaitForTransactionReceipt, } from 'wagmi'; +import { METHOD_NOT_SUPPORTED_ERROR_SUBSTRING } from '../constants'; import { useCallsStatus } from '../hooks/useCallsStatus'; import { useWriteContract } from '../hooks/useWriteContract'; import { useWriteContracts } from '../hooks/useWriteContracts'; @@ -235,16 +236,13 @@ describe('TransactionProvider', () => { statusWriteContracts: 'IDLE', writeContractsAsync: writeContractsAsyncMock, }); - render( , ); - const button = screen.getByText('Submit'); fireEvent.click(button); - await waitFor(() => { const errorMessage = screen.getByTestId('context-value-errorMessage'); expect(errorMessage.textContent).toBe('Request denied.'); @@ -259,7 +257,6 @@ describe('TransactionProvider', () => { (useCallsStatus as ReturnType).mockReturnValue({ transactionHash: 'hash', }); - render( { , ); - await waitFor(() => { expect(onSuccessMock).toHaveBeenCalledWith({ transactionReceipts: [{ status: 'success' }], @@ -283,21 +279,43 @@ describe('TransactionProvider', () => { switchChainAsync: switchChainAsyncMock, }); (useAccount as ReturnType).mockReturnValue({ chainId: 1 }); - render( , ); - const button = screen.getByText('Submit'); fireEvent.click(button); - await waitFor(() => { expect(switchChainAsyncMock).toHaveBeenCalledWith({ chainId: 2 }); }); }); + it('should call fallbackToWriteContract when executeContracts fails', async () => { + const writeContractsAsyncMock = vi + .fn() + .mockRejectedValue(new Error(METHOD_NOT_SUPPORTED_ERROR_SUBSTRING)); + const writeContractAsyncMock = vi.fn(); + (useWriteContracts as ReturnType).mockReturnValue({ + statusWriteContracts: 'IDLE', + writeContractsAsync: writeContractsAsyncMock, + }); + (useWriteContract as ReturnType).mockReturnValue({ + status: 'IDLE', + writeContractAsync: writeContractAsyncMock, + }); + render( + + + , + ); + const button = screen.getByText('Submit'); + fireEvent.click(button); + await waitFor(() => { + expect(writeContractAsyncMock).toHaveBeenCalled(); + }); + }); + it('should handle generic error during fallback', async () => { const writeContractsAsyncMock = vi .fn() @@ -313,16 +331,13 @@ describe('TransactionProvider', () => { status: 'IDLE', writeContractAsync: writeContractAsyncMock, }); - render( , ); - const button = screen.getByText('Submit'); fireEvent.click(button); - await waitFor(() => { expect(screen.getByTestId('context-value-errorMessage').textContent).toBe( 'Something went wrong. Please try again.', diff --git a/src/transaction/components/TransactionProvider.tsx b/src/transaction/components/TransactionProvider.tsx index 086b1fd6e0..b487ae0eb7 100644 --- a/src/transaction/components/TransactionProvider.tsx +++ b/src/transaction/components/TransactionProvider.tsx @@ -118,8 +118,14 @@ export function TransactionProvider({ }); receipts.push(txnReceipt); } catch (err) { - console.error('getTransactionReceiptsError', err); - setErrorMessage(GENERIC_ERROR_MESSAGE); + setLifeCycleStatus({ + statusName: 'error', + statusData: { + code: 'TmTPc01', // Transaction module TransactionProvider component 01 error + error: JSON.stringify(err), + message: GENERIC_ERROR_MESSAGE, + }, + }); } } setReceiptArray(receipts); @@ -144,7 +150,14 @@ export function TransactionProvider({ const errorMessage = isUserRejectedRequestError(err) ? 'Request denied.' : GENERIC_ERROR_MESSAGE; - setErrorMessage(errorMessage); + setLifeCycleStatus({ + statusName: 'error', + statusData: { + code: 'TmTPc02', // Transaction module TransactionProvider component 02 error + error: JSON.stringify(err), + message: errorMessage, + }, + }); } } }, [contracts, writeContractAsync]); @@ -165,30 +178,6 @@ export function TransactionProvider({ }); }, [writeContractsAsync, contracts, capabilities]); - const handleSubmitErrors = useCallback( - async (err: unknown) => { - // handles EOA writeContracts error - // (fallback to writeContract) - if ( - err instanceof Error && - err.message.includes(METHOD_NOT_SUPPORTED_ERROR_SUBSTRING) - ) { - try { - await fallbackToWriteContract(); - } catch (_err) { - setErrorMessage(GENERIC_ERROR_MESSAGE); - } - // handles user rejected request error - } else if (isUserRejectedRequestError(err)) { - setErrorMessage('Request denied.'); - // handles generic error - } else { - setErrorMessage(GENERIC_ERROR_MESSAGE); - } - }, - [fallbackToWriteContract], - ); - const handleSubmit = useCallback(async () => { setErrorMessage(''); setIsToastVisible(true); @@ -196,9 +185,27 @@ export function TransactionProvider({ await switchChain(chainId); await executeContracts(); } catch (err) { - await handleSubmitErrors(err); + // handles EOA writeContracts error (fallback to writeContract) + if ( + err instanceof Error && + err.message.includes(METHOD_NOT_SUPPORTED_ERROR_SUBSTRING) + ) { + await fallbackToWriteContract(); + return; + } + const errorMessage = isUserRejectedRequestError(err) + ? 'Request denied.' + : GENERIC_ERROR_MESSAGE; + setLifeCycleStatus({ + statusName: 'error', + statusData: { + code: 'TmTPc03', // Transaction module TransactionProvider component 03 error + error: JSON.stringify(err), + message: errorMessage, + }, + }); } - }, [chainId, executeContracts, handleSubmitErrors, switchChain]); + }, [chainId, executeContracts, fallbackToWriteContract, switchChain]); useEffect(() => { if (receiptArray?.length) { diff --git a/src/transaction/hooks/useTransactionReceipts.ts b/src/transaction/hooks/useTransactionReceipts.ts new file mode 100644 index 0000000000..e69de29bb2 From 56fbe088f36685bda7b3752fced3240134268405 Mon Sep 17 00:00:00 2001 From: Leonardo Zizzamia Date: Wed, 14 Aug 2024 14:12:37 -0700 Subject: [PATCH 2/2] tests --- .../components/TransactionProvider.test.tsx | 42 ++++++++++++++++++- .../components/TransactionProvider.tsx | 3 ++ src/transaction/types.ts | 1 + vitest.config.ts | 6 +-- 4 files changed, 47 insertions(+), 5 deletions(-) diff --git a/src/transaction/components/TransactionProvider.test.tsx b/src/transaction/components/TransactionProvider.test.tsx index 3c7cb18d60..9c5fd099d8 100644 --- a/src/transaction/components/TransactionProvider.test.tsx +++ b/src/transaction/components/TransactionProvider.test.tsx @@ -48,6 +48,7 @@ const TestComponent = () => { + {context.errorCode} {context.errorMessage} @@ -172,8 +173,10 @@ describe('TransactionProvider', () => { const button = screen.getByText('Submit'); fireEvent.click(button); await waitFor(() => { - const testComponent = screen.getByTestId('context-value-errorMessage'); - expect(testComponent.textContent).toBe( + expect(screen.getByTestId('context-value-errorCode').textContent).toBe( + 'TmTPc03', + ); + expect(screen.getByTestId('context-value-errorMessage').textContent).toBe( 'Something went wrong. Please try again.', ); }); @@ -339,6 +342,41 @@ describe('TransactionProvider', () => { const button = screen.getByText('Submit'); fireEvent.click(button); await waitFor(() => { + expect(screen.getByTestId('context-value-errorCode').textContent).toBe( + 'TmTPc03', + ); + expect(screen.getByTestId('context-value-errorMessage').textContent).toBe( + 'Something went wrong. Please try again.', + ); + }); + }); + + it('should call setLifeCycleStatus when calling fallbackToWriteContract when executeContracts fails', async () => { + const writeContractsAsyncMock = vi + .fn() + .mockRejectedValue(new Error(METHOD_NOT_SUPPORTED_ERROR_SUBSTRING)); + const writeContractAsyncMock = vi + .fn() + .mockRejectedValue(new Error('Basic error')); + (useWriteContracts as ReturnType).mockReturnValue({ + statusWriteContracts: 'IDLE', + writeContractsAsync: writeContractsAsyncMock, + }); + (useWriteContract as ReturnType).mockReturnValue({ + status: 'IDLE', + writeContractAsync: writeContractAsyncMock, + }); + render( + + + , + ); + const button = screen.getByText('Submit'); + fireEvent.click(button); + await waitFor(() => { + expect(screen.getByTestId('context-value-errorCode').textContent).toBe( + 'TmTPc02', + ); expect(screen.getByTestId('context-value-errorMessage').textContent).toBe( 'Something went wrong. Please try again.', ); diff --git a/src/transaction/components/TransactionProvider.tsx b/src/transaction/components/TransactionProvider.tsx index b487ae0eb7..99c2326abd 100644 --- a/src/transaction/components/TransactionProvider.tsx +++ b/src/transaction/components/TransactionProvider.tsx @@ -56,6 +56,7 @@ export function TransactionProvider({ const account = useAccount(); const config = useConfig(); const [errorMessage, setErrorMessage] = useState(''); + const [errorCode, setErrorCode] = useState(''); const [isToastVisible, setIsToastVisible] = useState(false); const [lifeCycleStatus, setLifeCycleStatus] = useState({ statusName: 'init', @@ -96,6 +97,7 @@ export function TransactionProvider({ // Emit Error if (lifeCycleStatus.statusName === 'error') { setErrorMessage(lifeCycleStatus.statusData.message); + setErrorCode(lifeCycleStatus.statusData.code); onError?.(lifeCycleStatus.statusData); } // Emit State @@ -219,6 +221,7 @@ export function TransactionProvider({ address, chainId, contracts, + errorCode, errorMessage, hasPaymaster: !!capabilities?.paymasterService?.url, isLoading: callStatus === 'PENDING', diff --git a/src/transaction/types.ts b/src/transaction/types.ts index 589adf37ee..b7c3ec95ce 100644 --- a/src/transaction/types.ts +++ b/src/transaction/types.ts @@ -42,6 +42,7 @@ export type TransactionContextType = { address: Address; // The wallet address involved in the transaction. chainId?: number; // The chainId for the transaction. contracts: ContractFunctionParameters[]; // An array of contracts for the transaction. + errorCode?: string; // An error code string if the transaction encounters an issue. errorMessage?: string; // An error message string if the transaction encounters an issue. hasPaymaster?: boolean; // A boolean indicating if app has paymaster configured isLoading: boolean; // A boolean indicating if the transaction is currently loading. diff --git a/vitest.config.ts b/vitest.config.ts index b98eac64fe..922b42015c 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -22,10 +22,10 @@ export default defineConfig({ ], reportOnFailure: true, thresholds: { - statements: 99.5, - branches: 98.96, + statements: 99.56, + branches: 98.97, functions: 97.19, - lines: 99.5, + lines: 99.56, }, }, environment: 'jsdom',