feat: align frontend scoring with People Chain pallet queries

- Update staking score/tracking calls from relayApi to peopleApi
- Fix referral score to use on-chain tiered scoring with penalties
- Fix perwerde score to query studentCourses + enrollments storage
- Update Dashboard and StakingDashboard for People Chain API
This commit is contained in:
2026-02-13 20:13:55 +03:00
parent 9963a759d3
commit f3450d1207
4 changed files with 117 additions and 60 deletions
+103 -47
View File
@@ -1,11 +1,12 @@
// ========================================
// Score Systems Integration
// ========================================
// Score pallets are distributed across chains:
// - Trust Score: pezpallet-trust (People Chain)
// All score pallets live on People Chain:
// - Trust Score: pezpallet-trust (People Chain) - composite score
// - Referral Score: pezpallet-referral (People Chain)
// - Staking Score: pezpallet-staking-score (Relay Chain - needs staking.ledger access)
// - Staking Score: pezpallet-staking-score (People Chain) - uses cached staking data from Asset Hub via XCM
// - Tiki Score: pezpallet-tiki (People Chain)
// - Perwerde Score: pezpallet-perwerde (People Chain)
import type { ApiPromise } from '@pezkuwi/api';
import { formatBalance } from './wallet';
@@ -84,31 +85,66 @@ export async function getTrustScore(
// ========================================
/**
* Fetch user's referral count and calculate score
* Storage: referral.referralCount(address)
* Fetch user's referral score from on-chain stats
* Storage: referral.referrerStatsStorage(address) → { totalReferrals, revokedReferrals, penaltyScore }
*
* Score calculation:
* On-chain tiered scoring (matches pezpallet-referral):
* - 0 referrals: 0 points
* - 1-5 referrals: count × 4 points
* - 6-20 referrals: 20 + (count - 5) × 2 points
* - 21+ referrals: capped at 50 points
* - 1-10 referrals: count × 10 points (max 100)
* - 11-50 referrals: 100 + (count - 10) × 5 points (max 300)
* - 51-100 referrals: 300 + (count - 50) × 4 points (max 500)
* - 101+ referrals: 500 points (maximum)
* Then subtract penalty_score from revoked referrals
*/
export async function getReferralScore(
peopleApi: ApiPromise,
address: string
): Promise<number> {
try {
if (!peopleApi?.query?.referral?.referralCount) {
return 0;
// Try reading from referrerStatsStorage for full stats with penalties
if (peopleApi?.query?.referral?.referrerStatsStorage) {
const stats = await peopleApi.query.referral.referrerStatsStorage(address);
if (!stats.isEmpty) {
const statsJson = stats.toJSON() as {
totalReferrals?: number;
total_referrals?: number;
revokedReferrals?: number;
revoked_referrals?: number;
penaltyScore?: number;
penalty_score?: number;
};
const totalReferrals = statsJson.totalReferrals ?? statsJson.total_referrals ?? 0;
const revokedReferrals = statsJson.revokedReferrals ?? statsJson.revoked_referrals ?? 0;
const penaltyScore = statsJson.penaltyScore ?? statsJson.penalty_score ?? 0;
// Step 1: Remove revoked referrals from count
const goodReferrals = Math.max(0, totalReferrals - revokedReferrals);
// Step 2: Tiered scoring
let baseScore: number;
if (goodReferrals === 0) baseScore = 0;
else if (goodReferrals <= 10) baseScore = goodReferrals * 10;
else if (goodReferrals <= 50) baseScore = 100 + ((goodReferrals - 10) * 5);
else if (goodReferrals <= 100) baseScore = 300 + ((goodReferrals - 50) * 4);
else baseScore = 500;
// Step 3: Subtract penalty
return Math.max(0, baseScore - penaltyScore);
}
}
const count = await peopleApi.query.referral.referralCount(address);
const referralCount = Number(count.toString());
// Fallback: simple count-based scoring
if (peopleApi?.query?.referral?.referralCount) {
const count = await peopleApi.query.referral.referralCount(address);
const referralCount = Number(count.toString());
if (referralCount === 0) return 0;
if (referralCount <= 10) return referralCount * 10;
if (referralCount <= 50) return 100 + ((referralCount - 10) * 5);
if (referralCount <= 100) return 300 + ((referralCount - 50) * 4);
return 500;
}
if (referralCount === 0) return 0;
if (referralCount <= 5) return referralCount * 4;
if (referralCount <= 20) return 20 + ((referralCount - 5) * 2);
return 50; // Capped at 50 points
return 0;
} catch (error) {
console.error('Error fetching referral score:', error);
return 0;
@@ -136,27 +172,27 @@ export async function getReferralCount(
}
// ========================================
// STAKING SCORE (pezpallet-staking-score on Relay Chain)
// STAKING SCORE (pezpallet-staking-score on People 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.
* The stakingScore pallet is on People Chain. It receives staking data
* from Asset Hub via XCM (stored in cachedStakingDetails).
*/
export async function getStakingScoreStatus(
relayApi: ApiPromise,
peopleApi: ApiPromise,
address: string
): Promise<StakingScoreStatus> {
try {
if (!relayApi?.query?.stakingScore?.stakingStartBlock) {
if (!peopleApi?.query?.stakingScore?.stakingStartBlock) {
return { isTracking: false, startBlock: null, currentBlock: 0, durationBlocks: 0 };
}
const startBlockResult = await relayApi.query.stakingScore.stakingStartBlock(address);
const currentBlock = Number((await relayApi.query.system.number()).toString());
const startBlockResult = await peopleApi.query.stakingScore.stakingStartBlock(address);
const currentBlock = Number((await peopleApi.query.system.number()).toString());
if (startBlockResult.isEmpty || startBlockResult.isNone) {
return { isTracking: false, startBlock: null, currentBlock, durationBlocks: 0 };
@@ -181,28 +217,27 @@ 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.
* Called on People Chain. Requires staking data to be available via
* cachedStakingDetails (pushed from Asset Hub via XCM).
*/
export async function startScoreTracking(
relayApi: ApiPromise,
peopleApi: ApiPromise,
address: string,
signer: any
): Promise<{ success: boolean; error?: string }> {
try {
if (!relayApi?.tx?.stakingScore?.startScoreTracking) {
if (!peopleApi?.tx?.stakingScore?.startScoreTracking) {
return { success: false, error: 'stakingScore pallet not available on this chain' };
}
const tx = relayApi.tx.stakingScore.startScoreTracking();
const tx = peopleApi.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 = relayApi.registry.findMetaError(dispatchError.asModule);
const decoded = peopleApi.registry.findMetaError(dispatchError.asModule);
resolve({ success: false, error: `${decoded.section}.${decoded.name}: ${decoded.docs.join(' ')}` });
} else {
resolve({ success: false, error: dispatchError.toString() });
@@ -351,6 +386,9 @@ export async function checkCitizenshipStatus(
/**
* Get Perwerde (education) score
* This is from pezpallet-perwerde on People Chain
*
* Queries studentCourses for enrolled course IDs, then checks enrollments
* for completed courses and sums their points.
*/
export async function getPerwerdeScore(
peopleApi: ApiPromise | null,
@@ -364,25 +402,43 @@ export async function getPerwerdeScore(
return 0;
}
// Try to get user's completed courses/certifications
if (peopleApi.query.perwerde.userScores) {
const score = await peopleApi.query.perwerde.userScores(address);
if (!score.isEmpty) {
return Number(score.toString());
// Get user's enrolled course IDs
if (!peopleApi.query.perwerde.studentCourses) {
return 0;
}
const coursesResult = await peopleApi.query.perwerde.studentCourses(address);
const courseIds = coursesResult.toJSON() as number[];
if (!Array.isArray(courseIds) || courseIds.length === 0) {
return 0;
}
// Sum points from completed courses
let totalPoints = 0;
for (const courseId of courseIds) {
try {
if (!peopleApi.query.perwerde.enrollments) break;
const enrollment = await peopleApi.query.perwerde.enrollments([address, courseId]);
if (enrollment.isEmpty || enrollment.isNone) continue;
const enrollmentJson = enrollment.toJSON() as {
completedAt?: number | null;
completed_at?: number | null;
pointsEarned?: number;
points_earned?: number;
};
const completedAt = enrollmentJson.completedAt ?? enrollmentJson.completed_at;
if (completedAt !== null && completedAt !== undefined) {
totalPoints += enrollmentJson.pointsEarned ?? enrollmentJson.points_earned ?? 0;
}
} catch {
// Skip individual enrollment errors
}
}
// Alternative: count completed courses
if (peopleApi.query.perwerde.completedCourses) {
const courses = await peopleApi.query.perwerde.completedCourses(address);
const coursesJson = courses.toJSON() as unknown[];
if (Array.isArray(coursesJson)) {
// Each completed course = 10 points, max 50
return Math.min(coursesJson.length * 10, 50);
}
}
return 0;
return totalPoints;
} catch (err) {
console.error('Error fetching perwerde score:', err);
return 0;
+5 -4
View File
@@ -238,16 +238,17 @@ export async function getStakingInfo(
let hasStartedScoreTracking = false;
try {
// stakingScore pallet is on Relay Chain (same as staking pallet, needs staking.ledger access)
if (api.query.stakingScore && api.query.stakingScore.stakingStartBlock) {
// stakingScore pallet is on People Chain - uses cached staking data from Asset Hub via XCM
const scoreApi = peopleApi || api;
if (scoreApi.query.stakingScore && scoreApi.query.stakingScore.stakingStartBlock) {
// Check if user has started score tracking
const scoreResult = await api.query.stakingScore.stakingStartBlock(address);
const scoreResult = await scoreApi.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 api.query.system.number()).toString());
const currentBlock = Number((await scoreApi.query.system.number()).toString());
const durationInBlocks = currentBlock - startBlock;
stakingDuration = durationInBlocks;
@@ -361,7 +361,7 @@ export const StakingDashboard: React.FC = () => {
};
const handleStartScoreTracking = async () => {
if (!api || !selectedAccount) return;
if (!peopleApi || !selectedAccount) return;
if (!stakingInfo || parseFloat(stakingInfo.bonded) === 0) {
toast.error('You must bond tokens before starting score tracking');
@@ -371,7 +371,8 @@ export const StakingDashboard: React.FC = () => {
setIsLoading(true);
try {
const injector = await web3FromAddress(selectedAccount.address);
const tx = api.tx.stakingScore.startScoreTracking();
// stakingScore pallet is on People Chain - uses cached staking data from Asset Hub
const tx = peopleApi.tx.stakingScore.startScoreTracking();
await tx.signAndSend(
selectedAccount.address,
@@ -381,7 +382,7 @@ export const StakingDashboard: React.FC = () => {
if (dispatchError) {
let errorMessage = 'Failed to start score tracking';
if (dispatchError.isModule) {
const decoded = api.registry.findMetaError(dispatchError.asModule);
const decoded = peopleApi.registry.findMetaError(dispatchError.asModule);
errorMessage = `${decoded.section}.${decoded.name}: ${decoded.docs.join(' ')}`;
}
toast.error(errorMessage);
+5 -6
View File
@@ -117,8 +117,8 @@ export default function Dashboard() {
const allScores = await getAllScores(peopleApi, selectedAccount.address);
setScores(allScores);
// Fetch staking score tracking status (from Relay Chain where stakingScore pallet lives)
const stakingStatusResult = await getStakingScoreStatus(api, selectedAccount.address);
// Fetch staking score tracking status (People Chain - uses cached staking data from Asset Hub)
const stakingStatusResult = await getStakingScoreStatus(peopleApi, selectedAccount.address);
setStakingStatus(stakingStatusResult);
// Fetch tikis from People Chain (tiki pallet is on People Chain)
@@ -144,7 +144,7 @@ export default function Dashboard() {
}, [selectedAccount, api, peopleApi]);
const handleStartScoreTracking = async () => {
if (!api || !selectedAccount) {
if (!peopleApi || !selectedAccount) {
toast({
title: "Hata",
description: "Lütfen önce cüzdanınızı bağlayın",
@@ -156,9 +156,8 @@ export default function Dashboard() {
setStartingScoreTracking(true);
try {
const injector = await web3FromAddress(selectedAccount.address);
// 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);
// startScoreTracking on People Chain - staking data comes from Asset Hub via XCM
const result = await startScoreTracking(peopleApi, selectedAccount.address, injector.signer);
if (result.success) {
toast({