From a0f709672dbd52a44314a8426a38da59ec810430 Mon Sep 17 00:00:00 2001 From: "dev.uche" Date: Mon, 1 Dec 2025 02:19:00 +0100 Subject: [PATCH] feat: Improve error messages for token transfers - Add token transfer specific error types to ContractErrorType enum - Enhance parseContractError to handle ERC20 transfer errors with specific messages - Improve error messages in useTokenApproval hook with actionable guidance - Update error handling in play page and rewards page for better UX - Add comprehensive tests for error detection and parsing - Create ERROR_HANDLING.md documentation Error types added: - INSUFFICIENT_TOKEN_BALANCE - INSUFFICIENT_ALLOWANCE - TOKEN_TRANSFER_FAILED - TOKEN_APPROVAL_FAILED - TOKEN_APPROVAL_REJECTED - ZERO_AMOUNT_TRANSFER - TRANSFER_TO_ZERO_ADDRESS All error messages are now user-friendly, actionable, and provide clear guidance on how to resolve issues. --- frontend/docs/ERROR_HANDLING.md | 167 +++++++++++++ frontend/docs/TOKEN_APPROVAL.md | 42 +++- frontend/src/app/play/page.tsx | 41 ++- frontend/src/app/rewards/page.tsx | 21 +- frontend/src/hooks/useTokenApproval.ts | 97 +++++--- .../utils/__tests__/contractErrors.test.ts | 233 ++++++++++++++++++ frontend/src/utils/contractErrors.ts | 129 ++++++++++ 7 files changed, 677 insertions(+), 53 deletions(-) create mode 100644 frontend/docs/ERROR_HANDLING.md create mode 100644 frontend/src/utils/__tests__/contractErrors.test.ts diff --git a/frontend/docs/ERROR_HANDLING.md b/frontend/docs/ERROR_HANDLING.md new file mode 100644 index 0000000..dddcfc2 --- /dev/null +++ b/frontend/docs/ERROR_HANDLING.md @@ -0,0 +1,167 @@ +# Error Handling for Token Transfers + +## Overview + +This document describes the improved error handling system for token transfers and approvals in the application. The system provides user-friendly, actionable error messages that help users understand what went wrong and how to fix it. + +## Error Types + +### Token Transfer Errors + +The system recognizes and handles the following token transfer errors: + +#### `INSUFFICIENT_TOKEN_BALANCE` +- **Message**: "Insufficient token balance. Please ensure you have enough cUSD in your wallet." +- **Cause**: User doesn't have enough tokens to complete the transfer +- **Solution**: User needs to add more cUSD tokens to their wallet + +#### `INSUFFICIENT_ALLOWANCE` +- **Message**: "Token approval required. Please approve the transaction to allow the contract to spend your tokens." +- **Cause**: Contract doesn't have permission to spend user's tokens +- **Solution**: User needs to approve the contract (handled automatically in the flow) + +#### `TOKEN_TRANSFER_FAILED` +- **Message**: "Token transfer failed. Please check your balance and try again." +- **Cause**: Generic transfer failure (could be network, gas, or contract issue) +- **Solution**: Check balance, network connection, and try again + +#### `ZERO_AMOUNT_TRANSFER` +- **Message**: "Transfer amount must be greater than zero." +- **Cause**: Attempted to transfer zero tokens +- **Solution**: Ensure transfer amount is greater than zero + +#### `TRANSFER_TO_ZERO_ADDRESS` +- **Message**: "Cannot transfer tokens to an invalid address." +- **Cause**: Attempted to transfer to zero address +- **Solution**: Use a valid recipient address + +### Token Approval Errors + +#### `TOKEN_APPROVAL_REJECTED` +- **Message**: "Token approval was cancelled. Please approve the transaction to continue." +- **Cause**: User rejected the approval transaction +- **Solution**: User needs to approve the transaction when prompted + +#### `TOKEN_APPROVAL_FAILED` +- **Message**: "Token approval failed. Please try again or check your wallet connection." +- **Cause**: Approval transaction failed (network, gas, or other issue) +- **Solution**: Check wallet connection and try again + +## Error Detection + +The system uses several helper functions to detect specific error types: + +### `isTokenTransferError(error)` +Detects if an error is related to token transfers by checking: +- Error message contains "transfer", "erc20", or "token" +- Error reason contains transfer-related keywords +- Error code indicates transfer failure + +### `isInsufficientAllowanceError(error)` +Detects insufficient allowance errors by checking: +- Error message contains "allowance" +- Error message contains "insufficient allowance" or "allowance too low" +- Error code is `INSUFFICIENT_ALLOWANCE` + +### `isTokenApprovalError(error)` +Detects token approval errors by checking: +- Error message contains "approve" or "approval" +- Error reason contains approval-related keywords +- Error code indicates approval failure + +## Error Parsing + +The `parseContractError` function automatically: +1. Detects the error type +2. Extracts relevant information +3. Returns a user-friendly message +4. Provides an error code for programmatic handling + +### Example Usage + +```typescript +import { parseContractError } from '@/utils/contractErrors'; + +try { + await transferTokens(amount); +} catch (error) { + const { message, code } = parseContractError(error); + // message: "Insufficient token balance. Please ensure you have enough cUSD in your wallet." + // code: ContractErrorType.INSUFFICIENT_TOKEN_BALANCE + toast.error(message); +} +``` + +## Integration Points + +### useTokenApproval Hook + +The hook automatically parses and enhances errors: + +```typescript +const { approve, error } = useTokenApproval(); + +try { + await approve(); +} catch (error) { + // Error is already parsed with user-friendly message + console.error(error.message); // "Token approval was cancelled..." +} +``` + +### Play Page + +The play page provides specific error messages based on error type: + +```typescript +if (error?.code === 'INSUFFICIENT_ALLOWANCE') { + toast.error('Token approval required. Please approve the transaction...'); +} else if (error?.code === 'INSUFFICIENT_TOKEN_BALANCE') { + toast.error('Insufficient cUSD balance. Please add more tokens...'); +} +``` + +### Rewards Page + +The rewards page handles transfer errors for reward claims: + +```typescript +if (error?.code === 'TOKEN_TRANSFER_FAILED') { + toast.error('Token transfer failed. The contract may not have enough funds...'); +} +``` + +## Best Practices + +1. **Always use parseContractError**: Don't display raw error messages to users +2. **Provide actionable messages**: Tell users what they can do to fix the issue +3. **Handle specific error codes**: Use error codes for conditional logic +4. **Log original errors**: Keep original errors for debugging while showing friendly messages +5. **Test error scenarios**: Ensure all error paths are tested + +## Testing + +Error handling is tested in `src/utils/__tests__/contractErrors.test.ts`: + +- Token transfer error detection +- Insufficient allowance detection +- Token approval error detection +- Error message parsing +- User-friendly message generation + +## Error Message Guidelines + +1. **Be specific**: Tell users exactly what went wrong +2. **Be actionable**: Provide guidance on how to fix the issue +3. **Be friendly**: Use conversational, non-technical language +4. **Be concise**: Keep messages short and to the point +5. **Include context**: Mention relevant details (e.g., "cUSD" instead of just "tokens") + +## Future Improvements + +- [ ] Add error recovery suggestions +- [ ] Implement retry mechanisms for transient errors +- [ ] Add error analytics to track common issues +- [ ] Create error code reference documentation +- [ ] Add i18n support for error messages + diff --git a/frontend/docs/TOKEN_APPROVAL.md b/frontend/docs/TOKEN_APPROVAL.md index c39ccdd..3caf2df 100644 --- a/frontend/docs/TOKEN_APPROVAL.md +++ b/frontend/docs/TOKEN_APPROVAL.md @@ -177,12 +177,29 @@ The hook provides granular loading states for better UX: ## Error Handling -The hook handles various error scenarios: +The hook handles various error scenarios with improved, user-friendly messages: -- **Wallet not connected**: Throws error when trying to approve without wallet -- **User rejection**: Detects when user rejects transaction in wallet -- **Transaction failure**: Handles failed transactions gracefully -- **Network errors**: Catches and reports network-related errors +### Token Transfer Errors + +- **Insufficient Token Balance**: "Insufficient token balance. Please ensure you have enough cUSD in your wallet." +- **Insufficient Allowance**: "Token approval required. Please approve the transaction to allow the contract to spend your tokens." +- **Transfer Failed**: "Token transfer failed. Please check your balance and try again." +- **Zero Amount**: "Transfer amount must be greater than zero." +- **Invalid Address**: "Cannot transfer tokens to an invalid address." + +### Token Approval Errors + +- **Approval Rejected**: "Token approval was cancelled. Please approve the transaction to continue." +- **Approval Failed**: "Token approval failed. Please try again or check your wallet connection." + +### General Errors + +- **Wallet not connected**: "Wallet not connected. Please connect your wallet to approve tokens." +- **User rejection**: Detects when user rejects transaction in wallet with clear messaging +- **Transaction failure**: Handles failed transactions gracefully with specific error messages +- **Network errors**: "Network error. Please check your connection and try again." + +All errors are parsed using the `parseContractError` utility which provides consistent, user-friendly error messages across the application. ## UI Integration @@ -235,8 +252,11 @@ The test suite covers: - Detection of approval needs - Loading states (allowance check, approving, waiting) - Approval function calls with correct parameters -- Error handling +- Error handling with improved error messages - Success states +- Token transfer error detection +- Token approval error detection +- User-friendly error message parsing ## Best Practices @@ -270,6 +290,16 @@ The test suite covers: - Verify ABI includes the `approve` function - Check that contract addresses are correct +### Common Error Messages + +If you encounter specific error messages, here's what they mean: + +- **"Insufficient token balance"**: The user doesn't have enough cUSD tokens. They need to add more tokens to their wallet. +- **"Token approval required"**: The user needs to approve the contract to spend their tokens. This happens automatically in the flow. +- **"Token transfer failed"**: The transfer transaction failed. This could be due to network issues, insufficient gas, or contract issues. +- **"Transaction was cancelled"**: The user rejected the transaction in their wallet. They can try again. +- **"Network error"**: There's a problem with the network connection. Check internet connectivity. + ## Security Considerations 1. **Unlimited approval**: The default approval uses `maxUint256` for convenience. For production, consider using specific amounts. diff --git a/frontend/src/app/play/page.tsx b/frontend/src/app/play/page.tsx index 92e0e04..c4d0d41 100644 --- a/frontend/src/app/play/page.tsx +++ b/frontend/src/app/play/page.tsx @@ -111,10 +111,20 @@ export default function PlayPage() { toast.dismiss('approval-waiting'); console.error('Approval error:', approvalError); - if (approvalError?.message?.includes('User rejected') || approvalError?.message?.includes('rejected')) { - toast.error('Approval cancelled'); + // Use the enhanced error message from the hook + const errorMessage = approvalError?.message || 'Failed to approve tokens. Please try again.'; + + // Provide specific guidance based on error type + if (approvalError?.code === 'TOKEN_APPROVAL_REJECTED' || + approvalError?.message?.includes('cancelled') || + approvalError?.message?.includes('rejected')) { + toast.error('Token approval was cancelled. Please approve the transaction to start the game.'); + } else if (approvalError?.code === 'WALLET_NOT_CONNECTED') { + toast.error('Wallet not connected. Please connect your wallet and try again.'); + } else if (approvalError?.code === 'NETWORK_ERROR') { + toast.error('Network error. Please check your connection and try again.'); } else { - toast.error(approvalError?.message || 'Failed to approve tokens'); + toast.error(errorMessage); } setIsStarting(false); return; @@ -134,13 +144,26 @@ export default function PlayPage() { toast.dismiss('approval-loading'); toast.dismiss('approval-waiting'); - if (error?.message?.includes('approval required')) { - // Approval is still needed, but we already tried - toast.error('Please wait for approval to complete before starting the game'); - } else if (error?.message?.includes('User rejected') || error?.message?.includes('rejected')) { - toast.error('Transaction cancelled'); + const errorMessage = error?.message || 'Failed to start game. Please try again.'; + + // Provide specific guidance based on error type + if (error?.code === 'INSUFFICIENT_ALLOWANCE' || error?.message?.includes('approval required')) { + toast.error('Token approval required. Please approve the transaction to allow the game to use your tokens.'); + } else if (error?.code === 'INSUFFICIENT_TOKEN_BALANCE' || error?.message?.includes('insufficient balance')) { + toast.error('Insufficient cUSD balance. Please add more tokens to your wallet.'); + } else if (error?.code === 'TOKEN_TRANSFER_FAILED' || error?.message?.includes('transfer failed')) { + toast.error('Token transfer failed. Please check your balance and try again.'); + } else if (error?.code === 'TRANSACTION_REJECTED' || + error?.message?.includes('User rejected') || + error?.message?.includes('rejected') || + error?.message?.includes('cancelled')) { + toast.error('Transaction was cancelled. Please try again when ready.'); + } else if (error?.code === 'WALLET_NOT_CONNECTED') { + toast.error('Wallet not connected. Please connect your wallet and try again.'); + } else if (error?.code === 'NETWORK_ERROR') { + toast.error('Network error. Please check your connection and try again.'); } else { - toast.error(error?.message || 'Failed to start game'); + toast.error(errorMessage); } setIsStarting(false); } diff --git a/frontend/src/app/rewards/page.tsx b/frontend/src/app/rewards/page.tsx index 6cb22d4..05c94be 100644 --- a/frontend/src/app/rewards/page.tsx +++ b/frontend/src/app/rewards/page.tsx @@ -204,7 +204,26 @@ export default function RewardsPage() { } catch (error: any) { console.error('Error claiming rewards:', error); toast.dismiss(); - toast.error(error?.message || 'Failed to claim rewards'); + + const errorMessage = error?.message || 'Failed to claim rewards. Please try again.'; + + // Provide specific guidance based on error type + if (error?.code === 'TOKEN_TRANSFER_FAILED' || error?.message?.includes('transfer failed')) { + toast.error('Token transfer failed. The contract may not have enough funds. Please contact support.'); + } else if (error?.code === 'INSUFFICIENT_TOKEN_BALANCE' || error?.message?.includes('insufficient balance')) { + toast.error('Insufficient contract balance. Please contact support.'); + } else if (error?.code === 'TRANSACTION_REJECTED' || + error?.message?.includes('User rejected') || + error?.message?.includes('rejected') || + error?.message?.includes('cancelled')) { + toast.error('Transaction was cancelled. Please try again when ready.'); + } else if (error?.code === 'WALLET_NOT_CONNECTED') { + toast.error('Wallet not connected. Please connect your wallet and try again.'); + } else if (error?.code === 'NETWORK_ERROR') { + toast.error('Network error. Please check your connection and try again.'); + } else { + toast.error(errorMessage); + } setIsClaimingRewards(false); } }; diff --git a/frontend/src/hooks/useTokenApproval.ts b/frontend/src/hooks/useTokenApproval.ts index 246cc7c..6a03700 100644 --- a/frontend/src/hooks/useTokenApproval.ts +++ b/frontend/src/hooks/useTokenApproval.ts @@ -3,6 +3,7 @@ import { CONTRACTS, GAME_CONSTANTS } from '@/config/contracts'; import { parseEther, maxUint256, encodeFunctionData } from 'viem'; import { useCallback, useMemo } from 'react'; import { useCeloMiniPay } from './useCeloMiniPay'; +import { parseContractError, ContractErrorType } from '@/utils/contractErrors'; /** * Hook for managing token approvals with proper loading states @@ -89,31 +90,42 @@ export function useTokenApproval() { */ const approve = useCallback(async (amount?: bigint) => { if (!address) { - throw new Error('Wallet not connected'); + const error = new Error('Wallet not connected. Please connect your wallet to approve tokens.'); + (error as any).code = ContractErrorType.WALLET_NOT_CONNECTED; + throw error; } const approvalAmount = amount || maxUint256; - if (isMiniPay && sendTransaction) { - // For MiniPay, encode the approval transaction - const data = encodeFunctionData({ + try { + if (isMiniPay && sendTransaction) { + // For MiniPay, encode the approval transaction + const data = encodeFunctionData({ + abi: CONTRACTS.cUSD.abi, + functionName: 'approve', + args: [CONTRACTS.triviaGameV2.address, approvalAmount], + }); + + return sendTransaction({ + to: CONTRACTS.cUSD.address, + data, + }); + } + + return approveToken({ + address: CONTRACTS.cUSD.address, abi: CONTRACTS.cUSD.abi, functionName: 'approve', args: [CONTRACTS.triviaGameV2.address, approvalAmount], }); - - return sendTransaction({ - to: CONTRACTS.cUSD.address, - data, - }); + } catch (error: any) { + // Parse and enhance error message + const parsedError = parseContractError(error); + const enhancedError = new Error(parsedError.message); + (enhancedError as any).code = parsedError.code; + (enhancedError as any).originalError = error; + throw enhancedError; } - - return approveToken({ - address: CONTRACTS.cUSD.address, - abi: CONTRACTS.cUSD.abi, - functionName: 'approve', - args: [CONTRACTS.triviaGameV2.address, approvalAmount], - }); }, [address, isMiniPay, sendTransaction, approveToken]); /** @@ -122,31 +134,42 @@ export function useTokenApproval() { */ const ensureApproval = useCallback(async (callback?: () => void | Promise) => { if (!address) { - throw new Error('Wallet not connected'); + const error = new Error('Wallet not connected. Please connect your wallet first.'); + (error as any).code = ContractErrorType.WALLET_NOT_CONNECTED; + throw error; } - // Refetch allowance to get latest value - const { data: currentAllowance } = await refetchAllowance(); - - if (!currentAllowance || currentAllowance < requiredAmount) { - // Approval needed - await approve(); - - // Wait for approval to complete - // Note: In a real scenario, you might want to poll for the allowance update - // For now, we'll rely on the transaction receipt - - if (callback) { - // Wait a bit for the approval to be processed - setTimeout(async () => { + try { + // Refetch allowance to get latest value + const { data: currentAllowance } = await refetchAllowance(); + + if (!currentAllowance || currentAllowance < requiredAmount) { + // Approval needed + await approve(); + + // Wait for approval to complete + // Note: In a real scenario, you might want to poll for the allowance update + // For now, we'll rely on the transaction receipt + + if (callback) { + // Wait a bit for the approval to be processed + setTimeout(async () => { + await callback(); + }, 2000); + } + } else { + // Already approved, execute callback immediately + if (callback) { await callback(); - }, 2000); - } - } else { - // Already approved, execute callback immediately - if (callback) { - await callback(); + } } + } catch (error: any) { + // Parse and enhance error message + const parsedError = parseContractError(error); + const enhancedError = new Error(parsedError.message); + (enhancedError as any).code = parsedError.code; + (enhancedError as any).originalError = error; + throw enhancedError; } }, [address, requiredAmount, approve, refetchAllowance]); diff --git a/frontend/src/utils/__tests__/contractErrors.test.ts b/frontend/src/utils/__tests__/contractErrors.test.ts new file mode 100644 index 0000000..fede1a3 --- /dev/null +++ b/frontend/src/utils/__tests__/contractErrors.test.ts @@ -0,0 +1,233 @@ +/** + * Tests for contract error handling utilities + * + * To run these tests, install the following dependencies: + * npm install --save-dev @testing-library/react @testing-library/jest-dom jest jest-environment-jsdom @types/jest + */ + +import { + parseContractError, + ContractErrorType, + isTokenTransferError, + isInsufficientAllowanceError, + isTokenApprovalError, + isUserRejectedError, + isInsufficientFundsError, +} from '../contractErrors'; + +describe('contractErrors', () => { + describe('isTokenTransferError', () => { + it('should detect token transfer errors from message', () => { + const error = { message: 'ERC20: transfer failed' }; + expect(isTokenTransferError(error)).toBe(true); + }); + + it('should detect token transfer errors from reason', () => { + const error = { reason: 'Token transfer reverted' }; + expect(isTokenTransferError(error)).toBe(true); + }); + + it('should return false for non-transfer errors', () => { + const error = { message: 'Some other error' }; + expect(isTokenTransferError(error)).toBe(false); + }); + }); + + describe('isInsufficientAllowanceError', () => { + it('should detect insufficient allowance errors', () => { + const error = { message: 'ERC20: insufficient allowance' }; + expect(isInsufficientAllowanceError(error)).toBe(true); + }); + + it('should detect allowance too low errors', () => { + const error = { message: 'Allowance too low' }; + expect(isInsufficientAllowanceError(error)).toBe(true); + }); + + it('should return false for other errors', () => { + const error = { message: 'Some other error' }; + expect(isInsufficientAllowanceError(error)).toBe(false); + }); + }); + + describe('isTokenApprovalError', () => { + it('should detect approval errors from message', () => { + const error = { message: 'Token approval failed' }; + expect(isTokenApprovalError(error)).toBe(true); + }); + + it('should detect approval errors from reason', () => { + const error = { reason: 'Approval reverted' }; + expect(isTokenApprovalError(error)).toBe(true); + }); + + it('should return false for other errors', () => { + const error = { message: 'Some other error' }; + expect(isTokenApprovalError(error)).toBe(false); + }); + }); + + describe('parseContractError', () => { + describe('Token transfer errors', () => { + it('should parse insufficient token balance error', () => { + const error = { message: 'ERC20: transfer amount exceeds balance' }; + const result = parseContractError(error); + + expect(result.code).toBe(ContractErrorType.INSUFFICIENT_TOKEN_BALANCE); + expect(result.message).toContain('Insufficient token balance'); + expect(result.message).toContain('cUSD'); + }); + + it('should parse insufficient allowance error', () => { + const error = { message: 'ERC20: insufficient allowance' }; + const result = parseContractError(error); + + expect(result.code).toBe(ContractErrorType.INSUFFICIENT_ALLOWANCE); + expect(result.message).toContain('Token approval required'); + }); + + it('should parse transfer to zero address error', () => { + const error = { message: 'ERC20: transfer to zero address' }; + const result = parseContractError(error); + + expect(result.code).toBe(ContractErrorType.TRANSFER_TO_ZERO_ADDRESS); + expect(result.message).toContain('invalid address'); + }); + + it('should parse zero amount transfer error', () => { + const error = { message: 'Transfer amount is zero' }; + const result = parseContractError(error); + + expect(result.code).toBe(ContractErrorType.ZERO_AMOUNT_TRANSFER); + expect(result.message).toContain('greater than zero'); + }); + + it('should parse generic token transfer failure', () => { + const error = { message: 'Token transfer reverted' }; + const result = parseContractError(error); + + expect(result.code).toBe(ContractErrorType.TOKEN_TRANSFER_FAILED); + expect(result.message).toContain('Token transfer failed'); + }); + }); + + describe('Token approval errors', () => { + it('should parse approval rejection error', () => { + const error = { + message: 'User rejected', + code: 4001 + }; + const result = parseContractError(error); + + expect(result.code).toBe(ContractErrorType.TOKEN_APPROVAL_REJECTED); + expect(result.message).toContain('cancelled'); + }); + + it('should parse approval failure error', () => { + const error = { message: 'Token approval failed' }; + const result = parseContractError(error); + + expect(result.code).toBe(ContractErrorType.TOKEN_APPROVAL_FAILED); + expect(result.message).toContain('Token approval failed'); + }); + }); + + describe('User rejection errors', () => { + it('should parse user rejection with code 4001', () => { + const error = { code: 4001 }; + const result = parseContractError(error); + + expect(result.code).toBe(ContractErrorType.TRANSACTION_REJECTED); + expect(result.message).toContain('rejected'); + }); + + it('should parse user rejection with ACTION_REJECTED', () => { + const error = { code: 'ACTION_REJECTED' }; + const result = parseContractError(error); + + expect(result.code).toBe(ContractErrorType.TRANSACTION_REJECTED); + }); + }); + + describe('Insufficient funds errors', () => { + it('should parse insufficient funds error', () => { + const error = { message: 'insufficient funds for transaction' }; + const result = parseContractError(error); + + expect(result.code).toBe(ContractErrorType.INSUFFICIENT_FUNDS); + expect(result.message).toContain('Insufficient funds'); + }); + }); + + describe('Network errors', () => { + it('should parse network errors', () => { + const error = { message: 'Network error occurred' }; + const result = parseContractError(error); + + expect(result.code).toBe(ContractErrorType.NETWORK_ERROR); + expect(result.message).toContain('Network error'); + }); + }); + + describe('Contract-specific errors', () => { + it('should parse not registered error', () => { + const error = { message: 'Player is not registered' }; + const result = parseContractError(error); + + expect(result.code).toBe(ContractErrorType.NOT_REGISTERED); + expect(result.message).toContain('register'); + }); + + it('should parse already registered error', () => { + const error = { message: 'Address is already registered' }; + const result = parseContractError(error); + + expect(result.code).toBe(ContractErrorType.ALREADY_REGISTERED); + expect(result.message).toContain('already registered'); + }); + + it('should parse invalid session error', () => { + const error = { message: 'Session does not exist' }; + const result = parseContractError(error); + + expect(result.code).toBe(ContractErrorType.INVALID_SESSION); + expect(result.message).toContain('Invalid game session'); + }); + }); + + describe('Unknown errors', () => { + it('should handle unknown errors gracefully', () => { + const error = { message: 'Some unknown error' }; + const result = parseContractError(error); + + expect(result.code).toBe(ContractErrorType.UNKNOWN_ERROR); + expect(result.message).toBe('Some unknown error'); + }); + + it('should handle errors without message', () => { + const error = {}; + const result = parseContractError(error); + + expect(result.code).toBe(ContractErrorType.UNKNOWN_ERROR); + expect(result.message).toContain('unknown error'); + }); + }); + }); + + describe('Error detection helpers', () => { + it('isUserRejectedError should detect user rejections', () => { + expect(isUserRejectedError({ code: 4001 })).toBe(true); + expect(isUserRejectedError({ code: 'ACTION_REJECTED' })).toBe(true); + expect(isUserRejectedError({ message: 'User rejected' })).toBe(true); + expect(isUserRejectedError({ message: 'Some error' })).toBe(false); + }); + + it('isInsufficientFundsError should detect insufficient funds', () => { + expect(isInsufficientFundsError({ message: 'insufficient funds' })).toBe(true); + expect(isInsufficientFundsError({ message: 'not enough funds' })).toBe(true); + expect(isInsufficientFundsError({ code: 'INSUFFICIENT_FUNDS' })).toBe(true); + expect(isInsufficientFundsError({ message: 'Some error' })).toBe(false); + }); + }); +}); + diff --git a/frontend/src/utils/contractErrors.ts b/frontend/src/utils/contractErrors.ts index f70b16e..b136b03 100644 --- a/frontend/src/utils/contractErrors.ts +++ b/frontend/src/utils/contractErrors.ts @@ -15,6 +15,16 @@ export enum ContractErrorType { INSUFFICIENT_FUNDS = 'INSUFFICIENT_FUNDS', GAS_ESTIMATION_FAILED = 'GAS_ESTIMATION_FAILED', + // Token transfer errors + INSUFFICIENT_TOKEN_BALANCE = 'INSUFFICIENT_TOKEN_BALANCE', + INSUFFICIENT_ALLOWANCE = 'INSUFFICIENT_ALLOWANCE', + TOKEN_TRANSFER_FAILED = 'TOKEN_TRANSFER_FAILED', + TOKEN_APPROVAL_FAILED = 'TOKEN_APPROVAL_FAILED', + TOKEN_APPROVAL_REJECTED = 'TOKEN_APPROVAL_REJECTED', + TOKEN_TRANSFER_REJECTED = 'TOKEN_TRANSFER_REJECTED', + ZERO_AMOUNT_TRANSFER = 'ZERO_AMOUNT_TRANSFER', + TRANSFER_TO_ZERO_ADDRESS = 'TRANSFER_TO_ZERO_ADDRESS', + // Contract-specific errors NOT_REGISTERED = 'NOT_REGISTERED', ALREADY_REGISTERED = 'ALREADY_REGISTERED', @@ -75,6 +85,60 @@ export function isInsufficientFundsError(error: any): boolean { ); } +/** + * Checks if an error is related to token transfer + */ +export function isTokenTransferError(error: any): boolean { + const message = error?.message?.toLowerCase() || ''; + const reason = error?.reason?.toLowerCase() || ''; + const data = error?.data?.message?.toLowerCase() || ''; + + return ( + message.includes('transfer') || + message.includes('erc20') || + message.includes('token') || + reason.includes('transfer') || + reason.includes('erc20') || + data.includes('transfer') || + error?.code === 'TRANSFER_FAILED' || + error?.code === 'TOKEN_TRANSFER_FAILED' + ); +} + +/** + * Checks if an error is related to insufficient token allowance + */ +export function isInsufficientAllowanceError(error: any): boolean { + const message = error?.message?.toLowerCase() || ''; + const reason = error?.reason?.toLowerCase() || ''; + + return ( + message.includes('allowance') || + message.includes('insufficient allowance') || + message.includes('allowance too low') || + reason.includes('allowance') || + error?.code === 'INSUFFICIENT_ALLOWANCE' || + error?.data?.message?.toLowerCase()?.includes('allowance') + ); +} + +/** + * Checks if an error is related to token approval + */ +export function isTokenApprovalError(error: any): boolean { + const message = error?.message?.toLowerCase() || ''; + const reason = error?.reason?.toLowerCase() || ''; + + return ( + message.includes('approve') || + message.includes('approval') || + reason.includes('approve') || + reason.includes('approval') || + error?.code === 'APPROVAL_FAILED' || + error?.code === 'TOKEN_APPROVAL_FAILED' + ); +} + /** * Checks if an error is a network error */ @@ -110,6 +174,71 @@ export function parseContractError(error: any): { message: string; code: Contrac }; } + // Handle token transfer errors (check before generic errors) + if (isTokenTransferError(error)) { + const message = error?.message?.toLowerCase() || ''; + const reason = error?.reason?.toLowerCase() || ''; + const errorData = message + ' ' + reason; + + // Insufficient token balance + if (errorData.includes('insufficient balance') || + errorData.includes('balance too low') || + errorData.includes('exceeds balance') || + errorData.includes('transfer amount exceeds balance')) { + return { + message: 'Insufficient token balance. Please ensure you have enough cUSD in your wallet.', + code: ContractErrorType.INSUFFICIENT_TOKEN_BALANCE, + }; + } + + // Insufficient allowance + if (isInsufficientAllowanceError(error)) { + return { + message: 'Token approval required. Please approve the transaction to allow the contract to spend your tokens.', + code: ContractErrorType.INSUFFICIENT_ALLOWANCE, + }; + } + + // Transfer to zero address + if (errorData.includes('transfer to zero address') || + errorData.includes('transfer to the zero address')) { + return { + message: 'Cannot transfer tokens to an invalid address.', + code: ContractErrorType.TRANSFER_TO_ZERO_ADDRESS, + }; + } + + // Zero amount transfer + if (errorData.includes('transfer amount is zero') || + errorData.includes('amount must be greater than zero')) { + return { + message: 'Transfer amount must be greater than zero.', + code: ContractErrorType.ZERO_AMOUNT_TRANSFER, + }; + } + + // Generic token transfer failure + return { + message: 'Token transfer failed. Please check your balance and try again.', + code: ContractErrorType.TOKEN_TRANSFER_FAILED, + }; + } + + // Handle token approval errors + if (isTokenApprovalError(error)) { + if (isUserRejectedError(error)) { + return { + message: 'Token approval was cancelled. Please approve the transaction to continue.', + code: ContractErrorType.TOKEN_APPROVAL_REJECTED, + }; + } + + return { + message: 'Token approval failed. Please try again or check your wallet connection.', + code: ContractErrorType.TOKEN_APPROVAL_FAILED, + }; + } + // Handle network errors if (isNetworkError(error)) { return {