mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-04-22 18:17:58 +00:00
feat(pez-rewards): align frontend with blockchain pallet storage queries
- Fix storage query names: getCurrentEpochInfo, epochStatus, getUserTrustScoreForEpoch, getClaimedReward, getEpochRewardPool - Add recordTrustScore() and claimPezReward() extrinsic functions - Add EpochStatus type and epoch status display (Open/ClaimPeriod/Closed) - Move PezRewardInfo and getPezRewards from staking.ts to scores.ts - Add PEZ Rewards error/success messages to error-handler.ts - Add PEZ Rewards card with Record/Claim to Dashboard, Wallet, and Staking pages - Add recordTrustScore to TransactionHistory tracking
This commit is contained in:
+151
-6
@@ -7,10 +7,10 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { usePezkuwi } from '@/contexts/PezkuwiContext';
|
||||
import { supabase } from '@/lib/supabase';
|
||||
import { User, Mail, Phone, Globe, MapPin, Calendar, Shield, AlertCircle, ArrowLeft, Award, Users, TrendingUp, UserMinus, Play, Loader2 } from 'lucide-react';
|
||||
import { User, Mail, Phone, Globe, MapPin, Calendar, Shield, AlertCircle, ArrowLeft, Award, Users, TrendingUp, UserMinus, Play, Loader2, Coins } from 'lucide-react';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import { fetchUserTikis, getPrimaryRole, getTikiDisplayName, getTikiColor, getTikiEmoji, getUserRoleCategories, getAllTikiNFTDetails, generateCitizenNumber, type TikiNFTDetails } from '@pezkuwi/lib/tiki';
|
||||
import { getAllScores, getStakingScoreStatus, startScoreTracking, type UserScores, type StakingScoreStatus, formatDuration } from '@pezkuwi/lib/scores';
|
||||
import { getAllScores, getStakingScoreStatus, startScoreTracking, getPezRewards, recordTrustScore, claimPezReward, type UserScores, type StakingScoreStatus, type PezRewardInfo, formatDuration } from '@pezkuwi/lib/scores';
|
||||
import { web3FromAddress } from '@pezkuwi/extension-dapp';
|
||||
import { getKycStatus } from '@pezkuwi/lib/kyc';
|
||||
import { ReferralDashboard } from '@/components/referral/ReferralDashboard';
|
||||
@@ -37,6 +37,9 @@ export default function Dashboard() {
|
||||
const [startingScoreTracking, setStartingScoreTracking] = useState(false);
|
||||
const [kycStatus, setKycStatus] = useState<string>('NotStarted');
|
||||
const [renouncingCitizenship, setRenouncingCitizenship] = useState(false);
|
||||
const [pezRewards, setPezRewards] = useState<PezRewardInfo | null>(null);
|
||||
const [isRecordingScore, setIsRecordingScore] = useState(false);
|
||||
const [isClaimingReward, setIsClaimingReward] = useState(false);
|
||||
const [nftDetails, setNftDetails] = useState<{ citizenNFT: TikiNFTDetails | null; roleNFTs: TikiNFTDetails[]; totalNFTs: number }>({
|
||||
citizenNFT: null,
|
||||
roleNFTs: [],
|
||||
@@ -114,8 +117,8 @@ export default function Dashboard() {
|
||||
const allScores = await getAllScores(peopleApi, selectedAccount.address);
|
||||
setScores(allScores);
|
||||
|
||||
// Fetch staking score tracking status
|
||||
const stakingStatusResult = await getStakingScoreStatus(peopleApi, selectedAccount.address);
|
||||
// Fetch staking score tracking status (from Relay Chain where stakingScore pallet lives)
|
||||
const stakingStatusResult = await getStakingScoreStatus(api, selectedAccount.address);
|
||||
setStakingStatus(stakingStatusResult);
|
||||
|
||||
// Fetch tikis from People Chain (tiki pallet is on People Chain)
|
||||
@@ -129,6 +132,10 @@ export default function Dashboard() {
|
||||
// Fetch KYC status from People Chain (identityKyc pallet is on People Chain)
|
||||
const kycStatusResult = await getKycStatus(peopleApi, selectedAccount.address);
|
||||
setKycStatus(kycStatusResult);
|
||||
|
||||
// Fetch PEZ rewards from People Chain
|
||||
const rewards = await getPezRewards(peopleApi, selectedAccount.address);
|
||||
setPezRewards(rewards);
|
||||
} catch (error) {
|
||||
if (import.meta.env.DEV) console.error('Error fetching scores and tikis:', error);
|
||||
} finally {
|
||||
@@ -137,7 +144,7 @@ export default function Dashboard() {
|
||||
}, [selectedAccount, api, peopleApi]);
|
||||
|
||||
const handleStartScoreTracking = async () => {
|
||||
if (!peopleApi || !selectedAccount) {
|
||||
if (!api || !selectedAccount) {
|
||||
toast({
|
||||
title: "Hata",
|
||||
description: "Lütfen önce cüzdanınızı bağlayın",
|
||||
@@ -149,7 +156,9 @@ export default function Dashboard() {
|
||||
setStartingScoreTracking(true);
|
||||
try {
|
||||
const injector = await web3FromAddress(selectedAccount.address);
|
||||
const result = await startScoreTracking(peopleApi, selectedAccount.address, injector.signer);
|
||||
// startScoreTracking must use Relay Chain API (api), not People Chain (peopleApi),
|
||||
// because the stakingScore pallet needs access to staking.ledger on Relay Chain
|
||||
const result = await startScoreTracking(api, selectedAccount.address, injector.signer);
|
||||
|
||||
if (result.success) {
|
||||
toast({
|
||||
@@ -177,6 +186,49 @@ export default function Dashboard() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleRecordTrustScore = async () => {
|
||||
if (!peopleApi || !selectedAccount) return;
|
||||
|
||||
setIsRecordingScore(true);
|
||||
try {
|
||||
const injector = await web3FromAddress(selectedAccount.address);
|
||||
const result = await recordTrustScore(peopleApi, selectedAccount.address, injector.signer);
|
||||
|
||||
if (result.success) {
|
||||
toast({ title: "Success", description: "Trust score recorded for this epoch." });
|
||||
fetchScoresAndTikis();
|
||||
} else {
|
||||
toast({ title: "Error", description: result.error || "Failed to record trust score", variant: "destructive" });
|
||||
}
|
||||
} catch (error) {
|
||||
toast({ title: "Error", description: error instanceof Error ? error.message : "Failed to record trust score", variant: "destructive" });
|
||||
} finally {
|
||||
setIsRecordingScore(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClaimReward = async (epochIndex: number) => {
|
||||
if (!peopleApi || !selectedAccount) return;
|
||||
|
||||
setIsClaimingReward(true);
|
||||
try {
|
||||
const injector = await web3FromAddress(selectedAccount.address);
|
||||
const result = await claimPezReward(peopleApi, selectedAccount.address, epochIndex, injector.signer);
|
||||
|
||||
if (result.success) {
|
||||
const rewardInfo = pezRewards?.claimableRewards.find(r => r.epoch === epochIndex);
|
||||
toast({ title: "Success", description: `${rewardInfo?.amount || '0'} PEZ reward claimed!` });
|
||||
fetchScoresAndTikis();
|
||||
} else {
|
||||
toast({ title: "Error", description: result.error || "Failed to claim reward", variant: "destructive" });
|
||||
}
|
||||
} catch (error) {
|
||||
toast({ title: "Error", description: error instanceof Error ? error.message : "Failed to claim reward", variant: "destructive" });
|
||||
} finally {
|
||||
setIsClaimingReward(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchProfile();
|
||||
if (selectedAccount && api && isApiReady && peopleApi && isPeopleReady) {
|
||||
@@ -529,6 +581,99 @@ export default function Dashboard() {
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* PEZ Rewards Card */}
|
||||
{selectedAccount && (
|
||||
<Card className="mb-6">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">PEZ Rewards</CardTitle>
|
||||
<div className="flex items-center gap-2">
|
||||
{pezRewards && (
|
||||
<Badge className={
|
||||
pezRewards.epochStatus === 'Open'
|
||||
? 'bg-green-500'
|
||||
: pezRewards.epochStatus === 'ClaimPeriod'
|
||||
? 'bg-orange-500'
|
||||
: 'bg-gray-500'
|
||||
}>
|
||||
{pezRewards.epochStatus === 'Open' ? 'Open' : pezRewards.epochStatus === 'ClaimPeriod' ? 'Claim Period' : 'Closed'}
|
||||
</Badge>
|
||||
)}
|
||||
<Coins className="h-4 w-4 text-orange-500" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{loadingScores ? (
|
||||
<div className="text-2xl font-bold">...</div>
|
||||
) : pezRewards ? (
|
||||
<div className="space-y-3">
|
||||
<p className="text-xs text-muted-foreground">Epoch {pezRewards.currentEpoch}</p>
|
||||
|
||||
{/* Open epoch: Record score or show recorded score */}
|
||||
{pezRewards.epochStatus === 'Open' && (
|
||||
pezRewards.hasRecordedThisEpoch ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="text-lg font-bold text-green-600">Score: {pezRewards.userScoreCurrentEpoch}</div>
|
||||
<Badge variant="outline" className="text-green-600 border-green-300">Recorded</Badge>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
size="sm"
|
||||
className="w-full bg-green-600 hover:bg-green-700"
|
||||
onClick={handleRecordTrustScore}
|
||||
disabled={isRecordingScore || loadingScores}
|
||||
>
|
||||
{isRecordingScore ? (
|
||||
<>
|
||||
<Loader2 className="h-3 w-3 mr-1 animate-spin" />
|
||||
Recording...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Play className="h-3 w-3 mr-1" />
|
||||
Record Trust Score
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
)
|
||||
)}
|
||||
|
||||
{/* Claimable rewards */}
|
||||
{pezRewards.hasPendingClaim ? (
|
||||
<div className="space-y-2">
|
||||
<div className="text-2xl font-bold text-orange-600">
|
||||
{parseFloat(pezRewards.totalClaimable).toFixed(2)} PEZ
|
||||
</div>
|
||||
{pezRewards.claimableRewards.map((reward) => (
|
||||
<div key={reward.epoch} className="flex items-center justify-between">
|
||||
<span className="text-xs text-muted-foreground">Epoch {reward.epoch}: {reward.amount} PEZ</span>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleClaimReward(reward.epoch)}
|
||||
disabled={isClaimingReward}
|
||||
className="h-6 text-xs px-2"
|
||||
>
|
||||
{isClaimingReward ? '...' : 'Claim'}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
!pezRewards.hasRecordedThisEpoch && pezRewards.epochStatus !== 'Open' && (
|
||||
<div className="text-2xl font-bold text-muted-foreground">0 PEZ</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-muted-foreground">0 PEZ</div>
|
||||
<p className="text-xs text-muted-foreground">No rewards available</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Tabs defaultValue="profile" className="space-y-4">
|
||||
<TabsList className="flex flex-wrap gap-1">
|
||||
<TabsTrigger value="profile" className="text-xs sm:text-sm px-2 sm:px-3">Profile</TabsTrigger>
|
||||
|
||||
@@ -7,7 +7,10 @@ import { ReceiveModal } from '@/components/ReceiveModal';
|
||||
import { TransactionHistory } from '@/components/TransactionHistory';
|
||||
import { NftList } from '@/components/NftList';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ArrowUpRight, ArrowDownRight, History, ArrowLeft, RefreshCw } from 'lucide-react';
|
||||
import { ArrowUpRight, ArrowDownRight, History, ArrowLeft, RefreshCw, Coins, Loader2 } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { web3FromAddress } from '@pezkuwi/extension-dapp';
|
||||
import { getPezRewards, recordTrustScore, claimPezReward, type PezRewardInfo } from '@pezkuwi/lib/scores';
|
||||
|
||||
interface Transaction {
|
||||
blockNumber: number;
|
||||
@@ -24,12 +27,15 @@ interface Transaction {
|
||||
|
||||
const WalletDashboard: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { api, isApiReady, selectedAccount } = usePezkuwi();
|
||||
const { api, isApiReady, peopleApi, isPeopleReady, selectedAccount } = usePezkuwi();
|
||||
const [isTransferModalOpen, setIsTransferModalOpen] = useState(false);
|
||||
const [isReceiveModalOpen, setIsReceiveModalOpen] = useState(false);
|
||||
const [isHistoryModalOpen, setIsHistoryModalOpen] = useState(false);
|
||||
const [recentTransactions, setRecentTransactions] = useState<Transaction[]>([]);
|
||||
const [isLoadingRecent, setIsLoadingRecent] = useState(false);
|
||||
const [pezRewards, setPezRewards] = useState<PezRewardInfo | null>(null);
|
||||
const [isRecordingScore, setIsRecordingScore] = useState(false);
|
||||
const [isClaimingReward, setIsClaimingReward] = useState(false);
|
||||
|
||||
// Fetch recent transactions (limited to last 10 blocks for performance)
|
||||
const fetchRecentTransactions = async () => {
|
||||
@@ -177,7 +183,7 @@ const WalletDashboard: React.FC = () => {
|
||||
|
||||
// Parse stakingScore & pezRewards
|
||||
else if ((method.section === 'stakingScore' && method.method === 'startTracking') ||
|
||||
(method.section === 'pezRewards' && method.method === 'claimReward')) {
|
||||
(method.section === 'pezRewards' && (method.method === 'claimReward' || method.method === 'recordTrustScore'))) {
|
||||
txList.push({
|
||||
blockNumber,
|
||||
extrinsicIndex: index,
|
||||
@@ -210,6 +216,64 @@ const WalletDashboard: React.FC = () => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedAccount, api, isApiReady]);
|
||||
|
||||
// Fetch PEZ rewards from People Chain
|
||||
useEffect(() => {
|
||||
const fetchPezRewards = async () => {
|
||||
if (!peopleApi || !isPeopleReady || !selectedAccount) return;
|
||||
try {
|
||||
const rewards = await getPezRewards(peopleApi, selectedAccount.address);
|
||||
setPezRewards(rewards);
|
||||
} catch (error) {
|
||||
if (import.meta.env.DEV) console.warn('Failed to fetch PEZ rewards:', error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchPezRewards();
|
||||
const interval = setInterval(fetchPezRewards, 30000);
|
||||
return () => clearInterval(interval);
|
||||
}, [peopleApi, isPeopleReady, selectedAccount]);
|
||||
|
||||
const handleRecordTrustScore = async () => {
|
||||
if (!peopleApi || !selectedAccount) return;
|
||||
setIsRecordingScore(true);
|
||||
try {
|
||||
const injector = await web3FromAddress(selectedAccount.address);
|
||||
const result = await recordTrustScore(peopleApi, selectedAccount.address, injector.signer);
|
||||
if (result.success) {
|
||||
toast.success('Trust score recorded for this epoch');
|
||||
const rewards = await getPezRewards(peopleApi, selectedAccount.address);
|
||||
setPezRewards(rewards);
|
||||
} else {
|
||||
toast.error(result.error || 'Failed to record trust score');
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : 'Failed to record trust score');
|
||||
} finally {
|
||||
setIsRecordingScore(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClaimReward = async (epochIndex: number) => {
|
||||
if (!peopleApi || !selectedAccount) return;
|
||||
setIsClaimingReward(true);
|
||||
try {
|
||||
const injector = await web3FromAddress(selectedAccount.address);
|
||||
const result = await claimPezReward(peopleApi, selectedAccount.address, epochIndex, injector.signer);
|
||||
if (result.success) {
|
||||
const rewardInfo = pezRewards?.claimableRewards.find(r => r.epoch === epochIndex);
|
||||
toast.success(`${rewardInfo?.amount || '0'} PEZ reward claimed!`);
|
||||
const rewards = await getPezRewards(peopleApi, selectedAccount.address);
|
||||
setPezRewards(rewards);
|
||||
} else {
|
||||
toast.error(result.error || 'Failed to claim reward');
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : 'Failed to claim reward');
|
||||
} finally {
|
||||
setIsClaimingReward(false);
|
||||
}
|
||||
};
|
||||
|
||||
const formatAmount = (amount: string, decimals: number = 12) => {
|
||||
const value = parseInt(amount) / Math.pow(10, decimals);
|
||||
return value.toFixed(4);
|
||||
@@ -355,6 +419,78 @@ const WalletDashboard: React.FC = () => {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* PEZ Rewards Card */}
|
||||
{pezRewards && (
|
||||
<div className="bg-gray-900 border border-gray-800 rounded-lg p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Coins className="w-5 h-5 text-orange-500" />
|
||||
<h3 className="text-lg font-semibold text-white">PEZ Rewards</h3>
|
||||
</div>
|
||||
<span className={`text-xs px-2 py-0.5 rounded-full font-medium ${
|
||||
pezRewards.epochStatus === 'Open'
|
||||
? 'bg-green-500/20 text-green-400'
|
||||
: pezRewards.epochStatus === 'ClaimPeriod'
|
||||
? 'bg-orange-500/20 text-orange-400'
|
||||
: 'bg-gray-500/20 text-gray-400'
|
||||
}`}>
|
||||
Epoch {pezRewards.currentEpoch} - {pezRewards.epochStatus === 'Open' ? 'Open' : pezRewards.epochStatus === 'ClaimPeriod' ? 'Claim Period' : 'Closed'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Open epoch: Record score */}
|
||||
{pezRewards.epochStatus === 'Open' && (
|
||||
pezRewards.hasRecordedThisEpoch ? (
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<span className="text-green-400 font-semibold">Score: {pezRewards.userScoreCurrentEpoch}</span>
|
||||
<span className="text-xs text-gray-500">Recorded for this epoch</span>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleRecordTrustScore}
|
||||
disabled={isRecordingScore}
|
||||
className="w-full mb-3 bg-green-600 hover:bg-green-700"
|
||||
>
|
||||
{isRecordingScore ? (
|
||||
<>
|
||||
<Loader2 className="w-3 h-3 mr-1 animate-spin" />
|
||||
Recording...
|
||||
</>
|
||||
) : 'Record Trust Score'}
|
||||
</Button>
|
||||
)
|
||||
)}
|
||||
|
||||
{/* Claimable rewards */}
|
||||
{pezRewards.hasPendingClaim ? (
|
||||
<div className="space-y-2">
|
||||
<div className="text-2xl font-bold text-orange-500">
|
||||
{parseFloat(pezRewards.totalClaimable).toFixed(2)} PEZ
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 mb-2">{pezRewards.claimableRewards.length} epoch(s) to claim</p>
|
||||
{pezRewards.claimableRewards.map((reward) => (
|
||||
<div key={reward.epoch} className="flex items-center justify-between bg-gray-800/50 rounded-lg px-3 py-2">
|
||||
<span className="text-xs text-gray-400">Epoch {reward.epoch}: {reward.amount} PEZ</span>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => handleClaimReward(reward.epoch)}
|
||||
disabled={isClaimingReward}
|
||||
className="h-6 text-xs px-3 bg-orange-600 hover:bg-orange-700"
|
||||
>
|
||||
{isClaimingReward ? '...' : 'Claim'}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
!pezRewards.hasRecordedThisEpoch && pezRewards.epochStatus !== 'Open' && (
|
||||
<div className="text-gray-500 text-sm">No claimable rewards</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Token Balances */}
|
||||
<AccountBalance />
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user