diff --git a/src/transaction/components/TransactionProvider.test.tsx b/src/transaction/components/TransactionProvider.test.tsx index 7e0fc9e370..9c5fd099d8 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'; @@ -47,6 +48,7 @@ const TestComponent = () => { + {context.errorCode} {context.errorMessage} @@ -171,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.', ); }); @@ -235,16 +239,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 +260,6 @@ describe('TransactionProvider', () => { (useCallsStatus as ReturnType).mockReturnValue({ transactionHash: 'hash', }); - render( { , ); - await waitFor(() => { expect(onSuccessMock).toHaveBeenCalledWith({ transactionReceipts: [{ status: 'success' }], @@ -283,21 +282,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,17 +334,49 @@ describe('TransactionProvider', () => { status: 'IDLE', writeContractAsync: writeContractAsyncMock, }); - render( , ); - 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 086b1fd6e0..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 @@ -118,8 +120,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 +152,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 +180,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 +187,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) { @@ -212,6 +221,7 @@ export function TransactionProvider({ address, chainId, contracts, + errorCode, errorMessage, hasPaymaster: !!capabilities?.paymasterService?.url, isLoading: callStatus === 'PENDING', diff --git a/src/transaction/hooks/useTransactionReceipts.ts b/src/transaction/hooks/useTransactionReceipts.ts new file mode 100644 index 0000000000..e69de29bb2 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',