Skip to content

Commit

Permalink
chore: more error cleanup (#1052)
Browse files Browse the repository at this point in the history
  • Loading branch information
Zizzamia authored Aug 14, 2024
1 parent c443f29 commit 3ca42c3
Show file tree
Hide file tree
Showing 5 changed files with 108 additions and 44 deletions.
77 changes: 65 additions & 12 deletions src/transaction/components/TransactionProvider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -47,6 +48,7 @@ const TestComponent = () => {
<button type="button" onClick={context.onSubmit}>
Submit
</button>
<span data-testid="context-value-errorCode">{context.errorCode}</span>
<span data-testid="context-value-errorMessage">
{context.errorMessage}
</span>
Expand Down Expand Up @@ -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.',
);
});
Expand Down Expand Up @@ -235,16 +239,13 @@ describe('TransactionProvider', () => {
statusWriteContracts: 'IDLE',
writeContractsAsync: writeContractsAsyncMock,
});

render(
<TransactionProvider address="0x123" contracts={[]}>
<TestComponent />
</TransactionProvider>,
);

const button = screen.getByText('Submit');
fireEvent.click(button);

await waitFor(() => {
const errorMessage = screen.getByTestId('context-value-errorMessage');
expect(errorMessage.textContent).toBe('Request denied.');
Expand All @@ -259,7 +260,6 @@ describe('TransactionProvider', () => {
(useCallsStatus as ReturnType<typeof vi.fn>).mockReturnValue({
transactionHash: 'hash',
});

render(
<TransactionProvider
address="0x123"
Expand All @@ -269,7 +269,6 @@ describe('TransactionProvider', () => {
<TestComponent />
</TransactionProvider>,
);

await waitFor(() => {
expect(onSuccessMock).toHaveBeenCalledWith({
transactionReceipts: [{ status: 'success' }],
Expand All @@ -283,21 +282,43 @@ describe('TransactionProvider', () => {
switchChainAsync: switchChainAsyncMock,
});
(useAccount as ReturnType<typeof vi.fn>).mockReturnValue({ chainId: 1 });

render(
<TransactionProvider address="0x123" chainId={2} contracts={[]}>
<TestComponent />
</TransactionProvider>,
);

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<typeof vi.fn>).mockReturnValue({
statusWriteContracts: 'IDLE',
writeContractsAsync: writeContractsAsyncMock,
});
(useWriteContract as ReturnType<typeof vi.fn>).mockReturnValue({
status: 'IDLE',
writeContractAsync: writeContractAsyncMock,
});
render(
<TransactionProvider address="0x123" contracts={[{}]}>
<TestComponent />
</TransactionProvider>,
);
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()
Expand All @@ -313,17 +334,49 @@ describe('TransactionProvider', () => {
status: 'IDLE',
writeContractAsync: writeContractAsyncMock,
});

render(
<TransactionProvider address="0x123" contracts={[{}]}>
<TestComponent />
</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<typeof vi.fn>).mockReturnValue({
statusWriteContracts: 'IDLE',
writeContractsAsync: writeContractsAsyncMock,
});
(useWriteContract as ReturnType<typeof vi.fn>).mockReturnValue({
status: 'IDLE',
writeContractAsync: writeContractAsyncMock,
});
render(
<TransactionProvider address="0x123" contracts={[{}]}>
<TestComponent />
</TransactionProvider>,
);
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.',
);
Expand Down
68 changes: 39 additions & 29 deletions src/transaction/components/TransactionProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<LifeCycleStatus>({
statusName: 'init',
Expand Down Expand Up @@ -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
Expand All @@ -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);
Expand All @@ -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]);
Expand All @@ -165,40 +180,34 @@ 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);
try {
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) {
Expand All @@ -212,6 +221,7 @@ export function TransactionProvider({
address,
chainId,
contracts,
errorCode,
errorMessage,
hasPaymaster: !!capabilities?.paymasterService?.url,
isLoading: callStatus === 'PENDING',
Expand Down
Empty file.
1 change: 1 addition & 0 deletions src/transaction/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 3 additions & 3 deletions vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down

0 comments on commit 3ca42c3

Please sign in to comment.