mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-04-22 02:07:55 +00:00
385039e228
- shared/lib/error-handler.ts: Substrate error → user-friendly EN/KMR messages
* Maps 30+ blockchain error types (Staking, Identity, Tiki, ValidatorPool, DEX, Governance)
* extractDispatchError() - Parse Substrate DispatchError
* getUserFriendlyError() - Convert to bilingual messages
* handleBlockchainError() - Toast helper with auto language detection
* SUCCESS_MESSAGES - Success templates with {{param}} interpolation
- web/src/components/ErrorBoundary.tsx: Global React error boundary
* Catches unhandled React errors with fallback UI
* Error details with stack trace (developer mode)
* Try Again / Reload Page / Go Home buttons
* RouteErrorBoundary - Smaller boundary for individual routes
* Support email link (info@pezkuwichain.io)
- shared/components/AsyncComponent.tsx: Async data loading patterns
* CardSkeleton / ListItemSkeleton / TableSkeleton - Animated loading states
* LoadingState - Kurdistan green spinner with custom message
* ErrorState - Red alert with retry button
* EmptyState - Empty inbox icon with optional action
* AsyncComponent<T> - Generic wrapper handling Loading/Error/Empty/Success states
- web/src/App.tsx: Wrapped with ErrorBoundary
* All React errors now caught gracefully
* Beautiful fallback UI instead of white screen of death
Production-ready error handling with bilingual support (EN/KMR).
374 lines
12 KiB
TypeScript
374 lines
12 KiB
TypeScript
// ========================================
|
|
// Error Handler & User-Friendly Messages
|
|
// ========================================
|
|
// Convert blockchain errors to human-readable messages
|
|
|
|
import type { ApiPromise } from '@polkadot/api';
|
|
import type { DispatchError } from '@polkadot/types/interfaces';
|
|
|
|
// ========================================
|
|
// ERROR MESSAGE MAPPINGS
|
|
// ========================================
|
|
|
|
interface ErrorMessage {
|
|
en: string;
|
|
kmr: string; // Kurmanji
|
|
}
|
|
|
|
/**
|
|
* User-friendly error messages for common blockchain errors
|
|
* Key format: "palletName.errorName"
|
|
*/
|
|
const ERROR_MESSAGES: Record<string, ErrorMessage> = {
|
|
// Staking errors
|
|
'staking.InsufficientBond': {
|
|
en: 'Bond amount too small. Please check minimum staking requirement.',
|
|
kmr: 'Mîqdara bond zêde piçûk e. Ji kerema xwe mîqdara kêmtirîn kontrol bike.',
|
|
},
|
|
'staking.AlreadyBonded': {
|
|
en: 'You have already bonded tokens. Use "Bond More" to add additional stake.',
|
|
kmr: 'We berê token bond kirine. Ji bo zêdekirin "Bond More" bikar bîne.',
|
|
},
|
|
'staking.NotStash': {
|
|
en: 'This account is not a stash account. Please use your staking controller.',
|
|
kmr: 'Ev account stash nîne. Ji kerema xwe controller bikar bîne.',
|
|
},
|
|
'staking.NoMoreChunks': {
|
|
en: 'Too many unbonding chunks. Please wait for previous unbondings to complete.',
|
|
kmr: 'Zêde chunk unbonding hene. Ji kerema xwe li çavkaniyên berê bisekine.',
|
|
},
|
|
|
|
// Identity KYC errors
|
|
'identityKyc.AlreadyApplied': {
|
|
en: 'You already have a pending citizenship application. Please wait for approval.',
|
|
kmr: 'We berê serlêdana welatîtiyê heye. Ji kerema xwe li pejirandina bisekine.',
|
|
},
|
|
'identityKyc.AlreadyApproved': {
|
|
en: 'Your citizenship application is already approved!',
|
|
kmr: 'Serlêdana welatîtiya we berê hatiye pejirandin!',
|
|
},
|
|
'identityKyc.NotApproved': {
|
|
en: 'Your KYC is not approved yet. Please complete citizenship application first.',
|
|
kmr: 'KYC-ya we hîn nehatiye pejirandin. Pêşî serlêdana welatîtiyê temam bike.',
|
|
},
|
|
'identityKyc.IdentityNotSet': {
|
|
en: 'Please set your identity information first.',
|
|
kmr: 'Ji kerema xwe pêşî agahdariya nasnameya xwe saz bike.',
|
|
},
|
|
|
|
// Tiki errors
|
|
'tiki.RoleAlreadyAssigned': {
|
|
en: 'This role is already assigned to the user.',
|
|
kmr: 'Ev rol berê ji bikarhêner re hatiye veqetandin.',
|
|
},
|
|
'tiki.UnauthorizedRoleAssignment': {
|
|
en: 'You do not have permission to assign this role.',
|
|
kmr: 'We destûra veqetandina vê rolê nîne.',
|
|
},
|
|
'tiki.RoleNotFound': {
|
|
en: 'The specified role does not exist.',
|
|
kmr: 'Rola diyarkirî tune ye.',
|
|
},
|
|
|
|
// ValidatorPool errors
|
|
'validatorPool.AlreadyInPool': {
|
|
en: 'You are already registered in the validator pool.',
|
|
kmr: 'We berê di pool-a validator de tomar bûyî.',
|
|
},
|
|
'validatorPool.NotInPool': {
|
|
en: 'You are not registered in the validator pool.',
|
|
kmr: 'We di pool-a validator de tomar nebûyî.',
|
|
},
|
|
'validatorPool.InsufficientStake': {
|
|
en: 'Insufficient stake for validator pool. Please increase your stake.',
|
|
kmr: 'Stake ji bo pool-a validator kêm e. Ji kerema xwe stake-ya xwe zêde bike.',
|
|
},
|
|
|
|
// DEX/AssetConversion errors
|
|
'assetConversion.PoolNotFound': {
|
|
en: 'Liquidity pool not found for this token pair.',
|
|
kmr: 'Pool-a liquidity ji bo vê cuda-token nehat dîtin.',
|
|
},
|
|
'assetConversion.InsufficientLiquidity': {
|
|
en: 'Insufficient liquidity in pool. Try a smaller amount.',
|
|
kmr: 'Liquidity-ya pool-ê kêm e. Mîqdareke piçûktir biceribîne.',
|
|
},
|
|
'assetConversion.SlippageTooHigh': {
|
|
en: 'Price impact too high. Increase slippage tolerance or reduce amount.',
|
|
kmr: 'Bandora bihayê zêde mezin e. Toleransa slippage zêde bike an mîqdarê kêm bike.',
|
|
},
|
|
'assetConversion.AmountTooSmall': {
|
|
en: 'Swap amount too small. Minimum swap amount not met.',
|
|
kmr: 'Mîqdara swap zêde piçûk e. Mîqdara kêmtirîn nehatiye gihîştin.',
|
|
},
|
|
|
|
// Balance/Asset errors
|
|
'balances.InsufficientBalance': {
|
|
en: 'Insufficient balance. You do not have enough tokens for this transaction.',
|
|
kmr: 'Balance-ya we kêm e. Ji bo vê transaction token-ên we têr nînin.',
|
|
},
|
|
'balances.ExistentialDeposit': {
|
|
en: 'Amount is below existential deposit. Account would be reaped.',
|
|
kmr: 'Mîqdar ji existential deposit kêmtir e. Account dê were jêbirin.',
|
|
},
|
|
'assets.BalanceLow': {
|
|
en: 'Asset balance too low for this operation.',
|
|
kmr: 'Balance-ya asset-ê ji bo vê operation zêde kêm e.',
|
|
},
|
|
'assets.NoPermission': {
|
|
en: 'You do not have permission to perform this operation on this asset.',
|
|
kmr: 'We destûra vê operation-ê li ser vê asset-ê nîne.',
|
|
},
|
|
|
|
// Governance errors
|
|
'referenda.NotOngoing': {
|
|
en: 'This referendum is not currently active.',
|
|
kmr: 'Ev referendum niha ne çalak e.',
|
|
},
|
|
'referenda.AlreadyVoted': {
|
|
en: 'You have already voted on this referendum.',
|
|
kmr: 'We berê li ser vê referendum-ê deng da.',
|
|
},
|
|
'convictionVoting.NotVoter': {
|
|
en: 'You are not eligible to vote. Citizenship required.',
|
|
kmr: 'We mafê dengdanê nîne. Welatîtî pêwîst e.',
|
|
},
|
|
|
|
// Treasury errors
|
|
'treasury.InsufficientProposersBalance': {
|
|
en: 'Insufficient balance to submit treasury proposal. Bond required.',
|
|
kmr: 'Ji bo pêşniyara treasury-yê balance kêm e. Bond pêwîst e.',
|
|
},
|
|
|
|
// System/General errors
|
|
'system.CallFiltered': {
|
|
en: 'This action is not permitted by the system filters.',
|
|
kmr: 'Ev çalakî ji hêla fîltireyên sîstemê ve nayê destûrdan.',
|
|
},
|
|
'BadOrigin': {
|
|
en: 'Unauthorized: You do not have permission for this action.',
|
|
kmr: 'Destûrnîn: We destûra vê çalakiyê nîne.',
|
|
},
|
|
'Module': {
|
|
en: 'A blockchain module error occurred. Please try again.',
|
|
kmr: 'Xeletiya module-ya blockchain-ê qewimî. Ji kerema xwe dîsa biceribîne.',
|
|
},
|
|
};
|
|
|
|
// ========================================
|
|
// ERROR EXTRACTION & FORMATTING
|
|
// ========================================
|
|
|
|
/**
|
|
* Extract error information from DispatchError
|
|
*/
|
|
export function extractDispatchError(
|
|
api: ApiPromise,
|
|
dispatchError: DispatchError
|
|
): {
|
|
section: string;
|
|
name: string;
|
|
docs: string;
|
|
raw: string;
|
|
} {
|
|
if (dispatchError.isModule) {
|
|
const decoded = api.registry.findMetaError(dispatchError.asModule);
|
|
return {
|
|
section: decoded.section,
|
|
name: decoded.name,
|
|
docs: decoded.docs.join(' ').trim(),
|
|
raw: `${decoded.section}.${decoded.name}`,
|
|
};
|
|
} else {
|
|
return {
|
|
section: 'Unknown',
|
|
name: dispatchError.type,
|
|
docs: dispatchError.toString(),
|
|
raw: dispatchError.toString(),
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get user-friendly error message
|
|
* Falls back to blockchain docs if no custom message exists
|
|
*/
|
|
export function getUserFriendlyError(
|
|
api: ApiPromise,
|
|
dispatchError: DispatchError,
|
|
language: 'en' | 'kmr' = 'en'
|
|
): string {
|
|
const errorInfo = extractDispatchError(api, dispatchError);
|
|
const errorKey = errorInfo.raw;
|
|
|
|
// Check if we have a custom message
|
|
const customMessage = ERROR_MESSAGES[errorKey];
|
|
if (customMessage) {
|
|
return customMessage[language];
|
|
}
|
|
|
|
// Fallback to blockchain documentation
|
|
if (errorInfo.docs && errorInfo.docs.length > 0) {
|
|
return errorInfo.docs;
|
|
}
|
|
|
|
// Final fallback
|
|
return `Transaction failed: ${errorInfo.section}.${errorInfo.name}`;
|
|
}
|
|
|
|
// ========================================
|
|
// TOAST HELPER
|
|
// ========================================
|
|
|
|
export interface ToastFunction {
|
|
(options: {
|
|
title: string;
|
|
description: string;
|
|
variant?: 'default' | 'destructive';
|
|
}): void;
|
|
}
|
|
|
|
/**
|
|
* Handle blockchain error with toast notification
|
|
* Automatically extracts user-friendly message
|
|
*/
|
|
export function handleBlockchainError(
|
|
error: any,
|
|
api: ApiPromise | null,
|
|
toast: ToastFunction,
|
|
language: 'en' | 'kmr' = 'en'
|
|
): void {
|
|
console.error('Blockchain error:', error);
|
|
|
|
// If it's a dispatch error from transaction callback
|
|
if (error?.isModule !== undefined && api) {
|
|
const userMessage = getUserFriendlyError(api, error, language);
|
|
toast({
|
|
title: language === 'en' ? 'Transaction Failed' : 'Transaction Têk Çû',
|
|
description: userMessage,
|
|
variant: 'destructive',
|
|
});
|
|
return;
|
|
}
|
|
|
|
// If it's a standard error object
|
|
if (error?.message) {
|
|
toast({
|
|
title: language === 'en' ? 'Error' : 'Xeletî',
|
|
description: error.message,
|
|
variant: 'destructive',
|
|
});
|
|
return;
|
|
}
|
|
|
|
// If it's a string
|
|
if (typeof error === 'string') {
|
|
toast({
|
|
title: language === 'en' ? 'Error' : 'Xeletî',
|
|
description: error,
|
|
variant: 'destructive',
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Generic fallback
|
|
toast({
|
|
title: language === 'en' ? 'Error' : 'Xeletî',
|
|
description:
|
|
language === 'en'
|
|
? 'An unexpected error occurred. Please try again.'
|
|
: 'Xeletîyek nediyar qewimî. Ji kerema xwe dîsa biceribîne.',
|
|
variant: 'destructive',
|
|
});
|
|
}
|
|
|
|
// ========================================
|
|
// SUCCESS MESSAGES
|
|
// ========================================
|
|
|
|
export interface SuccessMessage {
|
|
en: string;
|
|
kmr: string;
|
|
}
|
|
|
|
export const SUCCESS_MESSAGES: Record<string, SuccessMessage> = {
|
|
// Staking
|
|
'staking.bonded': {
|
|
en: 'Successfully staked {{amount}} HEZ. Rewards will start in the next era.',
|
|
kmr: '{{amount}} HEZ bi serkeftî stake kirin. Xelat di era pêşîn de dest pê dike.',
|
|
},
|
|
'staking.unbonded': {
|
|
en: 'Unbonded {{amount}} HEZ. Withdrawal available in {{days}} days.',
|
|
kmr: '{{amount}} HEZ unbond kirin. Di {{days}} rojan de derbasdarî dibe.',
|
|
},
|
|
'staking.nominated': {
|
|
en: 'Successfully nominated {{count}} validators.',
|
|
kmr: 'Bi serkeftî {{count}} validator nomînekirin.',
|
|
},
|
|
'staking.scoreStarted': {
|
|
en: 'Staking score tracking started! Your score will accumulate over time.',
|
|
kmr: 'Şopa staking dest pê kir! Xala we dê bi demê re kom bibe.',
|
|
},
|
|
|
|
// Citizenship
|
|
'citizenship.applied': {
|
|
en: 'Citizenship application submitted successfully! We will review your application.',
|
|
kmr: 'Serlêdana welatîtiyê bi serkeftî hate şandin! Em ê serlêdana we binirxînin.',
|
|
},
|
|
|
|
// Governance
|
|
'governance.voted': {
|
|
en: 'Your vote has been recorded successfully!',
|
|
kmr: 'Deng-a we bi serkeftî hate tomarkirin!',
|
|
},
|
|
'governance.proposed': {
|
|
en: 'Proposal submitted successfully! Voting will begin soon.',
|
|
kmr: 'Pêşniyar bi serkeftî hate şandin! Dengdan hêdî dest pê dike.',
|
|
},
|
|
|
|
// DEX
|
|
'dex.swapped': {
|
|
en: 'Successfully swapped {{from}} {{fromToken}} for {{to}} {{toToken}}',
|
|
kmr: 'Bi serkeftî {{from}} {{fromToken}} bo {{to}} {{toToken}} guhertin',
|
|
},
|
|
'dex.liquidityAdded': {
|
|
en: 'Successfully added liquidity to the pool!',
|
|
kmr: 'Bi serkeftî liquidity li pool-ê zêde kir!',
|
|
},
|
|
'dex.liquidityRemoved': {
|
|
en: 'Successfully removed liquidity from the pool!',
|
|
kmr: 'Bi serkeftî liquidity ji pool-ê derxist!',
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Handle successful blockchain transaction
|
|
*/
|
|
export function handleBlockchainSuccess(
|
|
messageKey: string,
|
|
toast: ToastFunction,
|
|
params: Record<string, string | number> = {},
|
|
language: 'en' | 'kmr' = 'en'
|
|
): void {
|
|
const template = SUCCESS_MESSAGES[messageKey];
|
|
|
|
if (!template) {
|
|
toast({
|
|
title: language === 'en' ? 'Success' : 'Serkeft',
|
|
description: language === 'en' ? 'Transaction successful!' : 'Transaction serkeftî!',
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Replace template variables like {{amount}}
|
|
let message = template[language];
|
|
Object.entries(params).forEach(([key, value]) => {
|
|
message = message.replace(new RegExp(`{{${key}}}`, 'g'), String(value));
|
|
});
|
|
|
|
toast({
|
|
title: language === 'en' ? 'Success' : 'Serkeft',
|
|
description: message,
|
|
});
|
|
}
|