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',