mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-06-12 19:01:03 +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:
@@ -38,6 +38,28 @@ const ERROR_MESSAGES: Record<string, ErrorMessage> = {
|
|||||||
kmr: 'Zêde chunk unbonding hene. Ji kerema xwe li çavkaniyên berê bisekine.',
|
kmr: 'Zêde chunk unbonding hene. Ji kerema xwe li çavkaniyên berê bisekine.',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// PEZ Rewards errors
|
||||||
|
'pezRewards.ClaimPeriodExpired': {
|
||||||
|
en: 'The claim period for this epoch has expired. Rewards can no longer be claimed.',
|
||||||
|
kmr: 'Dema daxwazkirinê ji bo vê epoch-ê qediya. Xelat êdî nayên wergirtin.',
|
||||||
|
},
|
||||||
|
'pezRewards.RewardAlreadyClaimed': {
|
||||||
|
en: 'You have already claimed your reward for this epoch.',
|
||||||
|
kmr: 'We berê xelata xwe ji bo vê epoch-ê wergirtiye.',
|
||||||
|
},
|
||||||
|
'pezRewards.NoTrustScoreForEpoch': {
|
||||||
|
en: 'No trust score recorded for this epoch. You must record your score before claiming.',
|
||||||
|
kmr: 'Ji bo vê epoch-ê skora emîniyê tomar nebûye. Pêşî divê skora xwe tomar bikî.',
|
||||||
|
},
|
||||||
|
'pezRewards.NoRewardToClaim': {
|
||||||
|
en: 'No reward available to claim for this epoch.',
|
||||||
|
kmr: 'Ji bo vê epoch-ê xelateke ku were wergirtin tune ye.',
|
||||||
|
},
|
||||||
|
'pezRewards.EpochAlreadyClosed': {
|
||||||
|
en: 'This epoch is already closed. No further actions can be taken.',
|
||||||
|
kmr: 'Ev epoch berê girtî ye. Tu çalakî êdî nayê kirin.',
|
||||||
|
},
|
||||||
|
|
||||||
// Identity KYC errors
|
// Identity KYC errors
|
||||||
'identityKyc.AlreadyApplied': {
|
'identityKyc.AlreadyApplied': {
|
||||||
en: 'You already have a pending citizenship application. Please wait for approval.',
|
en: 'You already have a pending citizenship application. Please wait for approval.',
|
||||||
@@ -430,6 +452,16 @@ export const SUCCESS_MESSAGES: Record<string, SuccessMessage> = {
|
|||||||
kmr: 'Şopa staking dest pê kir! Xala we dê bi demê re kom bibe.',
|
kmr: 'Şopa staking dest pê kir! Xala we dê bi demê re kom bibe.',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// PEZ Rewards
|
||||||
|
'pezRewards.recorded': {
|
||||||
|
en: 'Trust score recorded for this epoch. Your score will be used for reward calculation.',
|
||||||
|
kmr: 'Skora emîniyê ji bo vê epoch-ê tomar bû. Skora we dê ji bo hesabkirina xelatê were bikaranîn.',
|
||||||
|
},
|
||||||
|
'pezRewards.claimed': {
|
||||||
|
en: '{{amount}} PEZ reward claimed successfully!',
|
||||||
|
kmr: '{{amount}} PEZ xelat bi serkeftî hate wergirtin!',
|
||||||
|
},
|
||||||
|
|
||||||
// Citizenship
|
// Citizenship
|
||||||
'citizenship.applied': {
|
'citizenship.applied': {
|
||||||
en: 'Citizenship application submitted successfully! We will review your application.',
|
en: 'Citizenship application submitted successfully! We will review your application.',
|
||||||
|
|||||||
+236
-15
@@ -1,13 +1,14 @@
|
|||||||
// ========================================
|
// ========================================
|
||||||
// Score Systems Integration
|
// Score Systems Integration
|
||||||
// ========================================
|
// ========================================
|
||||||
// All scores come from People Chain (people-rpc.pezkuwichain.io)
|
// Score pallets are distributed across chains:
|
||||||
// - Trust Score: pezpallet-trust
|
// - Trust Score: pezpallet-trust (People Chain)
|
||||||
// - Referral Score: pezpallet-referral
|
// - Referral Score: pezpallet-referral (People Chain)
|
||||||
// - Staking Score: pezpallet-staking-score
|
// - Staking Score: pezpallet-staking-score (Relay Chain - needs staking.ledger access)
|
||||||
// - Tiki Score: pezpallet-tiki
|
// - Tiki Score: pezpallet-tiki (People Chain)
|
||||||
|
|
||||||
import type { ApiPromise } from '@pezkuwi/api';
|
import type { ApiPromise } from '@pezkuwi/api';
|
||||||
|
import { formatBalance } from './wallet';
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
// TYPE DEFINITIONS
|
// TYPE DEFINITIONS
|
||||||
@@ -28,6 +29,26 @@ export interface StakingScoreStatus {
|
|||||||
durationBlocks: number;
|
durationBlocks: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type EpochStatus = 'Open' | 'ClaimPeriod' | 'Closed';
|
||||||
|
|
||||||
|
export interface EpochRewardPool {
|
||||||
|
totalRewardPool: string;
|
||||||
|
totalTrustScore: number;
|
||||||
|
participantsCount: number;
|
||||||
|
rewardPerTrustPoint: string;
|
||||||
|
claimDeadline: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PezRewardInfo {
|
||||||
|
currentEpoch: number;
|
||||||
|
epochStatus: EpochStatus;
|
||||||
|
hasRecordedThisEpoch: boolean;
|
||||||
|
userScoreCurrentEpoch: number;
|
||||||
|
claimableRewards: { epoch: number; amount: string }[];
|
||||||
|
totalClaimable: string;
|
||||||
|
hasPendingClaim: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
// TRUST SCORE (pezpallet-trust on People Chain)
|
// TRUST SCORE (pezpallet-trust on People Chain)
|
||||||
// ========================================
|
// ========================================
|
||||||
@@ -115,24 +136,27 @@ export async function getReferralCount(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
// STAKING SCORE (pezpallet-staking-score on People Chain)
|
// STAKING SCORE (pezpallet-staking-score on Relay Chain)
|
||||||
// ========================================
|
// ========================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check staking score tracking status
|
* Check staking score tracking status
|
||||||
* Storage: stakingScore.stakingStartBlock(address)
|
* Storage: stakingScore.stakingStartBlock(address)
|
||||||
|
*
|
||||||
|
* IMPORTANT: stakingScore pallet is on the Relay Chain (not People Chain),
|
||||||
|
* because it needs access to staking.ledger for score calculation.
|
||||||
*/
|
*/
|
||||||
export async function getStakingScoreStatus(
|
export async function getStakingScoreStatus(
|
||||||
peopleApi: ApiPromise,
|
relayApi: ApiPromise,
|
||||||
address: string
|
address: string
|
||||||
): Promise<StakingScoreStatus> {
|
): Promise<StakingScoreStatus> {
|
||||||
try {
|
try {
|
||||||
if (!peopleApi?.query?.stakingScore?.stakingStartBlock) {
|
if (!relayApi?.query?.stakingScore?.stakingStartBlock) {
|
||||||
return { isTracking: false, startBlock: null, currentBlock: 0, durationBlocks: 0 };
|
return { isTracking: false, startBlock: null, currentBlock: 0, durationBlocks: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
const startBlockResult = await peopleApi.query.stakingScore.stakingStartBlock(address);
|
const startBlockResult = await relayApi.query.stakingScore.stakingStartBlock(address);
|
||||||
const currentBlock = Number((await peopleApi.query.system.number()).toString());
|
const currentBlock = Number((await relayApi.query.system.number()).toString());
|
||||||
|
|
||||||
if (startBlockResult.isEmpty || startBlockResult.isNone) {
|
if (startBlockResult.isEmpty || startBlockResult.isNone) {
|
||||||
return { isTracking: false, startBlock: null, currentBlock, durationBlocks: 0 };
|
return { isTracking: false, startBlock: null, currentBlock, durationBlocks: 0 };
|
||||||
@@ -156,25 +180,29 @@ export async function getStakingScoreStatus(
|
|||||||
/**
|
/**
|
||||||
* Start staking score tracking
|
* Start staking score tracking
|
||||||
* Calls: stakingScore.startScoreTracking()
|
* Calls: stakingScore.startScoreTracking()
|
||||||
|
*
|
||||||
|
* IMPORTANT: This must be called on the Relay Chain API (not People Chain),
|
||||||
|
* because the stakingScore pallet needs access to staking.ledger to verify
|
||||||
|
* the user has an active stake. The staking pallet only exists on Relay Chain.
|
||||||
*/
|
*/
|
||||||
export async function startScoreTracking(
|
export async function startScoreTracking(
|
||||||
peopleApi: ApiPromise,
|
relayApi: ApiPromise,
|
||||||
address: string,
|
address: string,
|
||||||
signer: any
|
signer: any
|
||||||
): Promise<{ success: boolean; error?: string }> {
|
): Promise<{ success: boolean; error?: string }> {
|
||||||
try {
|
try {
|
||||||
if (!peopleApi?.tx?.stakingScore?.startScoreTracking) {
|
if (!relayApi?.tx?.stakingScore?.startScoreTracking) {
|
||||||
return { success: false, error: 'stakingScore pallet not available' };
|
return { success: false, error: 'stakingScore pallet not available on this chain' };
|
||||||
}
|
}
|
||||||
|
|
||||||
const tx = peopleApi.tx.stakingScore.startScoreTracking();
|
const tx = relayApi.tx.stakingScore.startScoreTracking();
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
tx.signAndSend(address, { signer }, ({ status, dispatchError }) => {
|
tx.signAndSend(address, { signer }, ({ status, dispatchError }) => {
|
||||||
if (status.isInBlock || status.isFinalized) {
|
if (status.isInBlock || status.isFinalized) {
|
||||||
if (dispatchError) {
|
if (dispatchError) {
|
||||||
if (dispatchError.isModule) {
|
if (dispatchError.isModule) {
|
||||||
const decoded = peopleApi.registry.findMetaError(dispatchError.asModule);
|
const decoded = relayApi.registry.findMetaError(dispatchError.asModule);
|
||||||
resolve({ success: false, error: `${decoded.section}.${decoded.name}: ${decoded.docs.join(' ')}` });
|
resolve({ success: false, error: `${decoded.section}.${decoded.name}: ${decoded.docs.join(' ')}` });
|
||||||
} else {
|
} else {
|
||||||
resolve({ success: false, error: dispatchError.toString() });
|
resolve({ success: false, error: dispatchError.toString() });
|
||||||
@@ -360,3 +388,196 @@ export async function getPerwerdeScore(
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// PEZ REWARDS (pezRewards pallet on People Chain)
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get PEZ rewards information for an account
|
||||||
|
* Uses correct storage query names from pezRewards pallet:
|
||||||
|
* - getCurrentEpochInfo() → epoch info
|
||||||
|
* - epochStatus(epoch) → Open | ClaimPeriod | Closed
|
||||||
|
* - getUserTrustScoreForEpoch(epoch, addr) → user's recorded score
|
||||||
|
* - getClaimedReward(epoch, addr) → claimed reward amount
|
||||||
|
* - getEpochRewardPool(epoch) → reward pool info
|
||||||
|
*/
|
||||||
|
export async function getPezRewards(
|
||||||
|
peopleApi: ApiPromise,
|
||||||
|
address: string
|
||||||
|
): Promise<PezRewardInfo | null> {
|
||||||
|
try {
|
||||||
|
if (!peopleApi?.query?.pezRewards?.getCurrentEpochInfo) {
|
||||||
|
console.warn('PezRewards pallet not available on People Chain');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current epoch info
|
||||||
|
const epochInfoResult = await peopleApi.query.pezRewards.getCurrentEpochInfo();
|
||||||
|
if (!epochInfoResult) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const epochInfo = epochInfoResult.toJSON() as any;
|
||||||
|
const currentEpoch: number = epochInfo.currentEpoch ?? epochInfo.current_epoch ?? 0;
|
||||||
|
|
||||||
|
// Get current epoch status
|
||||||
|
let epochStatus: EpochStatus = 'Open';
|
||||||
|
try {
|
||||||
|
const statusResult = await peopleApi.query.pezRewards.epochStatus(currentEpoch);
|
||||||
|
const statusStr = statusResult.toString();
|
||||||
|
if (statusStr === 'ClaimPeriod') epochStatus = 'ClaimPeriod';
|
||||||
|
else if (statusStr === 'Closed') epochStatus = 'Closed';
|
||||||
|
else epochStatus = 'Open';
|
||||||
|
} catch {
|
||||||
|
// Default to Open if query fails
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user has recorded their score this epoch
|
||||||
|
let hasRecordedThisEpoch = false;
|
||||||
|
let userScoreCurrentEpoch = 0;
|
||||||
|
try {
|
||||||
|
const userScoreResult = await peopleApi.query.pezRewards.getUserTrustScoreForEpoch(currentEpoch, address);
|
||||||
|
if (userScoreResult.isSome) {
|
||||||
|
hasRecordedThisEpoch = true;
|
||||||
|
const scoreCodec = userScoreResult.unwrap() as { toString: () => string };
|
||||||
|
userScoreCurrentEpoch = Number(scoreCodec.toString());
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// User hasn't recorded
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for claimable rewards from completed epochs
|
||||||
|
const claimableRewards: { epoch: number; amount: string }[] = [];
|
||||||
|
let totalClaimable = BigInt(0);
|
||||||
|
|
||||||
|
for (let i = Math.max(0, currentEpoch - 3); i < currentEpoch; i++) {
|
||||||
|
try {
|
||||||
|
// Check epoch status - only ClaimPeriod epochs are claimable
|
||||||
|
const pastStatusResult = await peopleApi.query.pezRewards.epochStatus(i);
|
||||||
|
const pastStatus = pastStatusResult.toString();
|
||||||
|
if (pastStatus !== 'ClaimPeriod') continue;
|
||||||
|
|
||||||
|
// Check if user already claimed
|
||||||
|
const claimedResult = await peopleApi.query.pezRewards.getClaimedReward(i, address);
|
||||||
|
if (claimedResult.isSome) continue;
|
||||||
|
|
||||||
|
// Check if user has a score for this epoch
|
||||||
|
const userScoreResult = await peopleApi.query.pezRewards.getUserTrustScoreForEpoch(i, address);
|
||||||
|
if (!userScoreResult.isSome) continue;
|
||||||
|
|
||||||
|
// Get epoch reward pool
|
||||||
|
const epochPoolResult = await peopleApi.query.pezRewards.getEpochRewardPool(i);
|
||||||
|
if (!epochPoolResult.isSome) continue;
|
||||||
|
|
||||||
|
const epochPoolCodec = epochPoolResult.unwrap() as { toJSON: () => unknown };
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const epochPool = epochPoolCodec.toJSON() as any;
|
||||||
|
const userScoreCodec = userScoreResult.unwrap() as { toString: () => string };
|
||||||
|
const userScore = BigInt(userScoreCodec.toString());
|
||||||
|
const rewardPerPoint = BigInt(epochPool.rewardPerTrustPoint || epochPool.reward_per_trust_point || '0');
|
||||||
|
|
||||||
|
const rewardAmount = userScore * rewardPerPoint;
|
||||||
|
const rewardFormatted = formatBalance(rewardAmount.toString());
|
||||||
|
|
||||||
|
if (parseFloat(rewardFormatted) > 0) {
|
||||||
|
claimableRewards.push({ epoch: i, amount: rewardFormatted });
|
||||||
|
totalClaimable += rewardAmount;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`Error checking epoch ${i} rewards:`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
currentEpoch,
|
||||||
|
epochStatus,
|
||||||
|
hasRecordedThisEpoch,
|
||||||
|
userScoreCurrentEpoch,
|
||||||
|
claimableRewards,
|
||||||
|
totalClaimable: formatBalance(totalClaimable.toString()),
|
||||||
|
hasPendingClaim: claimableRewards.length > 0,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('PEZ rewards not available:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Record trust score for the current epoch
|
||||||
|
* Calls: pezRewards.recordTrustScore()
|
||||||
|
*/
|
||||||
|
export async function recordTrustScore(
|
||||||
|
peopleApi: ApiPromise,
|
||||||
|
address: string,
|
||||||
|
signer: any
|
||||||
|
): Promise<{ success: boolean; error?: string }> {
|
||||||
|
try {
|
||||||
|
if (!peopleApi?.tx?.pezRewards?.recordTrustScore) {
|
||||||
|
return { success: false, error: 'pezRewards pallet not available' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const tx = peopleApi.tx.pezRewards.recordTrustScore();
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
tx.signAndSend(address, { signer }, ({ status, dispatchError }: any) => {
|
||||||
|
if (status.isInBlock || status.isFinalized) {
|
||||||
|
if (dispatchError) {
|
||||||
|
if (dispatchError.isModule) {
|
||||||
|
const decoded = peopleApi.registry.findMetaError(dispatchError.asModule);
|
||||||
|
resolve({ success: false, error: `${decoded.section}.${decoded.name}` });
|
||||||
|
} else {
|
||||||
|
resolve({ success: false, error: dispatchError.toString() });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resolve({ success: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error recording trust score:', error);
|
||||||
|
return { success: false, error: error instanceof Error ? error.message : 'Unknown error' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Claim PEZ reward for a specific epoch
|
||||||
|
* Calls: pezRewards.claimReward(epochIndex)
|
||||||
|
*/
|
||||||
|
export async function claimPezReward(
|
||||||
|
peopleApi: ApiPromise,
|
||||||
|
address: string,
|
||||||
|
epochIndex: number,
|
||||||
|
signer: any
|
||||||
|
): Promise<{ success: boolean; error?: string }> {
|
||||||
|
try {
|
||||||
|
if (!peopleApi?.tx?.pezRewards?.claimReward) {
|
||||||
|
return { success: false, error: 'pezRewards pallet not available' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const tx = peopleApi.tx.pezRewards.claimReward(epochIndex);
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
tx.signAndSend(address, { signer }, ({ status, dispatchError }: any) => {
|
||||||
|
if (status.isInBlock || status.isFinalized) {
|
||||||
|
if (dispatchError) {
|
||||||
|
if (dispatchError.isModule) {
|
||||||
|
const decoded = peopleApi.registry.findMetaError(dispatchError.asModule);
|
||||||
|
resolve({ success: false, error: `${decoded.section}.${decoded.name}` });
|
||||||
|
} else {
|
||||||
|
resolve({ success: false, error: dispatchError.toString() });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resolve({ success: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error claiming PEZ reward:', error);
|
||||||
|
return { success: false, error: error instanceof Error ? error.message : 'Unknown error' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+5
-97
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
import { ApiPromise } from '@pezkuwi/api';
|
import { ApiPromise } from '@pezkuwi/api';
|
||||||
import { formatBalance } from './wallet';
|
import { formatBalance } from './wallet';
|
||||||
|
import { getPezRewards, type PezRewardInfo } from './scores';
|
||||||
|
|
||||||
export interface StakingLedger {
|
export interface StakingLedger {
|
||||||
stash: string;
|
stash: string;
|
||||||
@@ -30,14 +31,6 @@ export interface EraRewardPoints {
|
|||||||
individual: Record<string, number>;
|
individual: Record<string, number>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PezRewardInfo {
|
|
||||||
currentEpoch: number;
|
|
||||||
epochStartBlock: number;
|
|
||||||
claimableRewards: { epoch: number; amount: string }[]; // Unclaimed rewards from completed epochs
|
|
||||||
totalClaimable: string;
|
|
||||||
hasPendingClaim: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface StakingInfo {
|
export interface StakingInfo {
|
||||||
bonded: string;
|
bonded: string;
|
||||||
active: string;
|
active: string;
|
||||||
@@ -187,90 +180,6 @@ export async function getBlocksUntilEra(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get PEZ rewards information for an account
|
|
||||||
* Note: pezRewards pallet is on People Chain, not Relay Chain
|
|
||||||
*/
|
|
||||||
export async function getPezRewards(
|
|
||||||
peopleApi: ApiPromise,
|
|
||||||
address: string
|
|
||||||
): Promise<PezRewardInfo | null> {
|
|
||||||
try {
|
|
||||||
// Check if pezRewards pallet exists on People Chain
|
|
||||||
if (!peopleApi?.query?.pezRewards || !peopleApi.query.pezRewards.epochInfo) {
|
|
||||||
console.warn('PezRewards pallet not available on People Chain');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get current epoch info
|
|
||||||
const epochInfoResult = await peopleApi.query.pezRewards.epochInfo();
|
|
||||||
|
|
||||||
if (!epochInfoResult) {
|
|
||||||
console.warn('No epoch info found');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const epochInfo = epochInfoResult.toJSON() as any;
|
|
||||||
const currentEpoch = epochInfo.currentEpoch || 0;
|
|
||||||
const epochStartBlock = epochInfo.epochStartBlock || 0;
|
|
||||||
|
|
||||||
// Check for claimable rewards from completed epochs
|
|
||||||
const claimableRewards: { epoch: number; amount: string }[] = [];
|
|
||||||
let totalClaimable = BigInt(0);
|
|
||||||
|
|
||||||
// Check last 3 completed epochs for unclaimed rewards
|
|
||||||
for (let i = Math.max(0, currentEpoch - 3); i < currentEpoch; i++) {
|
|
||||||
try {
|
|
||||||
// Check if user has claimed this epoch already
|
|
||||||
const claimedResult = await peopleApi.query.pezRewards.claimedRewards(i, address);
|
|
||||||
|
|
||||||
if (claimedResult.isNone) {
|
|
||||||
// User hasn't claimed - check if they have rewards
|
|
||||||
const userScoreResult = await peopleApi.query.pezRewards.userEpochScores(i, address);
|
|
||||||
|
|
||||||
if (userScoreResult.isSome) {
|
|
||||||
// User has a score for this epoch - calculate their reward
|
|
||||||
const epochPoolResult = await peopleApi.query.pezRewards.epochRewardPools(i);
|
|
||||||
|
|
||||||
if (epochPoolResult.isSome) {
|
|
||||||
const epochPoolCodec = epochPoolResult.unwrap() as { toJSON: () => unknown };
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const epochPool = epochPoolCodec.toJSON() as any;
|
|
||||||
const userScoreCodec = userScoreResult.unwrap() as { toString: () => string };
|
|
||||||
const userScore = BigInt(userScoreCodec.toString());
|
|
||||||
const rewardPerPoint = BigInt(epochPool.rewardPerTrustPoint || '0');
|
|
||||||
|
|
||||||
const rewardAmount = userScore * rewardPerPoint;
|
|
||||||
const rewardFormatted = formatBalance(rewardAmount.toString());
|
|
||||||
|
|
||||||
if (parseFloat(rewardFormatted) > 0) {
|
|
||||||
claimableRewards.push({
|
|
||||||
epoch: i,
|
|
||||||
amount: rewardFormatted
|
|
||||||
});
|
|
||||||
totalClaimable += rewardAmount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.warn(`Error checking epoch ${i} rewards:`, err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
currentEpoch,
|
|
||||||
epochStartBlock,
|
|
||||||
claimableRewards,
|
|
||||||
totalClaimable: formatBalance(totalClaimable.toString()),
|
|
||||||
hasPendingClaim: claimableRewards.length > 0
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('PEZ rewards not available:', error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get comprehensive staking info for an account
|
* Get comprehensive staking info for an account
|
||||||
* @param api - Relay Chain API (for staking pallet)
|
* @param api - Relay Chain API (for staking pallet)
|
||||||
@@ -329,17 +238,16 @@ export async function getStakingInfo(
|
|||||||
let hasStartedScoreTracking = false;
|
let hasStartedScoreTracking = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// stakingScore pallet is on People Chain
|
// stakingScore pallet is on Relay Chain (same as staking pallet, needs staking.ledger access)
|
||||||
const scoreApi = peopleApi || api;
|
if (api.query.stakingScore && api.query.stakingScore.stakingStartBlock) {
|
||||||
if (scoreApi.query.stakingScore && scoreApi.query.stakingScore.stakingStartBlock) {
|
|
||||||
// Check if user has started score tracking
|
// Check if user has started score tracking
|
||||||
const scoreResult = await scoreApi.query.stakingScore.stakingStartBlock(address);
|
const scoreResult = await api.query.stakingScore.stakingStartBlock(address);
|
||||||
|
|
||||||
if (scoreResult.isSome) {
|
if (scoreResult.isSome) {
|
||||||
hasStartedScoreTracking = true;
|
hasStartedScoreTracking = true;
|
||||||
const startBlockCodec = scoreResult.unwrap() as { toString: () => string };
|
const startBlockCodec = scoreResult.unwrap() as { toString: () => string };
|
||||||
const startBlock = Number(startBlockCodec.toString());
|
const startBlock = Number(startBlockCodec.toString());
|
||||||
const currentBlock = Number((await scoreApi.query.system.number()).toString());
|
const currentBlock = Number((await api.query.system.number()).toString());
|
||||||
const durationInBlocks = currentBlock - startBlock;
|
const durationInBlocks = currentBlock - startBlock;
|
||||||
stakingDuration = durationInBlocks;
|
stakingDuration = durationInBlocks;
|
||||||
|
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ export const TransactionHistory: React.FC<TransactionHistoryProps> = ({ isOpen,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Parse pezRewards operations
|
// Parse pezRewards operations
|
||||||
else if (method.section === 'pezRewards' && method.method === 'claimReward') {
|
else if (method.section === 'pezRewards' && (method.method === 'claimReward' || method.method === 'recordTrustScore')) {
|
||||||
txList.push({
|
txList.push({
|
||||||
blockNumber,
|
blockNumber,
|
||||||
extrinsicIndex: index,
|
extrinsicIndex: index,
|
||||||
|
|||||||
@@ -20,6 +20,12 @@ import {
|
|||||||
parseAmount,
|
parseAmount,
|
||||||
type StakingInfo
|
type StakingInfo
|
||||||
} from '@pezkuwi/lib/staking';
|
} from '@pezkuwi/lib/staking';
|
||||||
|
import {
|
||||||
|
recordTrustScore,
|
||||||
|
claimPezReward,
|
||||||
|
getPezRewards,
|
||||||
|
type PezRewardInfo
|
||||||
|
} from '@pezkuwi/lib/scores';
|
||||||
import { LoadingState } from '@pezkuwi/components/AsyncComponent';
|
import { LoadingState } from '@pezkuwi/components/AsyncComponent';
|
||||||
import { ValidatorPoolDashboard } from './ValidatorPoolDashboard';
|
import { ValidatorPoolDashboard } from './ValidatorPoolDashboard';
|
||||||
import { handleBlockchainError, handleBlockchainSuccess } from '@pezkuwi/lib/error-handler';
|
import { handleBlockchainError, handleBlockchainSuccess } from '@pezkuwi/lib/error-handler';
|
||||||
@@ -39,6 +45,9 @@ export const StakingDashboard: React.FC = () => {
|
|||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isLoadingData, setIsLoadingData] = useState(false);
|
const [isLoadingData, setIsLoadingData] = useState(false);
|
||||||
|
const [pezRewards, setPezRewards] = useState<PezRewardInfo | null>(null);
|
||||||
|
const [isRecordingScore, setIsRecordingScore] = useState(false);
|
||||||
|
const [isClaimingReward, setIsClaimingReward] = useState(false);
|
||||||
|
|
||||||
// Fetch staking data
|
// Fetch staking data
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -81,6 +90,82 @@ export const StakingDashboard: React.FC = () => {
|
|||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [api, peopleApi, isApiReady, isPeopleReady, selectedAccount]);
|
}, [api, peopleApi, isApiReady, isPeopleReady, selectedAccount]);
|
||||||
|
|
||||||
|
// Fetch PEZ rewards data separately 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) {
|
||||||
|
handleBlockchainSuccess('pezRewards.recorded', toast);
|
||||||
|
// Refresh PEZ rewards data
|
||||||
|
setTimeout(async () => {
|
||||||
|
if (peopleApi && selectedAccount) {
|
||||||
|
const rewards = await getPezRewards(peopleApi, selectedAccount.address);
|
||||||
|
setPezRewards(rewards);
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
} else {
|
||||||
|
toast.error(result.error || 'Failed to record trust score');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (import.meta.env.DEV) console.error('Record trust score failed:', 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);
|
||||||
|
handleBlockchainSuccess('pezRewards.claimed', toast, { amount: rewardInfo?.amount || '0' });
|
||||||
|
refreshBalances();
|
||||||
|
// Refresh PEZ rewards data
|
||||||
|
setTimeout(async () => {
|
||||||
|
if (peopleApi && selectedAccount) {
|
||||||
|
const rewards = await getPezRewards(peopleApi, selectedAccount.address);
|
||||||
|
setPezRewards(rewards);
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
} else {
|
||||||
|
toast.error(result.error || 'Failed to claim reward');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (import.meta.env.DEV) console.error('Claim reward failed:', error);
|
||||||
|
toast.error(error instanceof Error ? error.message : 'Failed to claim reward');
|
||||||
|
} finally {
|
||||||
|
setIsClaimingReward(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleBond = async () => {
|
const handleBond = async () => {
|
||||||
if (!api || !selectedAccount || !bondAmount) return;
|
if (!api || !selectedAccount || !bondAmount) return;
|
||||||
|
|
||||||
@@ -425,36 +510,80 @@ export const StakingDashboard: React.FC = () => {
|
|||||||
|
|
||||||
<Card className="bg-gray-900 border-gray-800">
|
<Card className="bg-gray-900 border-gray-800">
|
||||||
<CardHeader className="pb-3">
|
<CardHeader className="pb-3">
|
||||||
<CardTitle className="text-sm text-gray-400">PEZ Rewards</CardTitle>
|
<div className="flex items-center justify-between">
|
||||||
|
<CardTitle className="text-sm text-gray-400">PEZ Rewards</CardTitle>
|
||||||
|
{pezRewards && (
|
||||||
|
<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'
|
||||||
|
}`}>
|
||||||
|
{pezRewards.epochStatus === 'Open' ? 'Open' : pezRewards.epochStatus === 'ClaimPeriod' ? 'Claim Period' : 'Closed'}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{stakingInfo?.pezRewards && stakingInfo.pezRewards.hasPendingClaim ? (
|
{pezRewards ? (
|
||||||
<>
|
<div className="space-y-2">
|
||||||
<div className="text-2xl font-bold text-orange-500">
|
<p className="text-xs text-gray-500">Epoch {pezRewards.currentEpoch}</p>
|
||||||
{parseFloat(stakingInfo.pezRewards.totalClaimable).toFixed(2)} PEZ
|
|
||||||
</div>
|
{/* Open epoch: Record score or show recorded score */}
|
||||||
<p className="text-xs text-gray-500 mt-1">
|
{pezRewards.epochStatus === 'Open' && (
|
||||||
{stakingInfo.pezRewards.claimableRewards.length} epoch(s) to claim
|
pezRewards.hasRecordedThisEpoch ? (
|
||||||
</p>
|
<div>
|
||||||
<Button
|
<div className="text-lg font-bold text-green-400">
|
||||||
size="sm"
|
Score: {pezRewards.userScoreCurrentEpoch}
|
||||||
onClick={() => {
|
</div>
|
||||||
toast.info('Claim PEZ rewards functionality will be available soon');
|
<p className="text-xs text-gray-500">Recorded for this epoch</p>
|
||||||
}}
|
</div>
|
||||||
disabled={isLoading}
|
) : (
|
||||||
className="mt-2 w-full bg-orange-600 hover:bg-orange-700"
|
<Button
|
||||||
>
|
size="sm"
|
||||||
Claim Rewards
|
onClick={handleRecordTrustScore}
|
||||||
</Button>
|
disabled={isRecordingScore}
|
||||||
</>
|
className="w-full bg-green-600 hover:bg-green-700"
|
||||||
|
>
|
||||||
|
{isRecordingScore ? 'Recording...' : 'Record Trust Score'}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Claimable rewards */}
|
||||||
|
{pezRewards.hasPendingClaim ? (
|
||||||
|
<>
|
||||||
|
<div className="text-2xl font-bold text-orange-500">
|
||||||
|
{parseFloat(pezRewards.totalClaimable).toFixed(2)} PEZ
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
{pezRewards.claimableRewards.map((reward) => (
|
||||||
|
<div key={reward.epoch} className="flex items-center justify-between">
|
||||||
|
<span className="text-xs text-gray-400">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 border-orange-500 text-orange-400 hover:bg-orange-500/20"
|
||||||
|
>
|
||||||
|
{isClaimingReward ? '...' : 'Claim'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
!pezRewards.hasRecordedThisEpoch && pezRewards.epochStatus !== 'Open' && (
|
||||||
|
<div className="text-2xl font-bold text-gray-500">0 PEZ</div>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="text-2xl font-bold text-gray-500">0 PEZ</div>
|
<div className="text-2xl font-bold text-gray-500">0 PEZ</div>
|
||||||
<p className="text-xs text-gray-500 mt-1">
|
<p className="text-xs text-gray-500 mt-1">No rewards available</p>
|
||||||
{stakingInfo?.pezRewards
|
|
||||||
? `Epoch ${stakingInfo.pezRewards.currentEpoch}`
|
|
||||||
: 'No rewards available'}
|
|
||||||
</p>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
+151
-6
@@ -7,10 +7,10 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|||||||
import { useAuth } from '@/contexts/AuthContext';
|
import { useAuth } from '@/contexts/AuthContext';
|
||||||
import { usePezkuwi } from '@/contexts/PezkuwiContext';
|
import { usePezkuwi } from '@/contexts/PezkuwiContext';
|
||||||
import { supabase } from '@/lib/supabase';
|
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 { useToast } from '@/hooks/use-toast';
|
||||||
import { fetchUserTikis, getPrimaryRole, getTikiDisplayName, getTikiColor, getTikiEmoji, getUserRoleCategories, getAllTikiNFTDetails, generateCitizenNumber, type TikiNFTDetails } from '@pezkuwi/lib/tiki';
|
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 { web3FromAddress } from '@pezkuwi/extension-dapp';
|
||||||
import { getKycStatus } from '@pezkuwi/lib/kyc';
|
import { getKycStatus } from '@pezkuwi/lib/kyc';
|
||||||
import { ReferralDashboard } from '@/components/referral/ReferralDashboard';
|
import { ReferralDashboard } from '@/components/referral/ReferralDashboard';
|
||||||
@@ -37,6 +37,9 @@ export default function Dashboard() {
|
|||||||
const [startingScoreTracking, setStartingScoreTracking] = useState(false);
|
const [startingScoreTracking, setStartingScoreTracking] = useState(false);
|
||||||
const [kycStatus, setKycStatus] = useState<string>('NotStarted');
|
const [kycStatus, setKycStatus] = useState<string>('NotStarted');
|
||||||
const [renouncingCitizenship, setRenouncingCitizenship] = useState(false);
|
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 }>({
|
const [nftDetails, setNftDetails] = useState<{ citizenNFT: TikiNFTDetails | null; roleNFTs: TikiNFTDetails[]; totalNFTs: number }>({
|
||||||
citizenNFT: null,
|
citizenNFT: null,
|
||||||
roleNFTs: [],
|
roleNFTs: [],
|
||||||
@@ -114,8 +117,8 @@ export default function Dashboard() {
|
|||||||
const allScores = await getAllScores(peopleApi, selectedAccount.address);
|
const allScores = await getAllScores(peopleApi, selectedAccount.address);
|
||||||
setScores(allScores);
|
setScores(allScores);
|
||||||
|
|
||||||
// Fetch staking score tracking status
|
// Fetch staking score tracking status (from Relay Chain where stakingScore pallet lives)
|
||||||
const stakingStatusResult = await getStakingScoreStatus(peopleApi, selectedAccount.address);
|
const stakingStatusResult = await getStakingScoreStatus(api, selectedAccount.address);
|
||||||
setStakingStatus(stakingStatusResult);
|
setStakingStatus(stakingStatusResult);
|
||||||
|
|
||||||
// Fetch tikis from People Chain (tiki pallet is on People Chain)
|
// 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)
|
// Fetch KYC status from People Chain (identityKyc pallet is on People Chain)
|
||||||
const kycStatusResult = await getKycStatus(peopleApi, selectedAccount.address);
|
const kycStatusResult = await getKycStatus(peopleApi, selectedAccount.address);
|
||||||
setKycStatus(kycStatusResult);
|
setKycStatus(kycStatusResult);
|
||||||
|
|
||||||
|
// Fetch PEZ rewards from People Chain
|
||||||
|
const rewards = await getPezRewards(peopleApi, selectedAccount.address);
|
||||||
|
setPezRewards(rewards);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (import.meta.env.DEV) console.error('Error fetching scores and tikis:', error);
|
if (import.meta.env.DEV) console.error('Error fetching scores and tikis:', error);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -137,7 +144,7 @@ export default function Dashboard() {
|
|||||||
}, [selectedAccount, api, peopleApi]);
|
}, [selectedAccount, api, peopleApi]);
|
||||||
|
|
||||||
const handleStartScoreTracking = async () => {
|
const handleStartScoreTracking = async () => {
|
||||||
if (!peopleApi || !selectedAccount) {
|
if (!api || !selectedAccount) {
|
||||||
toast({
|
toast({
|
||||||
title: "Hata",
|
title: "Hata",
|
||||||
description: "Lütfen önce cüzdanınızı bağlayın",
|
description: "Lütfen önce cüzdanınızı bağlayın",
|
||||||
@@ -149,7 +156,9 @@ export default function Dashboard() {
|
|||||||
setStartingScoreTracking(true);
|
setStartingScoreTracking(true);
|
||||||
try {
|
try {
|
||||||
const injector = await web3FromAddress(selectedAccount.address);
|
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) {
|
if (result.success) {
|
||||||
toast({
|
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(() => {
|
useEffect(() => {
|
||||||
fetchProfile();
|
fetchProfile();
|
||||||
if (selectedAccount && api && isApiReady && peopleApi && isPeopleReady) {
|
if (selectedAccount && api && isApiReady && peopleApi && isPeopleReady) {
|
||||||
@@ -529,6 +581,99 @@ export default function Dashboard() {
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</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">
|
<Tabs defaultValue="profile" className="space-y-4">
|
||||||
<TabsList className="flex flex-wrap gap-1">
|
<TabsList className="flex flex-wrap gap-1">
|
||||||
<TabsTrigger value="profile" className="text-xs sm:text-sm px-2 sm:px-3">Profile</TabsTrigger>
|
<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 { TransactionHistory } from '@/components/TransactionHistory';
|
||||||
import { NftList } from '@/components/NftList';
|
import { NftList } from '@/components/NftList';
|
||||||
import { Button } from '@/components/ui/button';
|
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 {
|
interface Transaction {
|
||||||
blockNumber: number;
|
blockNumber: number;
|
||||||
@@ -24,12 +27,15 @@ interface Transaction {
|
|||||||
|
|
||||||
const WalletDashboard: React.FC = () => {
|
const WalletDashboard: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { api, isApiReady, selectedAccount } = usePezkuwi();
|
const { api, isApiReady, peopleApi, isPeopleReady, selectedAccount } = usePezkuwi();
|
||||||
const [isTransferModalOpen, setIsTransferModalOpen] = useState(false);
|
const [isTransferModalOpen, setIsTransferModalOpen] = useState(false);
|
||||||
const [isReceiveModalOpen, setIsReceiveModalOpen] = useState(false);
|
const [isReceiveModalOpen, setIsReceiveModalOpen] = useState(false);
|
||||||
const [isHistoryModalOpen, setIsHistoryModalOpen] = useState(false);
|
const [isHistoryModalOpen, setIsHistoryModalOpen] = useState(false);
|
||||||
const [recentTransactions, setRecentTransactions] = useState<Transaction[]>([]);
|
const [recentTransactions, setRecentTransactions] = useState<Transaction[]>([]);
|
||||||
const [isLoadingRecent, setIsLoadingRecent] = useState(false);
|
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)
|
// Fetch recent transactions (limited to last 10 blocks for performance)
|
||||||
const fetchRecentTransactions = async () => {
|
const fetchRecentTransactions = async () => {
|
||||||
@@ -177,7 +183,7 @@ const WalletDashboard: React.FC = () => {
|
|||||||
|
|
||||||
// Parse stakingScore & pezRewards
|
// Parse stakingScore & pezRewards
|
||||||
else if ((method.section === 'stakingScore' && method.method === 'startTracking') ||
|
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({
|
txList.push({
|
||||||
blockNumber,
|
blockNumber,
|
||||||
extrinsicIndex: index,
|
extrinsicIndex: index,
|
||||||
@@ -210,6 +216,64 @@ const WalletDashboard: React.FC = () => {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [selectedAccount, api, isApiReady]);
|
}, [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 formatAmount = (amount: string, decimals: number = 12) => {
|
||||||
const value = parseInt(amount) / Math.pow(10, decimals);
|
const value = parseInt(amount) / Math.pow(10, decimals);
|
||||||
return value.toFixed(4);
|
return value.toFixed(4);
|
||||||
@@ -355,6 +419,78 @@ const WalletDashboard: React.FC = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</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 */}
|
{/* Token Balances */}
|
||||||
<AccountBalance />
|
<AccountBalance />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user