Merge branch 'claude/calisma-ya-011CV6DKKRcWvDTxoEY7rYV4' into main

This commit is contained in:
2025-11-17 03:15:30 +03:00
23 changed files with 3923 additions and 1333 deletions
+282
View File
@@ -0,0 +1,282 @@
// ========================================
// Async Component Pattern
// ========================================
// Standard pattern for loading/error/empty states
import React, { ReactNode } from 'react';
import { Card, CardContent } from '@/components/ui/card';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Button } from '@/components/ui/button';
import { Loader2, AlertCircle, Inbox, RefreshCw } from 'lucide-react';
// ========================================
// LOADING SKELETON
// ========================================
export const CardSkeleton: React.FC = () => {
return (
<Card className="bg-gray-900 border-gray-800 animate-pulse">
<CardContent className="p-6">
<div className="h-6 bg-gray-800 rounded w-1/3 mb-4"></div>
<div className="space-y-3">
<div className="h-4 bg-gray-800 rounded"></div>
<div className="h-4 bg-gray-800 rounded w-5/6"></div>
<div className="h-4 bg-gray-800 rounded w-4/6"></div>
</div>
</CardContent>
</Card>
);
};
export const ListItemSkeleton: React.FC = () => {
return (
<div className="flex items-center gap-4 p-4 bg-gray-900 border border-gray-800 rounded-lg animate-pulse">
<div className="w-12 h-12 bg-gray-800 rounded-full"></div>
<div className="flex-1 space-y-2">
<div className="h-4 bg-gray-800 rounded w-1/4"></div>
<div className="h-3 bg-gray-800 rounded w-1/2"></div>
</div>
<div className="h-8 w-20 bg-gray-800 rounded"></div>
</div>
);
};
export const TableSkeleton: React.FC<{ rows?: number }> = ({ rows = 5 }) => {
return (
<div className="space-y-2">
{Array.from({ length: rows }).map((_, i) => (
<div key={i} className="flex gap-4 p-3 bg-gray-900 border border-gray-800 rounded animate-pulse">
<div className="h-4 bg-gray-800 rounded flex-1"></div>
<div className="h-4 bg-gray-800 rounded w-24"></div>
<div className="h-4 bg-gray-800 rounded w-32"></div>
</div>
))}
</div>
);
};
// ========================================
// LOADING COMPONENT
// ========================================
export const LoadingState: React.FC<{
message?: string;
fullScreen?: boolean;
}> = ({ message = 'Loading...', fullScreen = false }) => {
const content = (
<div className="flex flex-col items-center gap-4">
<Loader2 className="w-12 h-12 text-green-500 animate-spin" />
<p className="text-gray-400">{message}</p>
</div>
);
if (fullScreen) {
return (
<div className="min-h-screen bg-gray-950 flex items-center justify-center">
<Card className="bg-gray-900 border-gray-800 p-8">
<CardContent>{content}</CardContent>
</Card>
</div>
);
}
return (
<div className="flex items-center justify-center p-12">
{content}
</div>
);
};
// ========================================
// ERROR STATE
// ========================================
export const ErrorState: React.FC<{
message?: string;
error?: Error | string;
onRetry?: () => void;
fullScreen?: boolean;
}> = ({
message = 'An error occurred',
error,
onRetry,
fullScreen = false,
}) => {
const errorMessage = typeof error === 'string' ? error : error?.message;
const content = (
<Alert className="bg-red-900/20 border-red-500">
<AlertCircle className="h-5 w-5 text-red-400" />
<AlertDescription className="text-gray-300">
<strong className="block mb-2">{message}</strong>
{errorMessage && (
<p className="text-sm text-gray-400 mb-4">{errorMessage}</p>
)}
{onRetry && (
<Button
onClick={onRetry}
size="sm"
className="bg-green-600 hover:bg-green-700"
>
<RefreshCw className="w-4 h-4 mr-2" />
Try Again
</Button>
)}
</AlertDescription>
</Alert>
);
if (fullScreen) {
return (
<div className="min-h-screen bg-gray-950 flex items-center justify-center p-4">
<Card className="bg-gray-900 border-gray-800 max-w-md">
<CardContent className="p-8">{content}</CardContent>
</Card>
</div>
);
}
return <div className="p-4">{content}</div>;
};
// ========================================
// EMPTY STATE
// ========================================
export const EmptyState: React.FC<{
message?: string;
description?: string;
icon?: ReactNode;
action?: {
label: string;
onClick: () => void;
};
fullScreen?: boolean;
}> = ({
message = 'No data found',
description,
icon,
action,
fullScreen = false,
}) => {
const content = (
<div className="flex flex-col items-center gap-4 text-center">
{icon || <Inbox className="w-16 h-16 text-gray-600" />}
<div>
<h3 className="text-lg font-semibold text-white mb-1">{message}</h3>
{description && <p className="text-sm text-gray-400">{description}</p>}
</div>
{action && (
<Button onClick={action.onClick} className="bg-green-600 hover:bg-green-700">
{action.label}
</Button>
)}
</div>
);
if (fullScreen) {
return (
<div className="min-h-screen bg-gray-950 flex items-center justify-center p-4">
<Card className="bg-gray-900 border-gray-800 p-8">
<CardContent>{content}</CardContent>
</Card>
</div>
);
}
return <div className="p-12">{content}</div>;
};
// ========================================
// ASYNC COMPONENT WRAPPER
// ========================================
export interface AsyncComponentProps<T> {
/** Loading state */
isLoading: boolean;
/** Error object */
error?: Error | string | null;
/** Data */
data?: T | null;
/** Children render function */
children: (data: T) => ReactNode;
/** Custom loading component */
LoadingComponent?: React.ComponentType;
/** Custom error component */
ErrorComponent?: React.ComponentType<{ error: Error | string; onRetry?: () => void }>;
/** Custom empty component */
EmptyComponent?: React.ComponentType;
/** Retry callback */
onRetry?: () => void;
/** Loading message */
loadingMessage?: string;
/** Error message */
errorMessage?: string;
/** Empty message */
emptyMessage?: string;
/** Full screen mode */
fullScreen?: boolean;
}
/**
* Standard async component pattern
* Handles loading, error, empty, and success states
*
* @example
* <AsyncComponent
* isLoading={loading}
* error={error}
* data={courses}
* onRetry={refetch}
* >
* {(courses) => <CourseList courses={courses} />}
* </AsyncComponent>
*/
export function AsyncComponent<T>({
isLoading,
error,
data,
children,
LoadingComponent,
ErrorComponent,
EmptyComponent,
onRetry,
loadingMessage = 'Loading...',
errorMessage = 'Failed to load data',
emptyMessage = 'No data available',
fullScreen = false,
}: AsyncComponentProps<T>): JSX.Element {
// Loading state
if (isLoading) {
if (LoadingComponent) {
return <LoadingComponent />;
}
return <LoadingState message={loadingMessage} fullScreen={fullScreen} />;
}
// Error state
if (error) {
if (ErrorComponent) {
return <ErrorComponent error={error} onRetry={onRetry} />;
}
return (
<ErrorState
message={errorMessage}
error={error}
onRetry={onRetry}
fullScreen={fullScreen}
/>
);
}
// Empty state
if (!data || (Array.isArray(data) && data.length === 0)) {
if (EmptyComponent) {
return <EmptyComponent />;
}
return <EmptyState message={emptyMessage} fullScreen={fullScreen} />;
}
// Success state - render children with data
return <>{children(data)}</>;
}
+537
View File
@@ -0,0 +1,537 @@
// ========================================
// 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.',
},
// Welati (Elections & Governance) errors
'welati.ElectionNotFound': {
en: 'Election not found. Please check the election ID.',
kmr: 'Hilbijartin nehat dîtin. Ji kerema xwe ID-ya hilbijartinê kontrol bike.',
},
'welati.ElectionNotActive': {
en: 'This election is not currently active.',
kmr: 'Ev hilbijartin niha ne çalak e.',
},
'welati.CandidacyPeriodExpired': {
en: 'Candidate registration period has ended.',
kmr: 'Dema qeydkirina berendaman qediya.',
},
'welati.VotingPeriodNotStarted': {
en: 'Voting period has not started yet. Please wait.',
kmr: 'Dema dengdanê hîn dest pê nekiriye. Ji kerema xwe bisekine.',
},
'welati.VotingPeriodExpired': {
en: 'Voting period has ended.',
kmr: 'Dema dengdanê qediya.',
},
'welati.AlreadyCandidate': {
en: 'You are already registered as a candidate in this election.',
kmr: 'We berê wekî berendam di vê hilbijartinê de tomar bûyî.',
},
'welati.AlreadyVoted': {
en: 'You have already voted in this election.',
kmr: 'We berê di vê hilbijartinê de deng da.',
},
'welati.InsufficientEndorsements': {
en: 'Insufficient endorsements. You need more citizen supporters.',
kmr: 'Piştgiriya têr tune. We piştgiriya zêdetir ji welatiyên pêwîst e.',
},
'welati.InsufficientTrustScore': {
en: 'Your trust score is too low for this election. Build your reputation first.',
kmr: 'Skora emîniya we ji bo vê hilbijartinê zêde kêm e. Pêşî navê xwe baş bike.',
},
'welati.NotACitizen': {
en: 'You must be a verified citizen (KYC approved) to participate.',
kmr: 'Divê we welatiyeke pejirandî (KYC pejirandî) bin da beşdar bibin.',
},
'welati.DepositRequired': {
en: 'Candidacy deposit required. Please pay the registration fee.',
kmr: 'Depozîta berendamiyê pêwîst e. Ji kerema xwe lêçûna qeydkirinê bidin.',
},
'welati.NotAuthorizedToNominate': {
en: 'You are not authorized to nominate officials. Minister or President only.',
kmr: 'We destûra hilbijartina karbidestan nîne. Tenê Wezîr an Serok.',
},
'welati.NotAuthorizedToApprove': {
en: 'Only the President can approve appointments.',
kmr: 'Tenê Serok dikare bicîhbûnan bipejirîne.',
},
'welati.NotAuthorizedToPropose': {
en: 'You are not authorized to submit proposals. Parliament members only.',
kmr: 'We destûra pêşniyaran pêşkêş kirinê nîne. Tenê endamên parlamentoyê.',
},
'welati.NotAuthorizedToVote': {
en: 'You are not authorized to vote on this proposal.',
kmr: 'We destûra dengdanê li ser vê pêşniyarê nîne.',
},
'welati.ProposalNotFound': {
en: 'Proposal not found. Please check the proposal ID.',
kmr: 'Pêşniyar nehat dîtin. Ji kerema xwe ID-ya pêşniyarê kontrol bike.',
},
'welati.ProposalNotActive': {
en: 'This proposal is not currently active or voting has ended.',
kmr: 'Ev pêşniyar niha ne çalak e an dengdan qediya.',
},
'welati.ProposalAlreadyVoted': {
en: 'You have already voted on this proposal.',
kmr: 'We berê li ser vê pêşniyarê deng da.',
},
'welati.QuorumNotMet': {
en: 'Quorum not met. Insufficient participation for this decision.',
kmr: 'Quorum nehat bidest xistin. Beşdariya têr ji bo vê biryarê tune ye.',
},
'welati.InvalidDistrict': {
en: 'Invalid electoral district. Please select a valid district.',
kmr: 'Qeza hilbijartinê nederbasdar e. Ji kerema xwe qezayeke derbasdar hilbijêre.',
},
'welati.RoleAlreadyFilled': {
en: 'This government position is already filled.',
kmr: 'Ev pozîsyona hukûmetê berê hatiye dagirtin.',
},
// Perwerde (Education) errors
'perwerde.CourseNotFound': {
en: 'Course not found. Please check the course ID.',
kmr: 'Ders nehat dîtin. Ji kerema xwe ID-ya dersê kontrol bike.',
},
'perwerde.AlreadyEnrolled': {
en: 'You are already enrolled in this course.',
kmr: 'We berê di vî dersê de tomar bûyî.',
},
'perwerde.NotEnrolled': {
en: 'You must enroll in this course first before completing it.',
kmr: 'Pêşî divê we di vî dersê de tomar bibin da ku temam bikin.',
},
'perwerde.CourseNotActive': {
en: 'This course is archived and no longer accepting enrollments.',
kmr: 'Ev ders di arşîvê de ye û êdî tomaran qebûl nake.',
},
'perwerde.CourseAlreadyCompleted': {
en: 'You have already completed this course.',
kmr: 'We berê ev ders temam kiriye.',
},
'perwerde.NotCourseOwner': {
en: 'Only the course owner can perform this action.',
kmr: 'Tenê xwediyê dersê dikare vê çalakiyê bike.',
},
'perwerde.TooManyCourses': {
en: 'Course enrollment limit reached. Please complete some courses first.',
kmr: 'Sînorê tomarkirina dersê gihîşt. Ji kerema xwe pêşî hin dersan temam bikin.',
},
// 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!',
},
// Welati (Elections & Governance)
'welati.candidateRegistered': {
en: 'Successfully registered as candidate! Deposit: {{deposit}} HEZ. Good luck!',
kmr: 'Bi serkeftî wekî berendam tomar bûn! Depozît: {{deposit}} HEZ. Serkeftinê!',
},
'welati.voteCast': {
en: 'Your vote has been cast successfully! Thank you for participating.',
kmr: 'Deng-a we bi serkeftî hate dayîn! Spas ji bo beşdarî bûnê.',
},
'welati.proposalSubmitted': {
en: 'Proposal submitted successfully! Voting period: {{days}} days.',
kmr: 'Pêşniyar bi serkeftî hate şandin! Dema dengdanê: {{days}} roj.',
},
'welati.proposalVoted': {
en: 'Vote recorded on proposal #{{id}}. Your voice matters!',
kmr: 'Deng li ser pêşniyara #{{id}} tomar bû. Deng-a we girîng e!',
},
'welati.officialNominated': {
en: 'Official nominated successfully! Awaiting presidential approval.',
kmr: 'Karbides bi serkeftî hate hilbijartin! Li pejirandina serokê bisekine.',
},
'welati.appointmentApproved': {
en: 'Appointment approved! {{nominee}} is now {{role}}.',
kmr: 'Bicîhbûn pejirandî! {{nominee}} niha {{role}} ye.',
},
'welati.electionFinalized': {
en: 'Election finalized! {{winners}} elected. Turnout: {{turnout}}%',
kmr: 'Hilbijartin temam bû! {{winners}} hate hilbijartin. Beşdarî: {{turnout}}%',
},
// Perwerde (Education)
'perwerde.courseCreated': {
en: 'Course "{{name}}" created successfully! Course ID: #{{id}}',
kmr: 'Dersa "{{name}}" bi serkeftî hate afirandin! ID-ya Dersê: #{{id}}',
},
'perwerde.enrolled': {
en: 'Successfully enrolled in course! Start learning now.',
kmr: 'Bi serkeftî di dersê de tomar bûn! Niha dest bi hînbûnê bike.',
},
'perwerde.completed': {
en: 'Congratulations! Course completed. Points earned: {{points}}',
kmr: 'Pîroz be! Ders temam bû. Xalên bidestxistî: {{points}}',
},
'perwerde.archived': {
en: 'Course archived successfully. No new enrollments will be accepted.',
kmr: 'Ders bi serkeftî hate arşîvkirin. Tomarên nû nayên qebûlkirin.',
},
};
/**
* 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,
});
}
+382
View File
@@ -0,0 +1,382 @@
// ========================================
// Route Guards & Permission Checking
// ========================================
// Functions to check user permissions for protected routes
import type { ApiPromise } from '@polkadot/api';
// ========================================
// CITIZENSHIP STATUS CHECK
// ========================================
/**
* Check if user has approved citizenship (KYC approved)
* Required for: Voting, Education, Validator Pool, etc.
*/
export async function checkCitizenStatus(
api: ApiPromise | null,
address: string | null | undefined
): Promise<boolean> {
if (!api || !address) {
return false;
}
try {
// Check if Identity KYC pallet exists
if (!api.query?.identityKyc?.kycStatuses) {
console.warn('Identity KYC pallet not available');
return false;
}
const kycStatus = await api.query.identityKyc.kycStatuses(address);
if (kycStatus.isEmpty) {
return false;
}
const statusStr = kycStatus.toString();
return statusStr === 'Approved';
} catch (error) {
console.error('Error checking citizen status:', error);
return false;
}
}
// ========================================
// VALIDATOR POOL STATUS CHECK
// ========================================
/**
* Check if user is registered in validator pool
* Required for: Validator Pool dashboard, validator settings
*/
export async function checkValidatorStatus(
api: ApiPromise | null,
address: string | null | undefined
): Promise<boolean> {
if (!api || !address) {
return false;
}
try {
// Check if ValidatorPool pallet exists
if (!api.query?.validatorPool?.poolMembers) {
console.warn('ValidatorPool pallet not available');
return false;
}
const poolMember = await api.query.validatorPool.poolMembers(address);
return !poolMember.isEmpty;
} catch (error) {
console.error('Error checking validator status:', error);
return false;
}
}
// ========================================
// TIKI ROLE CHECK
// ========================================
// Tiki role enum mapping (from pallet-tiki)
const TIKI_ROLES = [
'Hemwelatî', // 0 - Citizen
'Parlementer', // 1 - Parliament Member
'SerokiMeclise', // 2 - Speaker of Parliament
'Serok', // 3 - President
'Wezir', // 4 - Minister
'EndameDiwane', // 5 - Dîwan Member (Constitutional Court)
'Dadger', // 6 - Judge
'Dozger', // 7 - Prosecutor
'Hiquqnas', // 8 - Lawyer
'Noter', // 9 - Notary
'Xezinedar', // 10 - Treasurer
'Bacgir', // 11 - Tax Collector
'GerinendeyeCavkaniye',// 12 - Resource Manager
'OperatorêTorê', // 13 - Network Operator
'PisporêEwlehiyaSîber',// 14 - Cyber Security Expert
'GerinendeyeDaneye', // 15 - Data Manager
'Berdevk', // 16 - Spokesperson
'Qeydkar', // 17 - Registrar
'Balyoz', // 18 - Ambassador
'Navbeynkar', // 19 - Mediator
'ParêzvaneÇandî', // 20 - Cultural Protector
'Mufetîs', // 21 - Inspector
'KalîteKontrolker', // 22 - Quality Controller
'Mela', // 23 - Mullah
'Feqî', // 24 - Religious Scholar
'Perwerdekar', // 25 - Educator
'Rewsenbîr', // 26 - Intellectual
'RêveberêProjeyê', // 27 - Project Manager
'SerokêKomele', // 28 - Community Leader
'ModeratorêCivakê', // 29 - Society Moderator
'Axa', // 30 - Lord/Landowner
'Pêseng', // 31 - Pioneer
'Sêwirmend', // 32 - Counselor
'Hekem', // 33 - Wise Person
'Mamoste', // 34 - Teacher
'Bazargan', // 35 - Merchant
'SerokWeziran', // 36 - Prime Minister
'WezireDarayiye', // 37 - Finance Minister
'WezireParez', // 38 - Defense Minister
'WezireDad', // 39 - Justice Minister
'WezireBelaw', // 40 - Publication Minister
'WezireTend', // 41 - Health Minister
'WezireAva', // 42 - Infrastructure Minister
'WezireCand', // 43 - Education Minister
];
/**
* Check if user has specific Tiki role
* @param role - Kurdish name of role (e.g., 'Hemwelatî', 'Perwerdekar')
*/
export async function checkTikiRole(
api: ApiPromise | null,
address: string | null | undefined,
role: string
): Promise<boolean> {
if (!api || !address) {
return false;
}
try {
// Check if Tiki pallet exists
if (!api.query?.tiki?.userTikis) {
console.warn('Tiki pallet not available');
return false;
}
const tikis = await api.query.tiki.userTikis(address);
if (tikis.isEmpty) {
return false;
}
// userTikis returns BoundedVec of Tiki enum indices
const tikiIndices = tikis.toJSON() as number[];
// Find role index
const roleIndex = TIKI_ROLES.indexOf(role);
if (roleIndex === -1) {
console.warn(`Unknown Tiki role: ${role}`);
return false;
}
// Check if user has this role
return tikiIndices.includes(roleIndex);
} catch (error) {
console.error('Error checking Tiki role:', error);
return false;
}
}
/**
* Check if user has ANY Tiki role from a list
* Useful for checking multiple acceptable roles
*/
export async function checkAnyTikiRole(
api: ApiPromise | null,
address: string | null | undefined,
roles: string[]
): Promise<boolean> {
if (!api || !address) {
return false;
}
try {
for (const role of roles) {
const hasRole = await checkTikiRole(api, address, role);
if (hasRole) {
return true;
}
}
return false;
} catch (error) {
console.error('Error checking any Tiki role:', error);
return false;
}
}
/**
* Check if user is an educator (Perwerdekar)
* Required for: Creating courses in Perwerde
*/
export async function checkEducatorRole(
api: ApiPromise | null,
address: string | null | undefined
): Promise<boolean> {
return checkAnyTikiRole(api, address, [
'Perwerdekar', // Educator
'Mamoste', // Teacher
'WezireCand', // Education Minister
'Rewsenbîr', // Intellectual
]);
}
/**
* Check if user can moderate (ModeratorêCivakê or higher)
* Required for: Forum moderation, governance moderation
*/
export async function checkModeratorRole(
api: ApiPromise | null,
address: string | null | undefined
): Promise<boolean> {
return checkAnyTikiRole(api, address, [
'ModeratorêCivakê', // Society Moderator
'Berdevk', // Spokesperson
'Serok', // President
'SerokWeziran', // Prime Minister
]);
}
/**
* Check if user can participate in governance (citizen or higher)
* Required for: Voting, proposing, elections
*/
export async function checkGovernanceParticipation(
api: ApiPromise | null,
address: string | null | undefined
): Promise<boolean> {
// Any citizen with approved KYC can participate
return checkCitizenStatus(api, address);
}
/**
* Check if user can create proposals
* Required for: Creating referendum proposals
*/
export async function checkProposalCreationRights(
api: ApiPromise | null,
address: string | null | undefined
): Promise<boolean> {
// Citizen + certain roles can create proposals
const isCitizen = await checkCitizenStatus(api, address);
if (!isCitizen) {
return false;
}
// Additional check: has any leadership role
return checkAnyTikiRole(api, address, [
'Parlementer', // Parliament Member
'SerokiMeclise', // Speaker
'Serok', // President
'SerokWeziran', // Prime Minister
'Wezir', // Minister
'SerokêKomele', // Community Leader
'RêveberêProjeyê', // Project Manager
]);
}
// ========================================
// STAKING SCORE CHECK
// ========================================
/**
* Check if user has started staking score tracking
* Required for: Advanced staking features
*/
export async function checkStakingScoreTracking(
api: ApiPromise | null,
address: string | null | undefined
): Promise<boolean> {
if (!api || !address) {
return false;
}
try {
if (!api.query?.stakingScore?.stakingStartBlock) {
console.warn('Staking score pallet not available');
return false;
}
const startBlock = await api.query.stakingScore.stakingStartBlock(address);
return !startBlock.isNone;
} catch (error) {
console.error('Error checking staking score tracking:', error);
return false;
}
}
// ========================================
// COMBINED PERMISSION CHECKS
// ========================================
export interface UserPermissions {
isCitizen: boolean;
isValidator: boolean;
hasStakingScore: boolean;
canVote: boolean;
canCreateProposals: boolean;
canModerate: boolean;
canCreateCourses: boolean;
tikis: string[];
}
/**
* Get all user permissions at once
* Useful for dashboard/profile pages
*/
export async function getUserPermissions(
api: ApiPromise | null,
address: string | null | undefined
): Promise<UserPermissions> {
if (!api || !address) {
return {
isCitizen: false,
isValidator: false,
hasStakingScore: false,
canVote: false,
canCreateProposals: false,
canModerate: false,
canCreateCourses: false,
tikis: [],
};
}
try {
// Fetch all in parallel
const [
isCitizen,
isValidator,
hasStakingScore,
canCreateProposals,
canModerate,
canCreateCourses,
tikiData,
] = await Promise.all([
checkCitizenStatus(api, address),
checkValidatorStatus(api, address),
checkStakingScoreTracking(api, address),
checkProposalCreationRights(api, address),
checkModeratorRole(api, address),
checkEducatorRole(api, address),
api.query?.tiki?.userTikis?.(address),
]);
// Parse tikis
const tikiIndices = tikiData?.isEmpty ? [] : (tikiData?.toJSON() as number[]);
const tikis = tikiIndices.map((index) => TIKI_ROLES[index] || `Unknown(${index})`);
return {
isCitizen,
isValidator,
hasStakingScore,
canVote: isCitizen, // Citizens can vote
canCreateProposals,
canModerate,
canCreateCourses,
tikis,
};
} catch (error) {
console.error('Error getting user permissions:', error);
return {
isCitizen: false,
isValidator: false,
hasStakingScore: false,
canVote: false,
canCreateProposals: false,
canModerate: false,
canCreateCourses: false,
tikis: [],
};
}
}
+416
View File
@@ -0,0 +1,416 @@
/**
* Perwerde (Education) Pallet Integration
*
* This module provides helper functions for interacting with the Perwerde pallet,
* which handles:
* - Course creation and management
* - Student enrollment
* - Course completion tracking
* - Education points/scores
*/
import type { ApiPromise } from '@polkadot/api';
import type { Option } from '@polkadot/types';
// ============================================================================
// TYPE DEFINITIONS
// ============================================================================
export type CourseStatus = 'Active' | 'Archived';
export interface Course {
id: number;
owner: string;
name: string;
description: string;
contentLink: string;
status: CourseStatus;
createdAt: number;
}
export interface Enrollment {
student: string;
courseId: number;
enrolledAt: number;
completedAt?: number;
pointsEarned: number;
isCompleted: boolean;
}
export interface StudentProgress {
totalCourses: number;
completedCourses: number;
totalPoints: number;
activeCourses: number;
}
// ============================================================================
// QUERY FUNCTIONS (Read-only)
// ============================================================================
/**
* Get all courses (active and archived)
*/
export async function getAllCourses(api: ApiPromise): Promise<Course[]> {
const nextId = await api.query.perwerde.nextCourseId();
const currentId = (nextId.toJSON() as number) || 0;
const courses: Course[] = [];
for (let i = 0; i < currentId; i++) {
const courseOption = await api.query.perwerde.courses(i);
if (courseOption.isSome) {
const courseData = courseOption.unwrap().toJSON() as any;
courses.push({
id: i,
owner: courseData.owner,
name: hexToString(courseData.name),
description: hexToString(courseData.description),
contentLink: hexToString(courseData.contentLink),
status: courseData.status as CourseStatus,
createdAt: courseData.createdAt,
});
}
}
return courses;
}
/**
* Get active courses only
*/
export async function getActiveCourses(api: ApiPromise): Promise<Course[]> {
const allCourses = await getAllCourses(api);
return allCourses.filter((course) => course.status === 'Active');
}
/**
* Get course by ID
*/
export async function getCourseById(api: ApiPromise, courseId: number): Promise<Course | null> {
const courseOption = await api.query.perwerde.courses(courseId);
if (courseOption.isNone) {
return null;
}
const courseData = courseOption.unwrap().toJSON() as any;
return {
id: courseId,
owner: courseData.owner,
name: hexToString(courseData.name),
description: hexToString(courseData.description),
contentLink: hexToString(courseData.contentLink),
status: courseData.status as CourseStatus,
createdAt: courseData.createdAt,
};
}
/**
* Get student's enrolled courses
*/
export async function getStudentCourses(api: ApiPromise, studentAddress: string): Promise<number[]> {
const coursesOption = await api.query.perwerde.studentCourses(studentAddress);
if (coursesOption.isNone || coursesOption.isEmpty) {
return [];
}
return (coursesOption.toJSON() as number[]) || [];
}
/**
* Get enrollment details for a student in a specific course
*/
export async function getEnrollment(
api: ApiPromise,
studentAddress: string,
courseId: number
): Promise<Enrollment | null> {
const enrollmentOption = await api.query.perwerde.enrollments([studentAddress, courseId]);
if (enrollmentOption.isNone) {
return null;
}
const enrollmentData = enrollmentOption.unwrap().toJSON() as any;
return {
student: enrollmentData.student,
courseId: enrollmentData.courseId,
enrolledAt: enrollmentData.enrolledAt,
completedAt: enrollmentData.completedAt || undefined,
pointsEarned: enrollmentData.pointsEarned || 0,
isCompleted: !!enrollmentData.completedAt,
};
}
/**
* Get student's progress summary
*/
export async function getStudentProgress(api: ApiPromise, studentAddress: string): Promise<StudentProgress> {
const courseIds = await getStudentCourses(api, studentAddress);
let completedCourses = 0;
let totalPoints = 0;
for (const courseId of courseIds) {
const enrollment = await getEnrollment(api, studentAddress, courseId);
if (enrollment) {
if (enrollment.isCompleted) {
completedCourses++;
totalPoints += enrollment.pointsEarned;
}
}
}
return {
totalCourses: courseIds.length,
completedCourses,
totalPoints,
activeCourses: courseIds.length - completedCourses,
};
}
/**
* Get Perwerde score for a student (sum of all earned points)
*/
export async function getPerwerdeScore(api: ApiPromise, studentAddress: string): Promise<number> {
try {
// Try to call the get_perwerde_score runtime API
// This might not exist in all versions, fallback to manual calculation
const score = await api.call.perwerdeApi?.getPerwerdeScore(studentAddress);
return score ? (score.toJSON() as number) : 0;
} catch (error) {
// Fallback: manually sum all points
const progress = await getStudentProgress(api, studentAddress);
return progress.totalPoints;
}
}
/**
* Check if student is enrolled in a course
*/
export async function isEnrolled(
api: ApiPromise,
studentAddress: string,
courseId: number
): Promise<boolean> {
const enrollment = await getEnrollment(api, studentAddress, courseId);
return enrollment !== null;
}
/**
* Get course enrollment statistics
*/
export async function getCourseStats(
api: ApiPromise,
courseId: number
): Promise<{
totalEnrollments: number;
completions: number;
averagePoints: number;
}> {
// Note: This requires iterating through all enrollments, which can be expensive
// In production, consider caching or maintaining separate counters
const entries = await api.query.perwerde.enrollments.entries();
let totalEnrollments = 0;
let completions = 0;
let totalPoints = 0;
for (const [key, value] of entries) {
const enrollmentData = value.toJSON() as any;
const enrollmentCourseId = (key.args[1] as any).toNumber();
if (enrollmentCourseId === courseId) {
totalEnrollments++;
if (enrollmentData.completedAt) {
completions++;
totalPoints += enrollmentData.pointsEarned || 0;
}
}
}
return {
totalEnrollments,
completions,
averagePoints: completions > 0 ? Math.round(totalPoints / completions) : 0,
};
}
// ============================================================================
// TRANSACTION FUNCTIONS
// ============================================================================
/**
* Create a new course
* @requires AdminOrigin (only admin can create courses in current implementation)
*/
export async function createCourse(
api: ApiPromise,
signer: any,
name: string,
description: string,
contentLink: string
): Promise<void> {
const tx = api.tx.perwerde.createCourse(name, description, contentLink);
return new Promise((resolve, reject) => {
tx.signAndSend(signer, ({ status, dispatchError }) => {
if (status.isInBlock) {
if (dispatchError) {
reject(dispatchError);
} else {
resolve();
}
}
});
});
}
/**
* Enroll in a course
*/
export async function enrollInCourse(
api: ApiPromise,
signerAddress: string,
courseId: number
): Promise<void> {
const tx = api.tx.perwerde.enroll(courseId);
return new Promise((resolve, reject) => {
tx.signAndSend(signerAddress, ({ status, dispatchError }) => {
if (status.isInBlock) {
if (dispatchError) {
reject(dispatchError);
} else {
resolve();
}
}
});
});
}
/**
* Complete a course
* @requires Course owner to call this for student
*/
export async function completeCourse(
api: ApiPromise,
signer: any,
studentAddress: string,
courseId: number,
points: number
): Promise<void> {
const tx = api.tx.perwerde.completeCourse(courseId, points);
return new Promise((resolve, reject) => {
tx.signAndSend(signer, ({ status, dispatchError }) => {
if (status.isInBlock) {
if (dispatchError) {
reject(dispatchError);
} else {
resolve();
}
}
});
});
}
/**
* Archive a course
* @requires Course owner
*/
export async function archiveCourse(
api: ApiPromise,
signer: any,
courseId: number
): Promise<void> {
const tx = api.tx.perwerde.archiveCourse(courseId);
return new Promise((resolve, reject) => {
tx.signAndSend(signer, ({ status, dispatchError }) => {
if (status.isInBlock) {
if (dispatchError) {
reject(dispatchError);
} else {
resolve();
}
}
});
});
}
// ============================================================================
// HELPER FUNCTIONS
// ============================================================================
/**
* Convert hex string to UTF-8 string
*/
function hexToString(hex: any): string {
if (!hex) return '';
// If it's already a string, return it
if (typeof hex === 'string' && !hex.startsWith('0x')) {
return hex;
}
// If it's a hex string, convert it
const hexStr = hex.toString().replace(/^0x/, '');
let str = '';
for (let i = 0; i < hexStr.length; i += 2) {
const code = parseInt(hexStr.substr(i, 2), 16);
if (code !== 0) {
// Skip null bytes
str += String.fromCharCode(code);
}
}
return str.trim();
}
/**
* Get course difficulty label (based on points threshold)
*/
export function getCourseDifficulty(averagePoints: number): {
label: string;
color: string;
} {
if (averagePoints >= 100) {
return { label: 'Advanced', color: 'red' };
} else if (averagePoints >= 50) {
return { label: 'Intermediate', color: 'yellow' };
} else {
return { label: 'Beginner', color: 'green' };
}
}
/**
* Format IPFS link to gateway URL
*/
export function formatIPFSLink(ipfsHash: string): string {
if (!ipfsHash) return '';
// If already a full URL, return it
if (ipfsHash.startsWith('http')) {
return ipfsHash;
}
// If starts with ipfs://, convert to gateway
if (ipfsHash.startsWith('ipfs://')) {
const hash = ipfsHash.replace('ipfs://', '');
return `https://ipfs.io/ipfs/${hash}`;
}
// If it's just a hash, add gateway
return `https://ipfs.io/ipfs/${ipfsHash}`;
}
+616
View File
@@ -0,0 +1,616 @@
/**
* Welati (Elections & Governance) Pallet Integration
*
* This module provides helper functions for interacting with the Welati pallet,
* which handles:
* - Presidential and Parliamentary Elections
* - Speaker and Constitutional Court Elections
* - Official Appointments (Ministers, Diwan)
* - Collective Proposals (Parliament/Diwan voting)
*/
import type { ApiPromise } from '@polkadot/api';
import type { Option, Vec } from '@polkadot/types';
import type { AccountId, BlockNumber } from '@polkadot/types/interfaces';
// ============================================================================
// TYPE DEFINITIONS
// ============================================================================
export type ElectionType = 'Presidential' | 'Parliamentary' | 'SpeakerElection' | 'ConstitutionalCourt';
export type ElectionStatus = 'CandidacyPeriod' | 'CampaignPeriod' | 'VotingPeriod' | 'Completed';
export type VoteChoice = 'Aye' | 'Nay' | 'Abstain';
export type CollectiveDecisionType =
| 'ParliamentSimpleMajority'
| 'ParliamentSuperMajority'
| 'ParliamentAbsoluteMajority'
| 'ConstitutionalReview'
| 'ConstitutionalUnanimous'
| 'ExecutiveDecision';
export type ProposalPriority = 'Urgent' | 'High' | 'Normal' | 'Low';
export type ProposalStatus = 'Active' | 'Approved' | 'Rejected' | 'Expired' | 'Executed';
export type MinisterRole =
| 'WezireDarayiye' // Finance
| 'WezireParez' // Defense
| 'WezireDad' // Justice
| 'WezireBelaw' // Education
| 'WezireTend' // Health
| 'WezireAva' // Water Resources
| 'WezireCand'; // Culture
export type GovernmentPosition = 'Serok' | 'SerokWeziran' | 'MeclisBaskanı';
export interface ElectionInfo {
electionId: number;
electionType: ElectionType;
status: ElectionStatus;
startBlock: number;
candidacyEndBlock: number;
campaignEndBlock: number;
votingEndBlock: number;
totalCandidates: number;
totalVotes: number;
turnoutPercentage: number;
districtCount?: number;
}
export interface CandidateInfo {
account: string;
districtId?: number;
registeredAt: number;
endorsersCount: number;
voteCount: number;
depositPaid: string;
}
export interface ElectionResult {
electionId: number;
winners: string[];
totalVotes: number;
turnoutPercentage: number;
finalizedAt: number;
runoffRequired: boolean;
}
export interface ParliamentMember {
account: string;
electedAt: number;
termEndsAt: number;
votesParticipated: number;
totalVotesEligible: number;
participationRate: number;
committees: string[];
}
export interface CollectiveProposal {
proposalId: number;
proposer: string;
title: string;
description: string;
proposedAt: number;
votingStartsAt: number;
expiresAt: number;
decisionType: CollectiveDecisionType;
status: ProposalStatus;
ayeVotes: number;
nayVotes: number;
abstainVotes: number;
threshold: number;
votesCast: number;
priority: ProposalPriority;
}
export interface AppointmentProcess {
processId: number;
nominee: string;
role: string;
nominator: string;
justification: string;
status: 'Pending' | 'Approved' | 'Rejected';
createdAt: number;
deadline: number;
}
export interface GovernanceMetrics {
totalElectionsHeld: number;
activeElections: number;
parliamentSize: number;
diwanSize: number;
activeProposals: number;
totalProposalsSubmitted: number;
averageTurnout: number;
}
// ============================================================================
// QUERY FUNCTIONS (Read-only)
// ============================================================================
/**
* Get current government officials
*/
export async function getCurrentOfficials(api: ApiPromise): Promise<{
serok?: string;
serokWeziran?: string;
meclisBaskanı?: string;
}> {
const [serok, serokWeziran, speaker] = await Promise.all([
api.query.welati.currentOfficials('Serok'),
api.query.welati.currentOfficials('SerokWeziran'),
api.query.welati.currentOfficials('MeclisBaskanı'),
]);
return {
serok: serok.isSome ? serok.unwrap().toString() : undefined,
serokWeziran: serokWeziran.isSome ? serokWeziran.unwrap().toString() : undefined,
meclisBaskanı: speaker.isSome ? speaker.unwrap().toString() : undefined,
};
}
/**
* Get current cabinet ministers
*/
export async function getCurrentMinisters(api: ApiPromise): Promise<Record<MinisterRole, string | undefined>> {
const roles: MinisterRole[] = [
'WezireDarayiye',
'WezireParez',
'WezireDad',
'WezireBelaw',
'WezireTend',
'WezireAva',
'WezireCand',
];
const ministers = await Promise.all(
roles.map(role => api.query.welati.currentMinisters(role))
);
const result: Record<string, string | undefined> = {};
roles.forEach((role, index) => {
result[role] = ministers[index].isSome ? ministers[index].unwrap().toString() : undefined;
});
return result as Record<MinisterRole, string | undefined>;
}
/**
* Get parliament members list
*/
export async function getParliamentMembers(api: ApiPromise): Promise<ParliamentMember[]> {
const members = await api.query.welati.parliamentMembers();
if (!members || members.isEmpty) {
return [];
}
const memberList: ParliamentMember[] = [];
const accountIds = members.toJSON() as string[];
for (const accountId of accountIds) {
// In a real implementation, fetch detailed member info
// For now, return basic structure
memberList.push({
account: accountId,
electedAt: 0,
termEndsAt: 0,
votesParticipated: 0,
totalVotesEligible: 0,
participationRate: 0,
committees: [],
});
}
return memberList;
}
/**
* Get Diwan (Constitutional Court) members
*/
export async function getDiwanMembers(api: ApiPromise): Promise<string[]> {
const members = await api.query.welati.diwanMembers();
if (!members || members.isEmpty) {
return [];
}
return (members.toJSON() as string[]) || [];
}
/**
* Get active elections
*/
export async function getActiveElections(api: ApiPromise): Promise<ElectionInfo[]> {
const nextId = await api.query.welati.nextElectionId();
const currentId = (nextId.toJSON() as number) || 0;
const elections: ElectionInfo[] = [];
// Query last 10 elections
for (let i = Math.max(0, currentId - 10); i < currentId; i++) {
const election = await api.query.welati.activeElections(i);
if (election.isSome) {
const data = election.unwrap().toJSON() as any;
elections.push({
electionId: i,
electionType: data.electionType as ElectionType,
status: data.status as ElectionStatus,
startBlock: data.startBlock,
candidacyEndBlock: data.candidacyEndBlock,
campaignEndBlock: data.campaignEndBlock,
votingEndBlock: data.votingEndBlock,
totalCandidates: data.totalCandidates || 0,
totalVotes: data.totalVotes || 0,
turnoutPercentage: data.turnoutPercentage || 0,
districtCount: data.districtCount,
});
}
}
return elections.filter(e => e.status !== 'Completed');
}
/**
* Get election by ID
*/
export async function getElectionById(api: ApiPromise, electionId: number): Promise<ElectionInfo | null> {
const election = await api.query.welati.activeElections(electionId);
if (election.isNone) {
return null;
}
const data = election.unwrap().toJSON() as any;
return {
electionId,
electionType: data.electionType as ElectionType,
status: data.status as ElectionStatus,
startBlock: data.startBlock,
candidacyEndBlock: data.candidacyEndBlock,
campaignEndBlock: data.campaignEndBlock,
votingEndBlock: data.votingEndBlock,
totalCandidates: data.totalCandidates || 0,
totalVotes: data.totalVotes || 0,
turnoutPercentage: data.turnoutPercentage || 0,
districtCount: data.districtCount,
};
}
/**
* Get candidates for an election
*/
export async function getElectionCandidates(
api: ApiPromise,
electionId: number
): Promise<CandidateInfo[]> {
const entries = await api.query.welati.electionCandidates.entries(electionId);
const candidates: CandidateInfo[] = [];
for (const [key, value] of entries) {
const data = value.toJSON() as any;
const account = (key.args[1] as AccountId).toString();
candidates.push({
account,
districtId: data.districtId,
registeredAt: data.registeredAt,
endorsersCount: data.endorsers?.length || 0,
voteCount: data.voteCount || 0,
depositPaid: data.depositPaid?.toString() || '0',
});
}
return candidates.sort((a, b) => b.voteCount - a.voteCount);
}
/**
* Check if user has voted in an election
*/
export async function hasVoted(
api: ApiPromise,
electionId: number,
voterAddress: string
): Promise<boolean> {
const vote = await api.query.welati.electionVotes(electionId, voterAddress);
return vote.isSome;
}
/**
* Get election results
*/
export async function getElectionResults(
api: ApiPromise,
electionId: number
): Promise<ElectionResult | null> {
const result = await api.query.welati.electionResults(electionId);
if (result.isNone) {
return null;
}
const data = result.unwrap().toJSON() as any;
return {
electionId,
winners: data.winners || [],
totalVotes: data.totalVotes || 0,
turnoutPercentage: data.turnoutPercentage || 0,
finalizedAt: data.finalizedAt || 0,
runoffRequired: data.runoffRequired || false,
};
}
/**
* Get active proposals
*/
export async function getActiveProposals(api: ApiPromise): Promise<CollectiveProposal[]> {
const nextId = await api.query.welati.nextProposalId();
const currentId = (nextId.toJSON() as number) || 0;
const proposals: CollectiveProposal[] = [];
// Query last 50 proposals
for (let i = Math.max(0, currentId - 50); i < currentId; i++) {
const proposal = await api.query.welati.activeProposals(i);
if (proposal.isSome) {
const data = proposal.unwrap().toJSON() as any;
proposals.push({
proposalId: i,
proposer: data.proposer,
title: data.title,
description: data.description,
proposedAt: data.proposedAt,
votingStartsAt: data.votingStartsAt,
expiresAt: data.expiresAt,
decisionType: data.decisionType as CollectiveDecisionType,
status: data.status as ProposalStatus,
ayeVotes: data.ayeVotes || 0,
nayVotes: data.nayVotes || 0,
abstainVotes: data.abstainVotes || 0,
threshold: data.threshold || 0,
votesCast: data.votesCast || 0,
priority: data.priority as ProposalPriority,
});
}
}
return proposals.filter(p => p.status === 'Active').reverse();
}
/**
* Get proposal by ID
*/
export async function getProposalById(
api: ApiPromise,
proposalId: number
): Promise<CollectiveProposal | null> {
const proposal = await api.query.welati.activeProposals(proposalId);
if (proposal.isNone) {
return null;
}
const data = proposal.unwrap().toJSON() as any;
return {
proposalId,
proposer: data.proposer,
title: data.title,
description: data.description,
proposedAt: data.proposedAt,
votingStartsAt: data.votingStartsAt,
expiresAt: data.expiresAt,
decisionType: data.decisionType as CollectiveDecisionType,
status: data.status as ProposalStatus,
ayeVotes: data.ayeVotes || 0,
nayVotes: data.nayVotes || 0,
abstainVotes: data.abstainVotes || 0,
threshold: data.threshold || 0,
votesCast: data.votesCast || 0,
priority: data.priority as ProposalPriority,
};
}
/**
* Check if user has voted on a proposal
*/
export async function hasVotedOnProposal(
api: ApiPromise,
proposalId: number,
voterAddress: string
): Promise<boolean> {
const vote = await api.query.welati.collectiveVotes(proposalId, voterAddress);
return vote.isSome;
}
/**
* Get user's vote on a proposal
*/
export async function getProposalVote(
api: ApiPromise,
proposalId: number,
voterAddress: string
): Promise<VoteChoice | null> {
const vote = await api.query.welati.collectiveVotes(proposalId, voterAddress);
if (vote.isNone) {
return null;
}
const data = vote.unwrap().toJSON() as any;
return data.vote as VoteChoice;
}
/**
* Get pending appointments
*/
export async function getPendingAppointments(api: ApiPromise): Promise<AppointmentProcess[]> {
const nextId = await api.query.welati.nextAppointmentId();
const currentId = (nextId.toJSON() as number) || 0;
const appointments: AppointmentProcess[] = [];
for (let i = Math.max(0, currentId - 20); i < currentId; i++) {
const appointment = await api.query.welati.appointmentProcesses(i);
if (appointment.isSome) {
const data = appointment.unwrap().toJSON() as any;
if (data.status === 'Pending') {
appointments.push({
processId: i,
nominee: data.nominee,
role: data.role,
nominator: data.nominator,
justification: data.justification,
status: data.status,
createdAt: data.createdAt,
deadline: data.deadline,
});
}
}
}
return appointments;
}
/**
* Get governance statistics
*/
export async function getGovernanceStats(api: ApiPromise): Promise<GovernanceMetrics> {
const stats = await api.query.welati.governanceStats();
if (!stats || stats.isEmpty) {
return {
totalElectionsHeld: 0,
activeElections: 0,
parliamentSize: 0,
diwanSize: 0,
activeProposals: 0,
totalProposalsSubmitted: 0,
averageTurnout: 0,
};
}
const data = stats.toJSON() as any;
return {
totalElectionsHeld: data.totalElectionsHeld || 0,
activeElections: data.activeElections || 0,
parliamentSize: data.parliamentSize || 0,
diwanSize: data.diwanSize || 0,
activeProposals: data.activeProposals || 0,
totalProposalsSubmitted: data.totalProposalsSubmitted || 0,
averageTurnout: data.averageTurnout || 0,
};
}
/**
* Get current block number
*/
export async function getCurrentBlock(api: ApiPromise): Promise<number> {
const header = await api.rpc.chain.getHeader();
return header.number.toNumber();
}
/**
* Calculate remaining blocks until deadline
*/
export async function getRemainingBlocks(api: ApiPromise, deadlineBlock: number): Promise<number> {
const currentBlock = await getCurrentBlock(api);
return Math.max(0, deadlineBlock - currentBlock);
}
/**
* Convert blocks to approximate time (6 seconds per block average)
*/
export function blocksToTime(blocks: number): {
days: number;
hours: number;
minutes: number;
} {
const seconds = blocks * 6;
const days = Math.floor(seconds / 86400);
const hours = Math.floor((seconds % 86400) / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
return { days, hours, minutes };
}
// ============================================================================
// HELPER FUNCTIONS
// ============================================================================
/**
* Get election type label
*/
export function getElectionTypeLabel(type: ElectionType): { en: string; kmr: string } {
const labels = {
Presidential: { en: 'Presidential Election', kmr: 'Hilbijartina Serokî' },
Parliamentary: { en: 'Parliamentary Election', kmr: 'Hilbijartina Parlamentoyê' },
SpeakerElection: { en: 'Speaker Election', kmr: 'Hilbijartina Serokê Parlamentoyê' },
ConstitutionalCourt: { en: 'Constitutional Court Election', kmr: 'Hilbijartina Dadgeha Destûrî' },
};
return labels[type] || { en: type, kmr: type };
}
/**
* Get election status label
*/
export function getElectionStatusLabel(status: ElectionStatus): { en: string; kmr: string } {
const labels = {
CandidacyPeriod: { en: 'Candidate Registration Open', kmr: 'Qeydkirina Berendam Vekirî ye' },
CampaignPeriod: { en: 'Campaign Period', kmr: 'Dema Kampanyayê' },
VotingPeriod: { en: 'Voting Open', kmr: 'Dengdan Vekirî ye' },
Completed: { en: 'Completed', kmr: 'Temam bû' },
};
return labels[status] || { en: status, kmr: status };
}
/**
* Get minister role label
*/
export function getMinisterRoleLabel(role: MinisterRole): { en: string; kmr: string } {
const labels = {
WezireDarayiye: { en: 'Minister of Finance', kmr: 'Wezîrê Darayiyê' },
WezireParez: { en: 'Minister of Defense', kmr: 'Wezîrê Parezê' },
WezireDad: { en: 'Minister of Justice', kmr: 'Wezîrê Dadê' },
WezireBelaw: { en: 'Minister of Education', kmr: 'Wezîrê Perwerdeyê' },
WezireTend: { en: 'Minister of Health', kmr: 'Wezîrê Tendirustiyê' },
WezireAva: { en: 'Minister of Water Resources', kmr: 'Wezîrê Avê' },
WezireCand: { en: 'Minister of Culture', kmr: 'Wezîrê Çandî' },
};
return labels[role] || { en: role, kmr: role };
}
/**
* Get proposal decision type threshold
*/
export function getDecisionTypeThreshold(type: CollectiveDecisionType, totalMembers: number): number {
switch (type) {
case 'ParliamentSimpleMajority':
return Math.floor(totalMembers / 2) + 1; // > 50%
case 'ParliamentSuperMajority':
case 'ConstitutionalReview':
return Math.ceil((totalMembers * 2) / 3); // > 66.67%
case 'ParliamentAbsoluteMajority':
return Math.ceil((totalMembers * 3) / 4); // > 75%
case 'ConstitutionalUnanimous':
return totalMembers; // 100%
default:
return Math.floor(totalMembers / 2) + 1;
}
}