feat(governance): implement real blockchain data for governance section

- Update ElectionsInterface to fetch real elections from welati pallet
- Add MyVotes component for user voting history (proposals, elections, delegations)
- Add GovernanceHistory component for completed elections and proposals
- Integrate DelegationManager into GovernanceInterface delegation tab
- Fix linter errors across multiple files (unused imports, type annotations)
- Update eslint.config.js to ignore SDK docs and CJS files
This commit is contained in:
2025-12-11 01:45:13 +03:00
parent 11678fe7cd
commit 47ea12d0de
286 changed files with 1393 additions and 317 deletions
+1 -1
View File
@@ -113,7 +113,7 @@ const subdomains = [
const ChainSpecs: React.FC = () => {
const { t } = useTranslation();
const [copiedId, setCopiedId] = useState<string | null>(null);
const [selectedSpec, setSelectedSpec] = useState<ChainSpec>(chainSpecs[0]);
const [selectedSpec] = useState<ChainSpec>(chainSpecs[0]);
const navigate = useNavigate();
const copyToClipboard = (text: string, id: string) => {
+8 -10
View File
@@ -4,6 +4,10 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs';
import GovernanceOverview from './governance/GovernanceOverview';
import ProposalsList from './governance/ProposalsList';
import ElectionsInterface from './governance/ElectionsInterface';
import DelegationManager from './delegation/DelegationManager';
import MyVotes from './governance/MyVotes';
import GovernanceHistory from './governance/GovernanceHistory';
const GovernanceInterface: React.FC = () => {
const [activeTab, setActiveTab] = useState('overview');
@@ -62,21 +66,15 @@ const GovernanceInterface: React.FC = () => {
</TabsContent>
<TabsContent value="delegation" className="mt-6">
<div className="text-center py-12 text-gray-400">
Delegation interface coming soon...
</div>
<DelegationManager />
</TabsContent>
<TabsContent value="voting" className="mt-6">
<div className="text-center py-12 text-gray-400">
Voting history coming soon...
</div>
<MyVotes />
</TabsContent>
<TabsContent value="history" className="mt-6">
<div className="text-center py-12 text-gray-400">
Governance history coming soon...
</div>
<GovernanceHistory />
</TabsContent>
</Tabs>
</div>
@@ -84,4 +82,4 @@ const GovernanceInterface: React.FC = () => {
);
};
export default GovernanceInterface;
export default GovernanceInterface;
@@ -20,11 +20,6 @@ import {
Loader2,
AlertCircle,
Download,
Upload,
Send,
Link2,
Coins,
TestTube,
ChevronRight,
} from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
@@ -61,7 +56,7 @@ interface XCMConfigurationWizardProps {
interface StepStatus {
completed: boolean;
data?: any;
data?: unknown;
error?: string;
}
@@ -1,165 +1,420 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Progress } from '@/components/ui/progress';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Vote, Trophy, AlertCircle, CheckCircle } from 'lucide-react';
import { Vote, Trophy, AlertCircle, CheckCircle, Users, Clock, Activity, Loader2 } from 'lucide-react';
import { usePolkadot } from '@/contexts/PolkadotContext';
import { useWallet } from '@/contexts/WalletContext';
import {
getActiveElections,
getElectionCandidates,
getElectionResults,
hasVoted,
blocksToTime,
getCurrentBlock,
getElectionTypeLabel,
getElectionStatusLabel,
type ElectionInfo,
type CandidateInfo,
type ElectionResult
} from '@pezkuwi/lib/welati';
import { toast } from 'sonner';
interface Election {
id: number;
type: 'Presidential' | 'Parliamentary' | 'Constitutional Court';
status: 'Registration' | 'Campaign' | 'Voting' | 'Completed';
candidates: Candidate[];
totalVotes: number;
endBlock: number;
currentBlock: number;
}
interface Candidate {
id: string;
name: string;
votes: number;
percentage: number;
party?: string;
trustScore: number;
interface ElectionWithCandidates extends ElectionInfo {
candidates: CandidateInfo[];
userHasVoted: boolean;
}
const ElectionsInterface: React.FC = () => {
const [votedCandidates, setVotedCandidates] = useState<string[]>([]);
const { api, isApiReady } = usePolkadot();
const { account, signer } = useWallet();
const [elections, setElections] = useState<ElectionWithCandidates[]>([]);
const [completedResults, setCompletedResults] = useState<ElectionResult[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [currentBlock, setCurrentBlock] = useState(0);
const [votingElectionId, setVotingElectionId] = useState<number | null>(null);
const [selectedCandidates, setSelectedCandidates] = useState<Map<number, string[]>>(new Map());
const activeElections: Election[] = [
{
id: 1,
type: 'Presidential',
status: 'Voting',
totalVotes: 45678,
endBlock: 1000000,
currentBlock: 995000,
candidates: [
{ id: '1', name: 'Candidate A', votes: 23456, percentage: 51.3, trustScore: 850 },
{ id: '2', name: 'Candidate B', votes: 22222, percentage: 48.7, trustScore: 780 }
]
},
{
id: 2,
type: 'Parliamentary',
status: 'Registration',
totalVotes: 0,
endBlock: 1200000,
currentBlock: 995000,
candidates: []
useEffect(() => {
if (!api || !isApiReady) {
setLoading(false);
return;
}
];
const handleVote = (candidateId: string, electionType: string) => {
if (electionType === 'Parliamentary') {
setVotedCandidates(prev =>
prev.includes(candidateId)
? prev.filter(id => id !== candidateId)
: [...prev, candidateId]
);
} else {
setVotedCandidates([candidateId]);
const fetchElectionData = async () => {
try {
setLoading(true);
setError(null);
// Get current block
const block = await getCurrentBlock(api);
setCurrentBlock(block);
// Get active elections
const activeElections = await getActiveElections(api);
// Fetch candidates for each election
const electionsWithCandidates: ElectionWithCandidates[] = await Promise.all(
activeElections.map(async (election) => {
const candidates = await getElectionCandidates(api, election.electionId);
const userHasVoted = account
? await hasVoted(api, election.electionId, account)
: false;
return {
...election,
candidates,
userHasVoted
};
})
);
setElections(electionsWithCandidates);
// Get completed election results (last 5)
const results: ElectionResult[] = [];
for (let i = 0; i < 5; i++) {
const result = await getElectionResults(api, i);
if (result) {
results.push(result);
}
}
setCompletedResults(results);
} catch (err) {
console.error('Error fetching election data:', err);
setError(err instanceof Error ? err.message : 'Failed to fetch election data');
} finally {
setLoading(false);
}
};
fetchElectionData();
// Refresh every 30 seconds
const interval = setInterval(fetchElectionData, 30000);
return () => clearInterval(interval);
}, [api, isApiReady, account]);
const handleVote = async (electionId: number, candidateAccount: string, electionType: string) => {
if (!api || !account || !signer) {
toast.error('Please connect your wallet first');
return;
}
try {
setVotingElectionId(electionId);
// Handle multi-select for parliamentary elections
if (electionType === 'Parliamentary') {
const current = selectedCandidates.get(electionId) || [];
const updated = current.includes(candidateAccount)
? current.filter(c => c !== candidateAccount)
: [...current, candidateAccount];
setSelectedCandidates(new Map(selectedCandidates.set(electionId, updated)));
setVotingElectionId(null);
return;
}
// Single vote for other elections
const tx = api.tx.welati.vote(electionId, candidateAccount);
await tx.signAndSend(account, { signer }, ({ status, dispatchError }) => {
if (status.isInBlock) {
toast.success('Vote submitted successfully!');
setVotingElectionId(null);
}
if (dispatchError) {
if (dispatchError.isModule) {
const decoded = api.registry.findMetaError(dispatchError.asModule);
toast.error(`Vote failed: ${decoded.name}`);
} else {
toast.error(`Vote failed: ${dispatchError.toString()}`);
}
setVotingElectionId(null);
}
});
} catch (err) {
console.error('Error voting:', err);
toast.error('Failed to submit vote');
setVotingElectionId(null);
}
};
const submitParliamentaryVotes = async (electionId: number) => {
if (!api || !account || !signer) {
toast.error('Please connect your wallet first');
return;
}
const candidates = selectedCandidates.get(electionId) || [];
if (candidates.length === 0) {
toast.error('Please select at least one candidate');
return;
}
try {
setVotingElectionId(electionId);
const tx = api.tx.welati.voteMultiple(electionId, candidates);
await tx.signAndSend(account, { signer }, ({ status, dispatchError }) => {
if (status.isInBlock) {
toast.success('Votes submitted successfully!');
setSelectedCandidates(new Map(selectedCandidates.set(electionId, [])));
setVotingElectionId(null);
}
if (dispatchError) {
if (dispatchError.isModule) {
const decoded = api.registry.findMetaError(dispatchError.asModule);
toast.error(`Vote failed: ${decoded.name}`);
} else {
toast.error(`Vote failed: ${dispatchError.toString()}`);
}
setVotingElectionId(null);
}
});
} catch (err) {
console.error('Error voting:', err);
toast.error('Failed to submit votes');
setVotingElectionId(null);
}
};
const formatRemainingTime = (endBlock: number) => {
const remaining = endBlock - currentBlock;
if (remaining <= 0) return 'Ended';
const time = blocksToTime(remaining);
if (time.days > 0) return `${time.days}d ${time.hours}h remaining`;
if (time.hours > 0) return `${time.hours}h ${time.minutes}m remaining`;
return `${time.minutes}m remaining`;
};
const getStatusColor = (status: string) => {
switch (status) {
case 'VotingPeriod': return 'bg-green-500';
case 'CampaignPeriod': return 'bg-blue-500';
case 'CandidacyPeriod': return 'bg-yellow-500';
default: return 'bg-gray-500';
}
};
if (loading) {
return (
<div className="flex items-center justify-center py-12">
<Loader2 className="w-8 h-8 animate-spin text-green-500" />
<span className="ml-3 text-gray-400">Loading elections from blockchain...</span>
</div>
);
}
if (error) {
return (
<Card className="border-red-500/30 bg-red-500/10">
<CardContent className="pt-6">
<div className="flex items-center text-red-400">
<AlertCircle className="w-5 h-5 mr-2" />
Error loading elections: {error}
</div>
</CardContent>
</Card>
);
}
return (
<div className="space-y-6">
{/* Live Data Indicator */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Badge variant="outline" className="bg-green-500/10 border-green-500 text-green-400">
<Activity className="h-3 w-3 mr-1" />
Live Blockchain Data
</Badge>
<span className="text-sm text-gray-500">Block #{currentBlock.toLocaleString()}</span>
</div>
</div>
<Tabs defaultValue="active" className="w-full">
<TabsList className="grid w-full grid-cols-3">
<TabsList className="grid w-full grid-cols-3 bg-gray-800/50">
<TabsTrigger value="active">Active Elections</TabsTrigger>
<TabsTrigger value="register">Register</TabsTrigger>
<TabsTrigger value="results">Results</TabsTrigger>
</TabsList>
<TabsContent value="active" className="space-y-4">
{activeElections.map(election => (
<Card key={election.id}>
<CardHeader>
<div className="flex justify-between items-start">
<div>
<CardTitle>{election.type} Election</CardTitle>
<CardDescription>
{election.status === 'Voting'
? `${election.totalVotes.toLocaleString()} votes cast`
: `Registration ends in ${(election.endBlock - election.currentBlock).toLocaleString()} blocks`}
</CardDescription>
</div>
<Badge variant={election.status === 'Voting' ? 'default' : 'secondary'}>
{election.status}
</Badge>
</div>
</CardHeader>
<CardContent>
{election.status === 'Voting' && (
<div className="space-y-4">
{election.candidates.map(candidate => (
<div key={candidate.id} className="space-y-2">
<div className="flex justify-between items-center">
<div>
<p className="font-medium">{candidate.name}</p>
<p className="text-sm text-muted-foreground">
Trust Score: {candidate.trustScore}
</p>
</div>
<div className="text-right">
<p className="font-bold">{candidate.percentage}%</p>
<p className="text-sm text-muted-foreground">
{candidate.votes.toLocaleString()} votes
</p>
</div>
</div>
<Progress value={candidate.percentage} className="h-2" />
<Button
size="sm"
variant={votedCandidates.includes(candidate.id) ? "default" : "outline"}
onClick={() => handleVote(candidate.id, election.type)}
className="w-full"
>
{votedCandidates.includes(candidate.id) ? (
<>
<CheckCircle className="w-4 h-4 mr-2" />
Voted
</>
) : (
<>
<Vote className="w-4 h-4 mr-2" />
Vote
</>
)}
</Button>
</div>
))}
{election.type === 'Parliamentary' && (
<p className="text-sm text-muted-foreground text-center">
You can select multiple candidates
</p>
)}
</div>
)}
{elections.length === 0 ? (
<Card className="bg-gray-900/50 border-gray-800">
<CardContent className="pt-6 text-center text-gray-400">
<Users className="w-12 h-12 mx-auto mb-4 opacity-50" />
<p>No active elections at this time</p>
<p className="text-sm mt-2">Check back later for upcoming elections</p>
</CardContent>
</Card>
))}
) : (
elections.map(election => (
<Card key={election.electionId} className="bg-gray-900/50 border-gray-800">
<CardHeader>
<div className="flex justify-between items-start">
<div>
<CardTitle className="text-white">
{getElectionTypeLabel(election.electionType).en}
</CardTitle>
<CardDescription className="flex items-center gap-2 mt-1">
<Clock className="w-4 h-4" />
{election.status === 'VotingPeriod'
? `${election.totalVotes.toLocaleString()} votes cast • ${formatRemainingTime(election.votingEndBlock)}`
: formatRemainingTime(election.candidacyEndBlock)}
</CardDescription>
</div>
<div className="flex flex-col items-end gap-2">
<Badge className={`${getStatusColor(election.status)} text-white`}>
{getElectionStatusLabel(election.status).en}
</Badge>
{election.userHasVoted && (
<Badge variant="outline" className="border-green-500 text-green-400">
<CheckCircle className="w-3 h-3 mr-1" />
You Voted
</Badge>
)}
</div>
</div>
</CardHeader>
<CardContent>
{election.status === 'VotingPeriod' && (
<div className="space-y-4">
{election.candidates.length === 0 ? (
<p className="text-gray-400 text-center py-4">No candidates registered</p>
) : (
<>
{election.candidates.map(candidate => {
const percentage = election.totalVotes > 0
? (candidate.voteCount / election.totalVotes) * 100
: 0;
const isSelected = (selectedCandidates.get(election.electionId) || [])
.includes(candidate.account);
return (
<div key={candidate.account} className="space-y-2">
<div className="flex justify-between items-center">
<div>
<p className="font-medium text-white">
{candidate.account.substring(0, 8)}...{candidate.account.slice(-6)}
</p>
<p className="text-sm text-gray-400">
{candidate.endorsersCount} endorsements
</p>
</div>
<div className="text-right">
<p className="font-bold text-white">{percentage.toFixed(1)}%</p>
<p className="text-sm text-gray-400">
{candidate.voteCount.toLocaleString()} votes
</p>
</div>
</div>
<Progress value={percentage} className="h-2" />
{!election.userHasVoted && (
<Button
size="sm"
variant={isSelected ? "default" : "outline"}
onClick={() => handleVote(election.electionId, candidate.account, election.electionType)}
disabled={votingElectionId === election.electionId}
className="w-full"
>
{votingElectionId === election.electionId ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Submitting...
</>
) : isSelected ? (
<>
<CheckCircle className="w-4 h-4 mr-2" />
Selected
</>
) : (
<>
<Vote className="w-4 h-4 mr-2" />
{election.electionType === 'Parliamentary' ? 'Select' : 'Vote'}
</>
)}
</Button>
)}
</div>
);
})}
{election.electionType === 'Parliamentary' && !election.userHasVoted && (
<div className="mt-4 pt-4 border-t border-gray-700">
<p className="text-sm text-gray-400 text-center mb-3">
Select multiple candidates for parliamentary election
</p>
<Button
onClick={() => submitParliamentaryVotes(election.electionId)}
disabled={votingElectionId === election.electionId ||
(selectedCandidates.get(election.electionId) || []).length === 0}
className="w-full bg-green-600 hover:bg-green-700"
>
{votingElectionId === election.electionId ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Submitting Votes...
</>
) : (
<>
<Vote className="w-4 h-4 mr-2" />
Submit {(selectedCandidates.get(election.electionId) || []).length} Vote(s)
</>
)}
</Button>
</div>
)}
</>
)}
</div>
)}
{election.status === 'CandidacyPeriod' && (
<div className="text-center py-4">
<p className="text-gray-400 mb-4">
{election.totalCandidates} candidates registered so far
</p>
<Button variant="outline">
Register as Candidate
</Button>
</div>
)}
{election.status === 'CampaignPeriod' && (
<div className="text-center py-4 text-gray-400">
<p>{election.totalCandidates} candidates competing</p>
<p className="text-sm mt-2">Voting begins {formatRemainingTime(election.campaignEndBlock)}</p>
</div>
)}
</CardContent>
</Card>
))
)}
</TabsContent>
<TabsContent value="register">
<Card>
<Card className="bg-gray-900/50 border-gray-800">
<CardHeader>
<CardTitle>Candidate Registration</CardTitle>
<CardTitle className="text-white">Candidate Registration</CardTitle>
<CardDescription>
Register as a candidate for upcoming elections
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="p-4 bg-amber-50 dark:bg-amber-900/20 rounded-lg">
<div className="p-4 bg-amber-900/20 border border-amber-500/30 rounded-lg">
<div className="flex items-start space-x-3">
<AlertCircle className="w-5 h-5 text-amber-600 mt-0.5" />
<AlertCircle className="w-5 h-5 text-amber-500 mt-0.5" />
<div>
<p className="font-medium text-amber-900 dark:text-amber-100">
<p className="font-medium text-amber-400">
Requirements
</p>
<ul className="text-sm text-amber-800 dark:text-amber-200 mt-2 space-y-1">
<ul className="text-sm text-amber-300/80 mt-2 space-y-1">
<li> Minimum Trust Score: 300 (Parliamentary) / 600 (Presidential)</li>
<li> KYC Approved Status</li>
<li> Endorsements: 10 (Parliamentary) / 50 (Presidential)</li>
@@ -168,7 +423,7 @@ const ElectionsInterface: React.FC = () => {
</div>
</div>
</div>
<Button className="w-full" size="lg">
<Button className="w-full bg-green-600 hover:bg-green-700" size="lg">
Register as Candidate
</Button>
</CardContent>
@@ -176,40 +431,60 @@ const ElectionsInterface: React.FC = () => {
</TabsContent>
<TabsContent value="results">
<Card>
<Card className="bg-gray-900/50 border-gray-800">
<CardHeader>
<CardTitle>Election Results</CardTitle>
<CardTitle className="text-white">Election Results</CardTitle>
<CardDescription>Historical election outcomes</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="p-4 border rounded-lg">
<div className="flex justify-between items-start mb-3">
<div>
<p className="font-medium">Presidential Election 2024</p>
<p className="text-sm text-muted-foreground">Completed 30 days ago</p>
</div>
<Badge variant="outline">
<Trophy className="w-3 h-3 mr-1" />
Completed
</Badge>
</div>
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span>Winner: Candidate A</span>
<span className="font-bold">52.8%</span>
</div>
<div className="flex justify-between text-sm text-muted-foreground">
<span>Total Votes</span>
<span>89,234</span>
</div>
<div className="flex justify-between text-sm text-muted-foreground">
<span>Turnout</span>
<span>67.5%</span>
</div>
</div>
{completedResults.length === 0 ? (
<div className="text-center py-8 text-gray-400">
<Trophy className="w-12 h-12 mx-auto mb-4 opacity-50" />
<p>No completed elections yet</p>
</div>
</div>
) : (
<div className="space-y-4">
{completedResults.map((result) => (
<div key={result.electionId} className="p-4 border border-gray-700 rounded-lg">
<div className="flex justify-between items-start mb-3">
<div>
<p className="font-medium text-white">Election #{result.electionId}</p>
<p className="text-sm text-gray-400">
Finalized at block #{result.finalizedAt.toLocaleString()}
</p>
</div>
<Badge variant="outline" className="border-green-500 text-green-400">
<Trophy className="w-3 h-3 mr-1" />
Completed
</Badge>
</div>
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-gray-400">Winner(s)</span>
<span className="font-medium text-white">
{result.winners.length > 0
? result.winners.map(w => `${w.substring(0, 8)}...`).join(', ')
: 'N/A'}
</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-400">Total Votes</span>
<span className="text-white">{result.totalVotes.toLocaleString()}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-400">Turnout</span>
<span className="text-white">{result.turnoutPercentage}%</span>
</div>
{result.runoffRequired && (
<Badge className="bg-yellow-500/20 text-yellow-400">
Runoff Required
</Badge>
)}
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
</TabsContent>
@@ -218,4 +493,4 @@ const ElectionsInterface: React.FC = () => {
);
};
export default ElectionsInterface;
export default ElectionsInterface;
@@ -0,0 +1,373 @@
import React, { useState, useEffect } from 'react';
import { Card, CardContent } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { FileText, Users, Trophy, CheckCircle, XCircle, Clock, Activity, Loader2, TrendingUp, Calendar } from 'lucide-react';
import { usePolkadot } from '@/contexts/PolkadotContext';
import {
getElectionResults,
getGovernanceStats,
blocksToTime,
getCurrentBlock,
type ElectionResult,
type GovernanceMetrics
} from '@pezkuwi/lib/welati';
interface CompletedProposal {
proposalId: number;
title: string;
proposer: string;
status: 'Approved' | 'Rejected' | 'Expired';
ayeVotes: number;
nayVotes: number;
finalizedAt: number;
decisionType: string;
}
const GovernanceHistory: React.FC = () => {
const { api, isApiReady } = usePolkadot();
const [completedElections, setCompletedElections] = useState<ElectionResult[]>([]);
const [completedProposals, setCompletedProposals] = useState<CompletedProposal[]>([]);
const [stats, setStats] = useState<GovernanceMetrics | null>(null);
const [currentBlock, setCurrentBlock] = useState(0);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!api || !isApiReady) {
setLoading(false);
return;
}
const fetchHistory = async () => {
try {
setLoading(true);
setError(null);
// Get current block
const block = await getCurrentBlock(api);
setCurrentBlock(block);
// Get governance stats
const governanceStats = await getGovernanceStats(api);
setStats(governanceStats);
// Get completed elections
const elections: ElectionResult[] = [];
if (api.query.welati?.nextElectionId) {
const nextId = await api.query.welati.nextElectionId();
const currentId = (nextId.toJSON() as number) || 0;
for (let i = 0; i < currentId; i++) {
const result = await getElectionResults(api, i);
if (result && result.totalVotes > 0) {
elections.push(result);
}
}
}
setCompletedElections(elections.reverse());
// Get completed proposals
const proposals: CompletedProposal[] = [];
if (api.query.welati?.nextProposalId) {
const nextId = await api.query.welati.nextProposalId();
const currentId = (nextId.toJSON() as number) || 0;
for (let i = Math.max(0, currentId - 100); i < currentId; i++) {
const proposal = await api.query.welati.activeProposals(i);
if (proposal.isSome) {
const data = proposal.unwrap().toJSON() as Record<string, unknown>;
if (data.status !== 'Active') {
proposals.push({
proposalId: i,
title: (data.title as string) || `Proposal #${i}`,
proposer: (data.proposer as string) || 'Unknown',
status: data.status as 'Approved' | 'Rejected' | 'Expired',
ayeVotes: (data.ayeVotes as number) || 0,
nayVotes: (data.nayVotes as number) || 0,
finalizedAt: (data.expiresAt as number) || 0,
decisionType: (data.decisionType as string) || 'Unknown'
});
}
}
}
}
// Also check democracy referenda
if (api.query.democracy?.referendumInfoOf) {
const entries = await api.query.democracy.referendumInfoOf.entries();
for (const [key, value] of entries) {
const refIndex = key.args[0].toNumber();
const info = value.toJSON() as Record<string, unknown>;
if (info?.finished) {
const finished = info.finished as Record<string, unknown>;
proposals.push({
proposalId: refIndex,
title: `Democracy Referendum #${refIndex}`,
proposer: 'Democracy',
status: finished.approved ? 'Approved' : 'Rejected',
ayeVotes: 0,
nayVotes: 0,
finalizedAt: (finished.end as number) || 0,
decisionType: 'Democracy'
});
}
}
}
setCompletedProposals(proposals.sort((a, b) => b.finalizedAt - a.finalizedAt));
} catch (err) {
console.error('Error fetching governance history:', err);
setError(err instanceof Error ? err.message : 'Failed to fetch history');
} finally {
setLoading(false);
}
};
fetchHistory();
const interval = setInterval(fetchHistory, 60000); // Refresh every minute
return () => clearInterval(interval);
}, [api, isApiReady]);
const formatBlockTime = (blockNumber: number) => {
if (!blockNumber || blockNumber === 0) return 'Unknown';
const blocksAgo = currentBlock - blockNumber;
if (blocksAgo < 0) return 'Future';
const time = blocksToTime(blocksAgo);
if (time.days > 0) return `${time.days}d ago`;
if (time.hours > 0) return `${time.hours}h ago`;
return `${time.minutes}m ago`;
};
const getStatusIcon = (status: string) => {
switch (status) {
case 'Approved': return <CheckCircle className="w-4 h-4 text-green-400" />;
case 'Rejected': return <XCircle className="w-4 h-4 text-red-400" />;
default: return <Clock className="w-4 h-4 text-gray-400" />;
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'Approved': return 'bg-green-500/20 text-green-400';
case 'Rejected': return 'bg-red-500/20 text-red-400';
case 'Expired': return 'bg-yellow-500/20 text-yellow-400';
default: return 'bg-gray-500/20 text-gray-400';
}
};
if (loading) {
return (
<div className="flex items-center justify-center py-12">
<Loader2 className="w-8 h-8 animate-spin text-green-500" />
<span className="ml-3 text-gray-400">Loading governance history...</span>
</div>
);
}
if (error) {
return (
<Card className="border-red-500/30 bg-red-500/10">
<CardContent className="pt-6">
<div className="flex items-center text-red-400">
<XCircle className="w-5 h-5 mr-2" />
Error: {error}
</div>
</CardContent>
</Card>
);
}
return (
<div className="space-y-6">
{/* Overall Stats */}
{stats && (
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card className="bg-gray-900/50 border-gray-800">
<CardContent className="p-4">
<div className="flex items-center gap-3">
<Trophy className="w-8 h-8 text-yellow-500" />
<div>
<div className="text-2xl font-bold text-white">{stats.totalElectionsHeld}</div>
<div className="text-sm text-gray-400">Elections Held</div>
</div>
</div>
</CardContent>
</Card>
<Card className="bg-gray-900/50 border-gray-800">
<CardContent className="p-4">
<div className="flex items-center gap-3">
<FileText className="w-8 h-8 text-purple-500" />
<div>
<div className="text-2xl font-bold text-white">{stats.totalProposalsSubmitted}</div>
<div className="text-sm text-gray-400">Total Proposals</div>
</div>
</div>
</CardContent>
</Card>
<Card className="bg-gray-900/50 border-gray-800">
<CardContent className="p-4">
<div className="flex items-center gap-3">
<Users className="w-8 h-8 text-cyan-500" />
<div>
<div className="text-2xl font-bold text-white">{stats.parliamentSize}</div>
<div className="text-sm text-gray-400">Parliament Size</div>
</div>
</div>
</CardContent>
</Card>
<Card className="bg-gray-900/50 border-gray-800">
<CardContent className="p-4">
<div className="flex items-center gap-3">
<TrendingUp className="w-8 h-8 text-green-500" />
<div>
<div className="text-2xl font-bold text-white">{stats.averageTurnout}%</div>
<div className="text-sm text-gray-400">Avg Turnout</div>
</div>
</div>
</CardContent>
</Card>
</div>
)}
{/* Live Data Indicator */}
<div className="flex items-center gap-2">
<Badge variant="outline" className="bg-green-500/10 border-green-500 text-green-400">
<Activity className="h-3 w-3 mr-1" />
Live Blockchain Data
</Badge>
<span className="text-sm text-gray-500">Block #{currentBlock.toLocaleString()}</span>
</div>
<Tabs defaultValue="elections" className="w-full">
<TabsList className="grid w-full grid-cols-2 bg-gray-800/50">
<TabsTrigger value="elections">Election History</TabsTrigger>
<TabsTrigger value="proposals">Proposal History</TabsTrigger>
</TabsList>
<TabsContent value="elections" className="space-y-4">
{completedElections.length === 0 ? (
<Card className="bg-gray-900/50 border-gray-800">
<CardContent className="pt-6 text-center text-gray-400">
<Trophy className="w-12 h-12 mx-auto mb-4 opacity-50" />
<p>No completed elections in history</p>
<p className="text-sm mt-2">Election results will appear here once voting concludes</p>
</CardContent>
</Card>
) : (
completedElections.map((election) => (
<Card key={election.electionId} className="bg-gray-900/50 border-gray-800">
<CardContent className="p-4">
<div className="flex items-start justify-between mb-4">
<div>
<h4 className="font-medium text-white flex items-center gap-2">
<Trophy className="w-4 h-4 text-yellow-500" />
Election #{election.electionId}
</h4>
<p className="text-sm text-gray-400 mt-1 flex items-center gap-1">
<Calendar className="w-3 h-3" />
Finalized {formatBlockTime(election.finalizedAt)}
</p>
</div>
<Badge className="bg-green-500/20 text-green-400">
Completed
</Badge>
</div>
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="text-gray-400">Winner(s):</span>
<div className="mt-1 space-y-1">
{election.winners.length > 0 ? (
election.winners.map((winner, idx) => (
<Badge key={idx} variant="outline" className="mr-1 text-xs">
{winner.substring(0, 8)}...{winner.slice(-6)}
</Badge>
))
) : (
<span className="text-gray-500">No winners</span>
)}
</div>
</div>
<div className="text-right">
<div className="text-gray-400">Total Votes</div>
<div className="text-white font-medium">{election.totalVotes.toLocaleString()}</div>
</div>
</div>
<div className="mt-4 pt-4 border-t border-gray-700 flex justify-between text-sm">
<div>
<span className="text-gray-400">Turnout: </span>
<span className="text-white">{election.turnoutPercentage}%</span>
</div>
{election.runoffRequired && (
<Badge className="bg-yellow-500/20 text-yellow-400">
Runoff Required
</Badge>
)}
</div>
</CardContent>
</Card>
))
)}
</TabsContent>
<TabsContent value="proposals" className="space-y-4">
{completedProposals.length === 0 ? (
<Card className="bg-gray-900/50 border-gray-800">
<CardContent className="pt-6 text-center text-gray-400">
<FileText className="w-12 h-12 mx-auto mb-4 opacity-50" />
<p>No completed proposals in history</p>
<p className="text-sm mt-2">Proposal outcomes will appear here once voting concludes</p>
</CardContent>
</Card>
) : (
completedProposals.map((proposal) => (
<Card key={proposal.proposalId} className="bg-gray-900/50 border-gray-800">
<CardContent className="p-4">
<div className="flex items-start justify-between">
<div className="flex items-start gap-3">
{getStatusIcon(proposal.status)}
<div>
<h4 className="font-medium text-white">{proposal.title}</h4>
<p className="text-sm text-gray-400 mt-1">
Proposed by {proposal.proposer.substring(0, 8)}...
</p>
<div className="flex items-center gap-4 mt-2 text-sm">
<span className="text-green-400">
<CheckCircle className="w-3 h-3 inline mr-1" />
{proposal.ayeVotes} Aye
</span>
<span className="text-red-400">
<XCircle className="w-3 h-3 inline mr-1" />
{proposal.nayVotes} Nay
</span>
<span className="text-gray-500">
{formatBlockTime(proposal.finalizedAt)}
</span>
</div>
</div>
</div>
<div className="flex flex-col items-end gap-2">
<Badge className={getStatusColor(proposal.status)}>
{proposal.status}
</Badge>
<Badge variant="outline" className="text-xs">
{proposal.decisionType}
</Badge>
</div>
</div>
</CardContent>
</Card>
))
)}
</TabsContent>
</Tabs>
</div>
);
};
export default GovernanceHistory;
+433
View File
@@ -0,0 +1,433 @@
import React, { useState, useEffect } from 'react';
import { Card, CardContent } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Vote, FileText, Users, CheckCircle, XCircle, Clock, Activity, Loader2, Wallet } from 'lucide-react';
import { usePolkadot } from '@/contexts/PolkadotContext';
import { useWallet } from '@/contexts/WalletContext';
import { formatNumber } from '@/lib/utils';
interface ProposalVote {
proposalId: number;
proposalTitle: string;
vote: 'Aye' | 'Nay' | 'Abstain';
conviction: number;
amount: string;
votedAt: number;
status: 'Active' | 'Approved' | 'Rejected' | 'Expired';
}
interface ElectionVote {
electionId: number;
electionType: string;
candidates: string[];
votedAt: number;
status: 'Active' | 'Completed';
}
interface DelegationInfo {
delegateAddress: string;
amount: string;
conviction: number;
tracks: string[];
status: 'active' | 'expired';
}
const MyVotes: React.FC = () => {
const { api, isApiReady } = usePolkadot();
const { account, isConnected } = useWallet();
const [proposalVotes, setProposalVotes] = useState<ProposalVote[]>([]);
const [electionVotes, setElectionVotes] = useState<ElectionVote[]>([]);
const [delegations, setDelegations] = useState<DelegationInfo[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const formatTokenAmount = (amount: string | number) => {
const value = typeof amount === 'string' ? BigInt(amount) : BigInt(amount);
return formatNumber(Number(value) / 1e12, 2);
};
useEffect(() => {
if (!api || !isApiReady || !account) {
setLoading(false);
return;
}
const fetchUserVotes = async () => {
try {
setLoading(true);
setError(null);
const votes: ProposalVote[] = [];
const elections: ElectionVote[] = [];
const userDelegations: DelegationInfo[] = [];
// Fetch democracy votes
if (api.query.democracy?.votingOf) {
const votingInfo = await api.query.democracy.votingOf(account);
const data = votingInfo.toJSON() as Record<string, unknown>;
if (data?.direct?.votes) {
const directVotes = data.direct.votes as Array<[number, unknown]>;
for (const [refIndex, voteData] of directVotes) {
const voteInfo = voteData as Record<string, unknown>;
votes.push({
proposalId: refIndex,
proposalTitle: `Referendum #${refIndex}`,
vote: voteInfo.aye ? 'Aye' : 'Nay',
conviction: (voteInfo.conviction as number) || 0,
amount: (voteInfo.balance as string) || '0',
votedAt: Date.now(),
status: 'Active'
});
}
}
// Check for delegations
if (data?.delegating) {
const delegation = data.delegating as Record<string, unknown>;
userDelegations.push({
delegateAddress: delegation.target as string,
amount: (delegation.balance as string) || '0',
conviction: (delegation.conviction as number) || 0,
tracks: ['All'],
status: 'active'
});
}
}
// Fetch welati election votes
if (api.query.welati?.nextElectionId) {
const nextId = await api.query.welati.nextElectionId();
const currentId = (nextId.toJSON() as number) || 0;
for (let i = 0; i < currentId; i++) {
const vote = await api.query.welati.electionVotes(i, account);
if (vote.isSome) {
const voteData = vote.unwrap().toJSON() as Record<string, unknown>;
const election = await api.query.welati.activeElections(i);
const electionData = election.isSome ? election.unwrap().toJSON() as Record<string, unknown> : null;
elections.push({
electionId: i,
electionType: (electionData?.electionType as string) || 'Unknown',
candidates: (voteData.candidates as string[]) || [],
votedAt: (voteData.votedAt as number) || 0,
status: electionData?.status === 'Completed' ? 'Completed' : 'Active'
});
}
}
}
// Fetch welati proposal votes
if (api.query.welati?.nextProposalId) {
const nextId = await api.query.welati.nextProposalId();
const currentId = (nextId.toJSON() as number) || 0;
for (let i = Math.max(0, currentId - 50); i < currentId; i++) {
const vote = await api.query.welati.collectiveVotes(i, account);
if (vote.isSome) {
const voteData = vote.unwrap().toJSON() as Record<string, unknown>;
const proposal = await api.query.welati.activeProposals(i);
const proposalData = proposal.isSome ? proposal.unwrap().toJSON() as Record<string, unknown> : null;
votes.push({
proposalId: i,
proposalTitle: (proposalData?.title as string) || `Proposal #${i}`,
vote: voteData.vote as 'Aye' | 'Nay' | 'Abstain',
conviction: 1,
amount: '0',
votedAt: (voteData.votedAt as number) || 0,
status: (proposalData?.status as 'Active' | 'Approved' | 'Rejected' | 'Expired') || 'Active'
});
}
}
}
setProposalVotes(votes);
setElectionVotes(elections);
setDelegations(userDelegations);
} catch (err) {
console.error('Error fetching user votes:', err);
setError(err instanceof Error ? err.message : 'Failed to fetch voting history');
} finally {
setLoading(false);
}
};
fetchUserVotes();
const interval = setInterval(fetchUserVotes, 30000);
return () => clearInterval(interval);
}, [api, isApiReady, account]);
const getVoteIcon = (vote: string) => {
switch (vote) {
case 'Aye': return <CheckCircle className="w-4 h-4 text-green-400" />;
case 'Nay': return <XCircle className="w-4 h-4 text-red-400" />;
default: return <Clock className="w-4 h-4 text-gray-400" />;
}
};
const getVoteColor = (vote: string) => {
switch (vote) {
case 'Aye': return 'bg-green-500/20 text-green-400 border-green-500/30';
case 'Nay': return 'bg-red-500/20 text-red-400 border-red-500/30';
default: return 'bg-gray-500/20 text-gray-400 border-gray-500/30';
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'Active': return 'bg-blue-500/20 text-blue-400';
case 'Approved': return 'bg-green-500/20 text-green-400';
case 'Rejected': return 'bg-red-500/20 text-red-400';
case 'Completed': return 'bg-purple-500/20 text-purple-400';
default: return 'bg-gray-500/20 text-gray-400';
}
};
if (!isConnected) {
return (
<Card className="bg-gray-900/50 border-gray-800">
<CardContent className="pt-6">
<div className="text-center py-12">
<Wallet className="w-16 h-16 mx-auto mb-4 text-gray-600" />
<h3 className="text-xl font-semibold text-white mb-2">Connect Your Wallet</h3>
<p className="text-gray-400 mb-6">
Connect your wallet to view your voting history and delegations
</p>
<Button className="bg-green-600 hover:bg-green-700">
Connect Wallet
</Button>
</div>
</CardContent>
</Card>
);
}
if (loading) {
return (
<div className="flex items-center justify-center py-12">
<Loader2 className="w-8 h-8 animate-spin text-green-500" />
<span className="ml-3 text-gray-400">Loading your voting history...</span>
</div>
);
}
if (error) {
return (
<Card className="border-red-500/30 bg-red-500/10">
<CardContent className="pt-6">
<div className="flex items-center text-red-400">
<XCircle className="w-5 h-5 mr-2" />
Error: {error}
</div>
</CardContent>
</Card>
);
}
const totalVotes = proposalVotes.length + electionVotes.length;
const activeVotes = proposalVotes.filter(v => v.status === 'Active').length +
electionVotes.filter(v => v.status === 'Active').length;
return (
<div className="space-y-6">
{/* Stats Overview */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card className="bg-gray-900/50 border-gray-800">
<CardContent className="p-4">
<div className="flex items-center gap-3">
<Vote className="w-8 h-8 text-green-500" />
<div>
<div className="text-2xl font-bold text-white">{totalVotes}</div>
<div className="text-sm text-gray-400">Total Votes</div>
</div>
</div>
</CardContent>
</Card>
<Card className="bg-gray-900/50 border-gray-800">
<CardContent className="p-4">
<div className="flex items-center gap-3">
<Activity className="w-8 h-8 text-blue-500" />
<div>
<div className="text-2xl font-bold text-white">{activeVotes}</div>
<div className="text-sm text-gray-400">Active Votes</div>
</div>
</div>
</CardContent>
</Card>
<Card className="bg-gray-900/50 border-gray-800">
<CardContent className="p-4">
<div className="flex items-center gap-3">
<FileText className="w-8 h-8 text-purple-500" />
<div>
<div className="text-2xl font-bold text-white">{proposalVotes.length}</div>
<div className="text-sm text-gray-400">Proposal Votes</div>
</div>
</div>
</CardContent>
</Card>
<Card className="bg-gray-900/50 border-gray-800">
<CardContent className="p-4">
<div className="flex items-center gap-3">
<Users className="w-8 h-8 text-cyan-500" />
<div>
<div className="text-2xl font-bold text-white">{electionVotes.length}</div>
<div className="text-sm text-gray-400">Election Votes</div>
</div>
</div>
</CardContent>
</Card>
</div>
{/* Live Data Indicator */}
<div className="flex items-center gap-2">
<Badge variant="outline" className="bg-green-500/10 border-green-500 text-green-400">
<Activity className="h-3 w-3 mr-1" />
Live Blockchain Data
</Badge>
<span className="text-sm text-gray-500">
Connected as {account?.substring(0, 8)}...{account?.slice(-6)}
</span>
</div>
<Tabs defaultValue="proposals" className="w-full">
<TabsList className="grid w-full grid-cols-3 bg-gray-800/50">
<TabsTrigger value="proposals">Proposal Votes</TabsTrigger>
<TabsTrigger value="elections">Election Votes</TabsTrigger>
<TabsTrigger value="delegations">My Delegations</TabsTrigger>
</TabsList>
<TabsContent value="proposals" className="space-y-4">
{proposalVotes.length === 0 ? (
<Card className="bg-gray-900/50 border-gray-800">
<CardContent className="pt-6 text-center text-gray-400">
<FileText className="w-12 h-12 mx-auto mb-4 opacity-50" />
<p>You have not voted on any proposals yet</p>
<p className="text-sm mt-2">Check the Proposals tab to participate in governance</p>
</CardContent>
</Card>
) : (
proposalVotes.map((vote) => (
<Card key={vote.proposalId} className="bg-gray-900/50 border-gray-800">
<CardContent className="p-4">
<div className="flex items-start justify-between">
<div className="flex items-start gap-3">
{getVoteIcon(vote.vote)}
<div>
<h4 className="font-medium text-white">{vote.proposalTitle}</h4>
<p className="text-sm text-gray-400 mt-1">
Conviction: {vote.conviction}x
Amount: {formatTokenAmount(vote.amount)} HEZ
</p>
</div>
</div>
<div className="flex flex-col items-end gap-2">
<Badge className={getVoteColor(vote.vote)}>
{vote.vote}
</Badge>
<Badge className={getStatusColor(vote.status)}>
{vote.status}
</Badge>
</div>
</div>
</CardContent>
</Card>
))
)}
</TabsContent>
<TabsContent value="elections" className="space-y-4">
{electionVotes.length === 0 ? (
<Card className="bg-gray-900/50 border-gray-800">
<CardContent className="pt-6 text-center text-gray-400">
<Users className="w-12 h-12 mx-auto mb-4 opacity-50" />
<p>You have not voted in any elections yet</p>
<p className="text-sm mt-2">Check the Elections tab to participate</p>
</CardContent>
</Card>
) : (
electionVotes.map((vote) => (
<Card key={vote.electionId} className="bg-gray-900/50 border-gray-800">
<CardContent className="p-4">
<div className="flex items-start justify-between">
<div>
<h4 className="font-medium text-white">{vote.electionType} Election</h4>
<p className="text-sm text-gray-400 mt-1">
Election #{vote.electionId}
{vote.candidates.length} candidate(s) selected
</p>
<div className="flex flex-wrap gap-2 mt-2">
{vote.candidates.map((candidate, idx) => (
<Badge key={idx} variant="outline" className="text-xs">
{candidate.substring(0, 8)}...
</Badge>
))}
</div>
</div>
<Badge className={getStatusColor(vote.status)}>
{vote.status}
</Badge>
</div>
</CardContent>
</Card>
))
)}
</TabsContent>
<TabsContent value="delegations" className="space-y-4">
{delegations.length === 0 ? (
<Card className="bg-gray-900/50 border-gray-800">
<CardContent className="pt-6 text-center text-gray-400">
<Users className="w-12 h-12 mx-auto mb-4 opacity-50" />
<p>You have not delegated your voting power</p>
<p className="text-sm mt-2">Check the Delegation tab to delegate your votes</p>
</CardContent>
</Card>
) : (
delegations.map((delegation, idx) => (
<Card key={idx} className="bg-gray-900/50 border-gray-800">
<CardContent className="p-4">
<div className="flex items-start justify-between">
<div>
<h4 className="font-medium text-white">
Delegated to {delegation.delegateAddress.substring(0, 8)}...{delegation.delegateAddress.slice(-6)}
</h4>
<p className="text-sm text-gray-400 mt-1">
Amount: {formatTokenAmount(delegation.amount)} HEZ
Conviction: {delegation.conviction}x
</p>
<div className="flex flex-wrap gap-2 mt-2">
{delegation.tracks.map((track, tidx) => (
<Badge key={tidx} variant="secondary" className="text-xs">
{track}
</Badge>
))}
</div>
</div>
<div className="flex flex-col items-end gap-2">
<Badge className={delegation.status === 'active'
? 'bg-green-500/20 text-green-400'
: 'bg-gray-500/20 text-gray-400'}>
{delegation.status}
</Badge>
<Button size="sm" variant="outline" className="text-xs">
Revoke
</Button>
</div>
</div>
</CardContent>
</Card>
))
)}
</TabsContent>
</Tabs>
</div>
);
};
export default MyVotes;