fix: update score system to use correct People Chain APIs

- Remove LP staking score (not available in blockchain)
- All scores now fetched from People Chain
- Add Start Score Tracking button to Dashboard
- Add staking status display showing tracking duration
This commit is contained in:
2026-02-06 20:48:27 +03:00
parent b0c3626e7a
commit 61eb5cff20
2 changed files with 250 additions and 276 deletions
+161 -244
View File
@@ -1,7 +1,11 @@
// ========================================
// Score Systems Integration
// ========================================
// Centralized score fetching from blockchain pallets
// 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
import type { ApiPromise } from '@pezkuwi/api';
@@ -13,39 +17,35 @@ export interface UserScores {
trustScore: number;
referralScore: number;
stakingScore: number;
lpStakingScore: number;
tikiScore: number;
totalScore: number;
}
export interface TrustScoreDetails {
totalScore: number;
stakingPoints: number;
referralPoints: number;
tikiPoints: number;
activityPoints: number;
historyLength: number;
export interface StakingScoreStatus {
isTracking: boolean;
startBlock: number | null;
currentBlock: number;
durationBlocks: number;
}
// ========================================
// TRUST SCORE (pallet_trust)
// TRUST SCORE (pezpallet-trust on People Chain)
// ========================================
/**
* Fetch user's trust score from blockchain
* pallet_trust::TrustScores storage
* Storage: trust.trustScores(address)
*/
export async function getTrustScore(
api: ApiPromise,
peopleApi: ApiPromise,
address: string
): Promise<number> {
try {
if (!api?.query?.trust) {
// Trust pallet not available on this chain - this is expected
if (!peopleApi?.query?.trust?.trustScores) {
return 0;
}
const score = await api.query.trust.trustScores(address);
const score = await peopleApi.query.trust.trustScores(address);
if (score.isEmpty) {
return 0;
@@ -58,101 +58,36 @@ export async function getTrustScore(
}
}
/**
* Fetch detailed trust score breakdown
* pallet_trust::ScoreHistory storage
*/
export async function getTrustScoreDetails(
api: ApiPromise,
address: string
): Promise<TrustScoreDetails | null> {
try {
if (!api?.query?.trust) {
return null;
}
const totalScore = await getTrustScore(api, address);
// Get score history to show detailed breakdown
const historyResult = await api.query.trust.scoreHistory(address);
if (historyResult.isEmpty) {
return {
totalScore,
stakingPoints: 0,
referralPoints: 0,
tikiPoints: 0,
activityPoints: 0,
historyLength: 0
};
}
const history = historyResult.toJSON() as any[];
// Calculate points from history
// History format: [{blockNumber, score, reason}]
let stakingPoints = 0;
let referralPoints = 0;
let tikiPoints = 0;
let activityPoints = 0;
for (const entry of history) {
const reason = entry.reason || '';
const score = entry.score || 0;
if (reason.includes('Staking')) stakingPoints += score;
else if (reason.includes('Referral')) referralPoints += score;
else if (reason.includes('Tiki') || reason.includes('Role')) tikiPoints += score;
else activityPoints += score;
}
return {
totalScore,
stakingPoints,
referralPoints,
tikiPoints,
activityPoints,
historyLength: history.length
};
} catch (error) {
console.error('Error fetching trust score details:', error);
return null;
}
}
// ========================================
// REFERRAL SCORE (pallet_trust)
// REFERRAL SCORE (pezpallet-referral on People Chain)
// ========================================
/**
* Fetch user's referral score
* Reads from pallet_referral::ReferralCount storage
* Fetch user's referral count and calculate score
* Storage: referral.referralCount(address)
*
* Score calculation based on referral count:
* Score calculation:
* - 0 referrals: 0 points
* - 1-5 referrals: count × 4 points (4, 8, 12, 16, 20)
* - 1-5 referrals: count × 4 points
* - 6-20 referrals: 20 + (count - 5) × 2 points
* - 21+ referrals: capped at 50 points
*/
export async function getReferralScore(
api: ApiPromise,
peopleApi: ApiPromise,
address: string
): Promise<number> {
try {
if (!api?.query?.referral?.referralCount) {
if (typeof __DEV__ !== 'undefined' && __DEV__) console.warn('Referral pallet not available');
if (!peopleApi?.query?.referral?.referralCount) {
return 0;
}
const count = await api.query.referral.referralCount(address);
const count = await peopleApi.query.referral.referralCount(address);
const referralCount = Number(count.toString());
// Calculate score based on referral count
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
} catch (error) {
console.error('Error fetching referral score:', error);
return 0;
@@ -160,26 +95,19 @@ export async function getReferralScore(
}
/**
* Get referral count for user
* pallet_trust::Referrals storage
* Get raw referral count
*/
export async function getReferralCount(
api: ApiPromise,
peopleApi: ApiPromise,
address: string
): Promise<number> {
try {
if (!api?.query?.trust?.referrals) {
if (!peopleApi?.query?.referral?.referralCount) {
return 0;
}
const referrals = await api.query.trust.referrals(address);
if (referrals.isEmpty) {
return 0;
}
const referralList = referrals.toJSON() as any[];
return Array.isArray(referralList) ? referralList.length : 0;
const count = await peopleApi.query.referral.referralCount(address);
return Number(count.toString());
} catch (error) {
console.error('Error fetching referral count:', error);
return 0;
@@ -187,86 +115,103 @@ export async function getReferralCount(
}
// ========================================
// STAKING SCORE (pallet_staking_score)
// STAKING SCORE (pezpallet-staking-score on People Chain)
// ========================================
/**
* Get staking score from pallet_staking_score
* NOTE: stakingScore pallet is on People Chain, but staking.ledger is on Relay Chain
* So this function needs both APIs
*
* @param peopleApi - API for People Chain (stakingScore pallet)
* @param relayApi - API for Relay Chain (staking pallet) - optional, if not provided uses peopleApi
* Check staking score tracking status
* Storage: stakingScore.stakingStartBlock(address)
*/
export async function getStakingScoreFromPallet(
export async function getStakingScoreStatus(
peopleApi: ApiPromise,
address: string
): Promise<StakingScoreStatus> {
try {
if (!peopleApi?.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());
if (startBlockResult.isEmpty || startBlockResult.isNone) {
return { isTracking: false, startBlock: null, currentBlock, durationBlocks: 0 };
}
const startBlock = Number(startBlockResult.toString());
const durationBlocks = currentBlock - startBlock;
return {
isTracking: true,
startBlock,
currentBlock,
durationBlocks
};
} catch (error) {
console.error('Error fetching staking score status:', error);
return { isTracking: false, startBlock: null, currentBlock: 0, durationBlocks: 0 };
}
}
/**
* Get staking score from pallet
* Requires: User must have called startScoreTracking() first
* Score based on staking duration + amount staked on Relay Chain
*
* @param peopleApi - People Chain API (stakingScore pallet)
* @param address - User's blockchain address
* @param relayApi - Relay Chain API (staking.ledger for staked amount)
*/
export async function getStakingScore(
peopleApi: ApiPromise,
address: string,
relayApi?: ApiPromise
): Promise<number> {
try {
if (!peopleApi?.query?.stakingScore) {
// Staking score pallet not available on this chain
return 0;
const status = await getStakingScoreStatus(peopleApi, address);
if (!status.isTracking) {
return 0; // User hasn't started score tracking
}
// Check if user has started score tracking (People Chain)
const scoreResult = await peopleApi.query.stakingScore.stakingStartBlock(address);
if (scoreResult.isNone) {
return 0;
// Get staked amount from Relay Chain if available
let stakedAmount = 0;
if (relayApi?.query?.staking?.ledger) {
const ledger = await relayApi.query.staking.ledger(address);
if (!ledger.isEmpty && !ledger.isNone) {
const ledgerData = (ledger as any).unwrap().toJSON();
stakedAmount = Number(ledgerData.total || 0) / 1e12;
}
}
// Get staking info from staking pallet (Relay Chain)
const stakingApi = relayApi || peopleApi;
if (!stakingApi?.query?.staking?.ledger) {
// Staking pallet not available - can't calculate full score
// Return base score from duration only
const scoreCodec = scoreResult.unwrap() as { toString: () => string };
const startBlock = Number(scoreCodec.toString());
const currentBlock = Number((await peopleApi.query.system.number()).toString());
const durationInBlocks = currentBlock - startBlock;
const MONTH_IN_BLOCKS = 30 * 24 * 60 * 10;
if (durationInBlocks >= 12 * MONTH_IN_BLOCKS) return 40;
if (durationInBlocks >= 6 * MONTH_IN_BLOCKS) return 34;
if (durationInBlocks >= 3 * MONTH_IN_BLOCKS) return 28;
if (durationInBlocks >= MONTH_IN_BLOCKS) return 24;
return 20;
// Calculate score based on amount
// Amount-based score (0-50 points)
let amountScore = 0;
if (stakedAmount > 0) {
if (stakedAmount <= 10) amountScore = 10;
else if (stakedAmount <= 100) amountScore = 20;
else if (stakedAmount <= 250) amountScore = 30;
else if (stakedAmount <= 750) amountScore = 40;
else amountScore = 50;
}
const ledger = await stakingApi.query.staking.ledger(address);
if (ledger.isNone) {
return 0;
}
const ledgerCodec = ledger.unwrap() as { toJSON: () => unknown };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const ledgerData = ledgerCodec.toJSON() as any;
const stakedAmount = Number(ledgerData.total || 0) / 1e12; // Convert to HEZ
// Get duration from People Chain
const scoreCodec = scoreResult.unwrap() as { toString: () => string };
const startBlock = Number(scoreCodec.toString());
const currentBlock = Number((await peopleApi.query.system.number()).toString());
const durationInBlocks = currentBlock - startBlock;
// Calculate score based on amount and duration
// Amount-based score (20-50 points)
let amountScore = 20;
if (stakedAmount <= 100) amountScore = 20;
else if (stakedAmount <= 250) amountScore = 30;
else if (stakedAmount <= 750) amountScore = 40;
else amountScore = 50;
// Duration multiplier
const MONTH_IN_BLOCKS = 30 * 24 * 60 * 10; // ~30 days
const MONTH_IN_BLOCKS = 30 * 24 * 60 * 10; // ~432000 blocks per month
let durationMultiplier = 1.0;
if (durationInBlocks >= 12 * MONTH_IN_BLOCKS) durationMultiplier = 2.0;
else if (durationInBlocks >= 6 * MONTH_IN_BLOCKS) durationMultiplier = 1.7;
else if (durationInBlocks >= 3 * MONTH_IN_BLOCKS) durationMultiplier = 1.4;
else if (durationInBlocks >= MONTH_IN_BLOCKS) durationMultiplier = 1.2;
if (status.durationBlocks >= 12 * MONTH_IN_BLOCKS) durationMultiplier = 2.0;
else if (status.durationBlocks >= 6 * MONTH_IN_BLOCKS) durationMultiplier = 1.7;
else if (status.durationBlocks >= 3 * MONTH_IN_BLOCKS) durationMultiplier = 1.4;
else if (status.durationBlocks >= MONTH_IN_BLOCKS) durationMultiplier = 1.2;
// If no staking amount but tracking is active, give base points for duration
if (amountScore === 0 && status.isTracking) {
if (status.durationBlocks >= 12 * MONTH_IN_BLOCKS) return 20;
if (status.durationBlocks >= 6 * MONTH_IN_BLOCKS) return 15;
if (status.durationBlocks >= 3 * MONTH_IN_BLOCKS) return 10;
if (status.durationBlocks >= MONTH_IN_BLOCKS) return 5;
return 0;
}
return Math.min(100, Math.floor(amountScore * durationMultiplier));
} catch (error) {
@@ -275,83 +220,59 @@ export async function getStakingScoreFromPallet(
}
}
// ========================================
// LP STAKING SCORE (Asset Hub assetRewards pallet)
// ========================================
/**
* Get LP staking score from Asset Hub
* Based on total LP tokens staked across all pools
*
* @param assetHubApi - API for Asset Hub (assetRewards pallet)
* @param address - User's blockchain address
* Start staking score tracking
* Calls: stakingScore.startScoreTracking()
*/
export async function getLpStakingScore(
assetHubApi: ApiPromise,
address: string
): Promise<number> {
export async function startScoreTracking(
peopleApi: ApiPromise,
address: string,
signer: any
): Promise<{ success: boolean; error?: string }> {
try {
if (!assetHubApi?.query?.assetRewards?.poolStakers) {
return 0;
if (!peopleApi?.tx?.stakingScore?.startScoreTracking) {
return { success: false, error: 'stakingScore pallet not available' };
}
// Query all staking pool entries
const poolEntries = await assetHubApi.query.assetRewards.pools.entries();
const tx = peopleApi.tx.stakingScore.startScoreTracking();
let totalStaked = BigInt(0);
for (const [key] of poolEntries) {
const poolId = parseInt(key.args[0].toString());
try {
const stakeInfo = await assetHubApi.query.assetRewards.poolStakers([poolId, address]);
if (stakeInfo && (stakeInfo as { isSome: boolean }).isSome) {
const stakeData = (stakeInfo as { unwrap: () => { toJSON: () => { amount: string } } }).unwrap().toJSON();
totalStaked += BigInt(stakeData.amount || '0');
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);
resolve({ success: false, error: `${decoded.section}.${decoded.name}: ${decoded.docs.join(' ')}` });
} else {
resolve({ success: false, error: dispatchError.toString() });
}
} else {
resolve({ success: true });
}
}
} catch {
// Skip this pool on error
}
}
// Convert to human readable (12 decimals for LP tokens)
const stakedAmount = Number(totalStaked) / 1e12;
// Calculate score based on amount staked
// 0-10 LP: 0 points
// 10-50 LP: 10 points
// 50-100 LP: 20 points
// 100-500 LP: 30 points
// 500-1000 LP: 40 points
// 1000+ LP: 50 points
if (stakedAmount < 10) return 0;
if (stakedAmount < 50) return 10;
if (stakedAmount < 100) return 20;
if (stakedAmount < 500) return 30;
if (stakedAmount < 1000) return 40;
return 50;
});
});
} catch (error) {
console.error('Error fetching LP staking score:', error);
return 0;
console.error('Error starting score tracking:', error);
return { success: false, error: error instanceof Error ? error.message : 'Unknown error' };
}
}
// ========================================
// TIKI SCORE (from lib/tiki.ts)
// TIKI SCORE (pezpallet-tiki on People Chain)
// ========================================
/**
* Calculate Tiki score from user's roles
* Import from lib/tiki.ts
*/
import { fetchUserTikis, calculateTikiScore } from './tiki';
/**
* Get tiki score from user's roles
*/
export async function getTikiScore(
api: ApiPromise,
peopleApi: ApiPromise,
address: string
): Promise<number> {
try {
const tikis = await fetchUserTikis(api, address);
const tikis = await fetchUserTikis(peopleApi, address);
return calculateTikiScore(tikis);
} catch (error) {
console.error('Error fetching tiki score:', error);
@@ -365,17 +286,16 @@ export async function getTikiScore(
/**
* Fetch all scores for a user in one call
* All scores come from People Chain except staking amount (Relay Chain)
*
* @param peopleApi - API for People Chain (trust, referral, tiki, stakingScore pallets)
* @param peopleApi - People Chain API (trust, referral, stakingScore, tiki pallets)
* @param address - User's blockchain address
* @param relayApi - Optional API for Relay Chain (staking pallet for validator staking score)
* @param assetHubApi - Optional API for Asset Hub (assetRewards pallet for LP staking score)
* @param relayApi - Optional Relay Chain API (for staking.ledger amount)
*/
export async function getAllScores(
peopleApi: ApiPromise,
address: string,
relayApi?: ApiPromise,
assetHubApi?: ApiPromise
relayApi?: ApiPromise
): Promise<UserScores> {
try {
if (!peopleApi || !address) {
@@ -383,31 +303,25 @@ export async function getAllScores(
trustScore: 0,
referralScore: 0,
stakingScore: 0,
lpStakingScore: 0,
tikiScore: 0,
totalScore: 0
};
}
// Fetch all scores in parallel
// - Trust, referral, tiki scores: People Chain
// - Staking score: People Chain (stakingScore pallet) + Relay Chain (staking.ledger)
// - LP Staking score: Asset Hub (assetRewards pallet)
const [trustScore, referralScore, stakingScore, lpStakingScore, tikiScore] = await Promise.all([
// Fetch all scores in parallel from People Chain
const [trustScore, referralScore, stakingScore, tikiScore] = await Promise.all([
getTrustScore(peopleApi, address),
getReferralScore(peopleApi, address),
getStakingScoreFromPallet(peopleApi, address, relayApi),
assetHubApi ? getLpStakingScore(assetHubApi, address) : Promise.resolve(0),
getStakingScore(peopleApi, address, relayApi),
getTikiScore(peopleApi, address)
]);
const totalScore = trustScore + referralScore + stakingScore + lpStakingScore + tikiScore;
const totalScore = trustScore + referralScore + stakingScore + tikiScore;
return {
trustScore,
referralScore,
stakingScore,
lpStakingScore,
tikiScore,
totalScore
};
@@ -417,7 +331,6 @@ export async function getAllScores(
trustScore: 0,
referralScore: 0,
stakingScore: 0,
lpStakingScore: 0,
tikiScore: 0,
totalScore: 0
};
@@ -428,9 +341,6 @@ export async function getAllScores(
// SCORE DISPLAY HELPERS
// ========================================
/**
* Get color class based on score
*/
export function getScoreColor(score: number): string {
if (score >= 200) return 'text-purple-500';
if (score >= 150) return 'text-pink-500';
@@ -441,9 +351,6 @@ export function getScoreColor(score: number): string {
return 'text-gray-500';
}
/**
* Get score rating label
*/
export function getScoreRating(score: number): string {
if (score >= 250) return 'Legendary';
if (score >= 200) return 'Excellent';
@@ -455,9 +362,19 @@ export function getScoreRating(score: number): string {
return 'Very Low';
}
/**
* Format score for display
*/
export function formatScore(score: number): string {
return score.toFixed(0);
}
export function formatDuration(blocks: number): string {
const BLOCKS_PER_MINUTE = 10;
const minutes = blocks / BLOCKS_PER_MINUTE;
const hours = minutes / 60;
const days = hours / 24;
const months = days / 30;
if (months >= 1) return `${Math.floor(months)} month${Math.floor(months) > 1 ? 's' : ''}`;
if (days >= 1) return `${Math.floor(days)} day${Math.floor(days) > 1 ? 's' : ''}`;
if (hours >= 1) return `${Math.floor(hours)} hour${Math.floor(hours) > 1 ? 's' : ''}`;
return `${Math.floor(minutes)} minute${Math.floor(minutes) > 1 ? 's' : ''}`;
}