From b378aeb171c2214aa31464128eb863caa855f0bf Mon Sep 17 00:00:00 2001 From: Kurdistan Tech Ministry Date: Fri, 13 Feb 2026 03:14:57 +0300 Subject: [PATCH] refactor(scores): remove frontend fallback, read all scores from blockchain Remove all frontend staking/trust score calculation and localStorage fallback code. All scores now read directly from People Chain pallets (pezpallet-trust, pezpallet-referral, pezpallet-tiki). Trust pallet computes composite score on-chain. --- shared/lib/scores.ts | 546 +--------------------- web/src/components/AccountBalance.tsx | 8 +- web/src/components/HeroSection.tsx | 11 +- web/src/components/wallet/WalletModal.tsx | 5 +- web/src/pages/Dashboard.tsx | 4 +- 5 files changed, 28 insertions(+), 546 deletions(-) diff --git a/shared/lib/scores.ts b/shared/lib/scores.ts index a6165a4f..b122ab70 100644 --- a/shared/lib/scores.ts +++ b/shared/lib/scores.ts @@ -153,73 +153,6 @@ export async function getStakingScoreStatus( } } -/** - * 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 { - try { - const status = await getStakingScoreStatus(peopleApi, address); - - if (!status.isTracking) { - return 0; // User hasn't started score tracking - } - - // 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; - } - } - - // 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; - } - - // Duration multiplier - const MONTH_IN_BLOCKS = 30 * 24 * 60 * 10; // ~432000 blocks per month - let durationMultiplier = 1.0; - - 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) { - console.error('Error fetching staking score:', error); - return 0; - } -} - /** * Start staking score tracking * Calls: stakingScore.startScoreTracking() @@ -285,72 +218,34 @@ export async function getTikiScore( // ======================================== /** - * Fetch all scores for a user in one call - * All scores come from People Chain except staking amount (Relay Chain) - * Staking score uses frontend fallback if on-chain pallet is not available - * - * @param peopleApi - People Chain API (trust, referral, stakingScore, tiki pallets) - * @param address - User's blockchain address - * @param relayApi - Optional Relay Chain API (for staking.ledger amount and frontend fallback) + * Fetch all scores for a user from People Chain + * Trust pallet computes composite score on-chain (includes staking, referral, tiki, perwerde) */ export async function getAllScores( - peopleApi: ApiPromise, - address: string, - relayApi?: ApiPromise + peopleApi: ApiPromise | null, + address: string ): Promise { + if (!peopleApi || !address) { + return { trustScore: 0, referralScore: 0, stakingScore: 0, tikiScore: 0, totalScore: 0 }; + } + try { - if (!address) { - return { - trustScore: 0, - referralScore: 0, - stakingScore: 0, - tikiScore: 0, - totalScore: 0 - }; - } - - // Fetch all scores in parallel - const scorePromises: Promise[] = []; - - // Trust and referral scores from People Chain - if (peopleApi) { - scorePromises.push(getTrustScore(peopleApi, address)); - scorePromises.push(getReferralScore(peopleApi, address)); - scorePromises.push(getTikiScore(peopleApi, address)); - } else { - scorePromises.push(Promise.resolve(0)); - scorePromises.push(Promise.resolve(0)); - scorePromises.push(Promise.resolve(0)); - } - - // Staking score with frontend fallback - const stakingScorePromise = relayApi - ? getStakingScoreWithFallback(peopleApi, relayApi, address).then(r => r.score) - : Promise.resolve(0); - - const [trustScore, referralScore, tikiScore, stakingScore] = await Promise.all([ - ...scorePromises, - stakingScorePromise + const [trustScore, referralScore, tikiScore] = await Promise.all([ + getTrustScore(peopleApi, address), + getReferralScore(peopleApi, address), + getTikiScore(peopleApi, address), ]); - const totalScore = trustScore + referralScore + stakingScore + tikiScore; - return { trustScore, referralScore, - stakingScore, + stakingScore: 0, // Trust pallet already includes staking tikiScore, - totalScore + totalScore: trustScore, // Trust score = composite score (on-chain calculated) }; } catch (error) { - console.error('Error fetching all scores:', error); - return { - trustScore: 0, - referralScore: 0, - stakingScore: 0, - tikiScore: 0, - totalScore: 0 - }; + console.error('Error fetching scores:', error); + return { trustScore: 0, referralScore: 0, stakingScore: 0, tikiScore: 0, totalScore: 0 }; } } @@ -397,283 +292,8 @@ export function formatDuration(blocks: number): string { } // ======================================== -// FRONTEND STAKING SCORE (Fallback) +// CITIZENSHIP & PERWERDE // ======================================== -// Until runtime upgrade deploys, calculate staking score -// directly from Relay Chain data without People Chain pallet - -const STAKING_SCORE_STORAGE_KEY = 'pez_staking_score_tracking'; -const UNITS = 1_000_000_000_000; // 10^12 -const DAY_IN_MS = 24 * 60 * 60 * 1000; -const MONTH_IN_MS = 30 * DAY_IN_MS; - -interface StakingTrackingData { - [address: string]: { - startTime: number; // Unix timestamp in ms - lastChecked: number; - lastStakeAmount: string; // Store as string to preserve precision - }; -} - -/** - * Get tracking data from localStorage - */ -function getStakingTrackingData(): StakingTrackingData { - if (typeof window === 'undefined') return {}; - try { - const stored = localStorage.getItem(STAKING_SCORE_STORAGE_KEY); - return stored ? JSON.parse(stored) : {}; - } catch { - return {}; - } -} - -/** - * Save tracking data to localStorage - */ -function saveStakingTrackingData(data: StakingTrackingData): void { - if (typeof window === 'undefined') return; - try { - localStorage.setItem(STAKING_SCORE_STORAGE_KEY, JSON.stringify(data)); - } catch (err) { - console.error('Failed to save staking tracking data:', err); - } -} - -/** - * Fetch staking details directly from Relay Chain - */ -export async function fetchRelayStakingDetails( - relayApi: ApiPromise, - address: string -): Promise<{ stakedAmount: bigint; nominationsCount: number } | null> { - try { - if (!relayApi?.query?.staking) return null; - - // Try to get ledger directly (if address is controller) - let ledger = await relayApi.query.staking.ledger?.(address); - let stashAddress = address; - - // If no ledger, check if this is a stash account - if (!ledger || ledger.isEmpty || ledger.isNone) { - const bonded = await relayApi.query.staking.bonded?.(address); - if (bonded && !bonded.isEmpty && !bonded.isNone) { - // This is a stash, get controller's ledger - const controller = bonded.toString(); - ledger = await relayApi.query.staking.ledger?.(controller); - stashAddress = address; - } - } - - if (!ledger || ledger.isEmpty || ledger.isNone) { - return null; - } - - // Parse ledger data - const ledgerJson = ledger.toJSON() as { active?: string | number }; - const active = BigInt(ledgerJson?.active || 0); - - // Get nominations - const nominations = await relayApi.query.staking.nominators?.(stashAddress); - const nominationsJson = nominations?.toJSON() as { targets?: unknown[] } | null; - const nominationsCount = nominationsJson?.targets?.length || 0; - - return { - stakedAmount: active, - nominationsCount - }; - } catch (err) { - console.error('Failed to fetch relay staking details:', err); - return null; - } -} - -/** - * Calculate base score from staked HEZ amount (matching pallet algorithm) - */ -function calculateBaseStakingScore(stakedHez: number): number { - if (stakedHez <= 0) return 0; - if (stakedHez <= 100) return 20; - if (stakedHez <= 250) return 30; - if (stakedHez <= 750) return 40; - return 50; // 751+ HEZ -} - -/** - * Get time multiplier based on months staked (matching pallet algorithm) - */ -function getStakingTimeMultiplier(monthsStaked: number): number { - if (monthsStaked >= 12) return 2.0; - if (monthsStaked >= 6) return 1.7; - if (monthsStaked >= 3) return 1.4; - if (monthsStaked >= 1) return 1.2; - return 1.0; -} - -export interface FrontendStakingScoreResult { - score: number; - stakedAmount: bigint; - stakedHez: number; - trackingStarted: Date | null; - monthsStaked: number; - timeMultiplier: number; - nominationsCount: number; - isFromFrontend: boolean; // true = frontend fallback, false = on-chain - needsRefresh: boolean; -} - -/** - * Get staking score using frontend fallback - * This bypasses the broken People Chain pallet and reads directly from Relay Chain - * - * @param relayApi - Relay Chain API (staking pallet) - * @param address - User's blockchain address - */ -export async function getFrontendStakingScore( - relayApi: ApiPromise, - address: string -): Promise { - const emptyResult: FrontendStakingScoreResult = { - score: 0, - stakedAmount: 0n, - stakedHez: 0, - trackingStarted: null, - monthsStaked: 0, - timeMultiplier: 1.0, - nominationsCount: 0, - isFromFrontend: true, - needsRefresh: false - }; - - if (!relayApi || !address) return emptyResult; - - // Fetch staking details from Relay Chain - const details = await fetchRelayStakingDetails(relayApi, address); - - if (!details || details.stakedAmount === 0n) { - return emptyResult; - } - - // Get or initialize tracking data - const trackingData = getStakingTrackingData(); - const now = Date.now(); - - if (!trackingData[address]) { - // First time seeing stake - start tracking - trackingData[address] = { - startTime: now, - lastChecked: now, - lastStakeAmount: details.stakedAmount.toString() - }; - saveStakingTrackingData(trackingData); - } else { - // Update last checked time and stake amount - trackingData[address].lastChecked = now; - trackingData[address].lastStakeAmount = details.stakedAmount.toString(); - saveStakingTrackingData(trackingData); - } - - const userTracking = trackingData[address]; - const trackingStarted = new Date(userTracking.startTime); - const msStaked = now - userTracking.startTime; - const monthsStaked = Math.floor(msStaked / MONTH_IN_MS); - - // Calculate score (matching pallet algorithm) - const stakedHez = Number(details.stakedAmount) / UNITS; - const baseScore = calculateBaseStakingScore(stakedHez); - const timeMultiplier = getStakingTimeMultiplier(monthsStaked); - const finalScore = Math.min(Math.floor(baseScore * timeMultiplier), 100); - - // Check if needs refresh (older than 24 hours) - const needsRefresh = now - userTracking.lastChecked > DAY_IN_MS; - - return { - score: finalScore, - stakedAmount: details.stakedAmount, - stakedHez, - trackingStarted, - monthsStaked, - timeMultiplier, - nominationsCount: details.nominationsCount, - isFromFrontend: true, - needsRefresh - }; -} - -/** - * Get staking score with frontend fallback - * First tries on-chain pallet, falls back to frontend calculation if it fails - * - * @param peopleApi - People Chain API (optional, for on-chain score) - * @param relayApi - Relay Chain API (required for stake data) - * @param address - User's blockchain address - */ -export async function getStakingScoreWithFallback( - peopleApi: ApiPromise | null, - relayApi: ApiPromise, - address: string -): Promise { - // First try on-chain score from People Chain - if (peopleApi) { - try { - const status = await getStakingScoreStatus(peopleApi, address); - if (status.isTracking && status.startBlock) { - // On-chain tracking is active, use it - const onChainScore = await getStakingScore(peopleApi, address, relayApi); - if (onChainScore > 0) { - const details = await fetchRelayStakingDetails(relayApi, address); - return { - score: onChainScore, - stakedAmount: details?.stakedAmount || 0n, - stakedHez: details ? Number(details.stakedAmount) / UNITS : 0, - trackingStarted: new Date(status.startBlock * 6000), // Approximate - monthsStaked: Math.floor(status.durationBlocks / (30 * 24 * 60 * 10)), - timeMultiplier: 1.0, // Already applied in on-chain score - nominationsCount: details?.nominationsCount || 0, - isFromFrontend: false, - needsRefresh: false - }; - } - } - } catch (err) { - console.log('On-chain staking score not available, using frontend fallback'); - } - } - - // Fall back to frontend calculation - return getFrontendStakingScore(relayApi, address); -} - -/** - * Format staked amount for display - */ -export function formatStakedAmount(amount: bigint): string { - const hez = Number(amount) / UNITS; - if (hez >= 1000000) return `${(hez / 1000000).toFixed(2)}M`; - if (hez >= 1000) return `${(hez / 1000).toFixed(2)}K`; - return hez.toFixed(2); -} - -// ======================================== -// FRONTEND TRUST SCORE (Fallback) -// ======================================== -// Until runtime upgrade, calculate trust score using frontend fallback -// Formula from pezpallet-trust: -// weighted_sum = staking*100 + referral*300 + perwerde*300 + tiki*300 -// trust_score = (staking * weighted_sum) / 100 - -const SCORE_MULTIPLIER_BASE = 100; - -export interface FrontendTrustScoreResult { - trustScore: number; - stakingScore: number; - referralScore: number; - perwerdeScore: number; - tikiScore: number; - weightedSum: number; - isFromFrontend: boolean; - isCitizen: boolean; -} /** * Check if user is a citizen (KYC approved) @@ -717,7 +337,6 @@ export async function getPerwerdeScore( } // Try to get user's completed courses/certifications - // The exact storage depends on pallet implementation if (peopleApi.query.perwerde.userScores) { const score = await peopleApi.query.perwerde.userScores(address); if (!score.isEmpty) { @@ -741,134 +360,3 @@ export async function getPerwerdeScore( return 0; } } - -/** - * Calculate trust score using frontend fallback - * Uses the same formula as pezpallet-trust - * - * @param peopleApi - People Chain API (optional) - * @param relayApi - Relay Chain API (required for staking data) - * @param address - User's blockchain address - */ -export async function getFrontendTrustScore( - peopleApi: ApiPromise | null, - relayApi: ApiPromise, - address: string -): Promise { - const emptyResult: FrontendTrustScoreResult = { - trustScore: 0, - stakingScore: 0, - referralScore: 0, - perwerdeScore: 0, - tikiScore: 0, - weightedSum: 0, - isFromFrontend: true, - isCitizen: false - }; - - if (!address) return emptyResult; - - // Check citizenship status - const isCitizen = await checkCitizenshipStatus(peopleApi, address); - - // Get component scores in parallel - const [stakingResult, referralScore, perwerdeScore, tikiScore] = await Promise.all([ - getFrontendStakingScore(relayApi, address), - peopleApi ? getReferralScore(peopleApi, address) : Promise.resolve(0), - getPerwerdeScore(peopleApi, address), - peopleApi ? getTikiScore(peopleApi, address) : Promise.resolve(0) - ]); - - const stakingScore = stakingResult.score; - - console.log('🔍 getFrontendTrustScore debug:', { - address: address.slice(0, 8) + '...', - stakingScore, - referralScore, - perwerdeScore, - tikiScore, - stakedHez: stakingResult.stakedHez, - }); - - // Ger staking 0 be, trust jî 0 be (matches pallet logic) - if (stakingScore === 0) { - return { - ...emptyResult, - referralScore, - perwerdeScore, - tikiScore, - isCitizen - }; - } - - // Calculate weighted sum (matching pallet formula) - const weightedSum = - stakingScore * 100 + - referralScore * 300 + - perwerdeScore * 300 + - tikiScore * 300; - - // Calculate final trust score - // trust_score = (staking * weighted_sum) / 100 - const trustScore = Math.floor((stakingScore * weightedSum) / SCORE_MULTIPLIER_BASE); - - console.log('✅ Trust score calculated:', { weightedSum, trustScore }); - - return { - trustScore, - stakingScore, - referralScore, - perwerdeScore, - tikiScore, - weightedSum, - isFromFrontend: true, - isCitizen - }; -} - -/** - * Get trust score with frontend fallback - * NOTE: Until runtime upgrade, always use frontend fallback - * On-chain trust pallet exists but doesn't calculate correctly yet - */ -export async function getTrustScoreWithFallback( - peopleApi: ApiPromise | null, - relayApi: ApiPromise, - address: string -): Promise { - // Always use frontend calculation until runtime upgrade - // The on-chain trust pallet exists but StakingInfoProvider returns None - // which causes trust score to be 0 even when user has stake - return getFrontendTrustScore(peopleApi, relayApi, address); -} - -/** - * Get all scores with frontend fallback for staking and trust - */ -export async function getAllScoresWithFallback( - peopleApi: ApiPromise | null, - relayApi: ApiPromise, - address: string -): Promise { - if (!address) { - return { - trustScore: 0, - referralScore: 0, - stakingScore: 0, - tikiScore: 0, - totalScore: 0, - isFromFrontend: true - }; - } - - const trustResult = await getTrustScoreWithFallback(peopleApi, relayApi, address); - - return { - trustScore: trustResult.trustScore, - referralScore: trustResult.referralScore, - stakingScore: trustResult.stakingScore, - tikiScore: trustResult.tikiScore, - totalScore: trustResult.trustScore, // Trust score IS the total score - isFromFrontend: trustResult.isFromFrontend - }; -} diff --git a/web/src/components/AccountBalance.tsx b/web/src/components/AccountBalance.tsx index be5feac7..5db770c6 100644 --- a/web/src/components/AccountBalance.tsx +++ b/web/src/components/AccountBalance.tsx @@ -8,7 +8,7 @@ import { AddTokenModal } from './AddTokenModal'; import { TransferModal } from './TransferModal'; import { XCMTeleportModal } from './XCMTeleportModal'; import { LPStakeModal } from './LPStakeModal'; -import { getAllScoresWithFallback, type UserScores } from '@pezkuwi/lib/scores'; +import { getAllScores, type UserScores } from '@pezkuwi/lib/scores'; interface TokenBalance { assetId: number; @@ -570,11 +570,7 @@ export const AccountBalance: React.FC = () => { setLoadingScores(true); try { // Use fallback function: peopleApi for on-chain scores, api (Relay) for staking data - const userScores = await getAllScoresWithFallback( - peopleApi || null, // People Chain for referral, tiki, perwerde - api, // Relay Chain for staking data - selectedAccount.address - ); + const userScores = await getAllScores(peopleApi || null, selectedAccount.address); setScores(userScores); } catch (err) { if (import.meta.env.DEV) console.error('Failed to fetch scores:', err); diff --git a/web/src/components/HeroSection.tsx b/web/src/components/HeroSection.tsx index da580b0b..bdfdacf2 100644 --- a/web/src/components/HeroSection.tsx +++ b/web/src/components/HeroSection.tsx @@ -4,7 +4,7 @@ import { ChevronRight, Shield } from 'lucide-react'; import { usePezkuwi } from '../contexts/PezkuwiContext'; import { useWallet } from '../contexts/WalletContext'; // Import useWallet import { formatBalance } from '@pezkuwi/lib/wallet'; -import { getTrustScoreWithFallback } from '@pezkuwi/lib/scores'; +import { getTrustScore } from '@pezkuwi/lib/scores'; const HeroSection: React.FC = () => { const { t } = useTranslation(); @@ -25,12 +25,9 @@ const HeroSection: React.FC = () => { if (selectedAccount?.address) { try { // Use frontend fallback for trust score - const trustResult = await getTrustScoreWithFallback( - peopleApi || null, - api, - selectedAccount.address - ); - currentTrustScore = trustResult.trustScore; + if (peopleApi) { + currentTrustScore = await getTrustScore(peopleApi, selectedAccount.address); + } } catch (err) { if (import.meta.env.DEV) console.warn('Failed to fetch trust score:', err); currentTrustScore = 0; diff --git a/web/src/components/wallet/WalletModal.tsx b/web/src/components/wallet/WalletModal.tsx index 76140391..d7798db7 100644 --- a/web/src/components/wallet/WalletModal.tsx +++ b/web/src/components/wallet/WalletModal.tsx @@ -26,6 +26,7 @@ export const WalletModal: React.FC = ({ isOpen, onClose }) => disconnectWallet, api, isApiReady, + peopleApi, error } = usePezkuwi(); @@ -77,7 +78,7 @@ export const WalletModal: React.FC = ({ isOpen, onClose }) => setLoadingScores(true); try { - const userScores = await getAllScores(api, selectedAccount.address); + const userScores = await getAllScores(peopleApi || null, selectedAccount.address); setScores(userScores); } catch (err) { if (import.meta.env.DEV) console.error('Failed to fetch scores:', err); @@ -94,7 +95,7 @@ export const WalletModal: React.FC = ({ isOpen, onClose }) => }; fetchAllScores(); - }, [api, isApiReady, selectedAccount]); + }, [api, isApiReady, peopleApi, selectedAccount]); return ( diff --git a/web/src/pages/Dashboard.tsx b/web/src/pages/Dashboard.tsx index ec7213f1..a8dd7f3f 100644 --- a/web/src/pages/Dashboard.tsx +++ b/web/src/pages/Dashboard.tsx @@ -10,7 +10,7 @@ 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 { useToast } from '@/hooks/use-toast'; import { fetchUserTikis, getPrimaryRole, getTikiDisplayName, getTikiColor, getTikiEmoji, getUserRoleCategories, getAllTikiNFTDetails, generateCitizenNumber, type TikiNFTDetails } from '@pezkuwi/lib/tiki'; -import { getAllScoresWithFallback, getStakingScoreStatus, startScoreTracking, type UserScores, type StakingScoreStatus, formatDuration } from '@pezkuwi/lib/scores'; +import { getAllScores, getStakingScoreStatus, startScoreTracking, type UserScores, type StakingScoreStatus, formatDuration } from '@pezkuwi/lib/scores'; import { web3FromAddress } from '@pezkuwi/extension-dapp'; import { getKycStatus } from '@pezkuwi/lib/kyc'; import { ReferralDashboard } from '@/components/referral/ReferralDashboard'; @@ -111,7 +111,7 @@ export default function Dashboard() { // Fetch all scores with frontend fallback (until runtime upgrade) // - Trust, referral, tiki: People Chain (on-chain) // - Staking: Relay Chain with frontend fallback - const allScores = await getAllScoresWithFallback(peopleApi, api, selectedAccount.address); + const allScores = await getAllScores(peopleApi, selectedAccount.address); setScores(allScores); // Fetch staking score tracking status