mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-04-22 02:07:55 +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.',
|
||||
},
|
||||
|
||||
// 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
|
||||
'identityKyc.AlreadyApplied': {
|
||||
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.',
|
||||
},
|
||||
|
||||
// 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.applied': {
|
||||
en: 'Citizenship application submitted successfully! We will review your application.',
|
||||
|
||||
+236
-15
@@ -1,13 +1,14 @@
|
||||
// ========================================
|
||||
// Score Systems Integration
|
||||
// ========================================
|
||||
// All scores come from People Chain (people-rpc.pezkuwichain.io)
|
||||
// - Trust Score: pezpallet-trust
|
||||
// - Referral Score: pezpallet-referral
|
||||
// - Staking Score: pezpallet-staking-score
|
||||
// - Tiki Score: pezpallet-tiki
|
||||
// Score pallets are distributed across chains:
|
||||
// - Trust Score: pezpallet-trust (People Chain)
|
||||
// - Referral Score: pezpallet-referral (People Chain)
|
||||
// - Staking Score: pezpallet-staking-score (Relay Chain - needs staking.ledger access)
|
||||
// - Tiki Score: pezpallet-tiki (People Chain)
|
||||
|
||||
import type { ApiPromise } from '@pezkuwi/api';
|
||||
import { formatBalance } from './wallet';
|
||||
|
||||
// ========================================
|
||||
// TYPE DEFINITIONS
|
||||
@@ -28,6 +29,26 @@ export interface StakingScoreStatus {
|
||||
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)
|
||||
// ========================================
|
||||
@@ -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
|
||||
* 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(
|
||||
peopleApi: ApiPromise,
|
||||
relayApi: ApiPromise,
|
||||
address: string
|
||||
): Promise<StakingScoreStatus> {
|
||||
try {
|
||||
if (!peopleApi?.query?.stakingScore?.stakingStartBlock) {
|
||||
if (!relayApi?.query?.stakingScore?.stakingStartBlock) {
|
||||
return { isTracking: false, startBlock: null, currentBlock: 0, durationBlocks: 0 };
|
||||
}
|
||||
|
||||
const startBlockResult = await peopleApi.query.stakingScore.stakingStartBlock(address);
|
||||
const currentBlock = Number((await peopleApi.query.system.number()).toString());
|
||||
const startBlockResult = await relayApi.query.stakingScore.stakingStartBlock(address);
|
||||
const currentBlock = Number((await relayApi.query.system.number()).toString());
|
||||
|
||||
if (startBlockResult.isEmpty || startBlockResult.isNone) {
|
||||
return { isTracking: false, startBlock: null, currentBlock, durationBlocks: 0 };
|
||||
@@ -156,25 +180,29 @@ export async function getStakingScoreStatus(
|
||||
/**
|
||||
* Start staking score tracking
|
||||
* 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(
|
||||
peopleApi: ApiPromise,
|
||||
relayApi: ApiPromise,
|
||||
address: string,
|
||||
signer: any
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
if (!peopleApi?.tx?.stakingScore?.startScoreTracking) {
|
||||
return { success: false, error: 'stakingScore pallet not available' };
|
||||
if (!relayApi?.tx?.stakingScore?.startScoreTracking) {
|
||||
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) => {
|
||||
tx.signAndSend(address, { signer }, ({ status, dispatchError }) => {
|
||||
if (status.isInBlock || status.isFinalized) {
|
||||
if (dispatchError) {
|
||||
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(' ')}` });
|
||||
} else {
|
||||
resolve({ success: false, error: dispatchError.toString() });
|
||||
@@ -360,3 +388,196 @@ export async function getPerwerdeScore(
|
||||
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 { formatBalance } from './wallet';
|
||||
import { getPezRewards, type PezRewardInfo } from './scores';
|
||||
|
||||
export interface StakingLedger {
|
||||
stash: string;
|
||||
@@ -30,14 +31,6 @@ export interface EraRewardPoints {
|
||||
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 {
|
||||
bonded: 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
|
||||
* @param api - Relay Chain API (for staking pallet)
|
||||
@@ -329,17 +238,16 @@ export async function getStakingInfo(
|
||||
let hasStartedScoreTracking = false;
|
||||
|
||||
try {
|
||||
// stakingScore pallet is on People Chain
|
||||
const scoreApi = peopleApi || api;
|
||||
if (scoreApi.query.stakingScore && scoreApi.query.stakingScore.stakingStartBlock) {
|
||||
// stakingScore pallet is on Relay Chain (same as staking pallet, needs staking.ledger access)
|
||||
if (api.query.stakingScore && api.query.stakingScore.stakingStartBlock) {
|
||||
// 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) {
|
||||
hasStartedScoreTracking = true;
|
||||
const startBlockCodec = scoreResult.unwrap() as { toString: () => string };
|
||||
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;
|
||||
stakingDuration = durationInBlocks;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user