Merge pull request #50 from pezkuwichain/fix/rewards-without-recording

rewards: claim what the chain pays, without recording a score first
This commit is contained in:
SatoshiQaziMuhammed authored and GitHub committed 2026-10-25 20:29:14 +04:00
commit 1f0b687e6e
9 files changed
+46 -197

No files matched your search

-6
View File
@@ -140,11 +140,6 @@ const ar: Translations = {
startingTracking: 'جاري بدء التتبع...',
trackingStarted: 'بدأ التتبع!',
trackingFailed: 'فشل بدء التتبع',
recordTrustScore: 'تسجيل نقاط الثقة',
recordTrustDesc: 'سجل نقاطك لمكافآت هذه الفترة',
recordingTrustScore: 'جاري تسجيل نقاط الثقة...',
trustScoreRecorded: 'تم تسجيل نقاط الثقة!',
trustScoreRecordFailed: 'فشل تسجيل النقاط',
citizenCountTitle: 'الأكراد في العالم',
citizenCountDesc: 'المواطنون المسجلون على PezkuwiChain',
beCitizen: 'كن مواطناً',
@@ -153,7 +148,6 @@ const ar: Translations = {
claimPeriod: 'فترة المطالبة',
epochClosed: 'مغلقة',
epochOpen: 'مفتوحة',
scoreRecorded: 'مسجّل',
claimablePez: 'PEZ قابل للمطالبة',
claim: 'طالب',
claimAll: 'طالب بالكل',
-6
View File
@@ -141,11 +141,6 @@ const ckb: Translations = {
startingTracking: 'شوێنکەوتن دەست پێ دەکات...',
trackingStarted: 'شوێنکەوتن دەست پێ کرد!',
trackingFailed: 'دەست پێکردنی شوێنکەوتن سەرنەکەوت',
recordTrustScore: 'خاڵی متمانە تۆمار بکە',
recordTrustDesc: 'خاڵەکەت بۆ خەڵاتەکانی ئەم سەردەمە تۆمار بکە',
recordingTrustScore: 'خاڵ تۆمار دەکرێت...',
trustScoreRecorded: 'خاڵی متمانە تۆمار کرا!',
trustScoreRecordFailed: 'تۆمارکردنی خاڵ سەرنەکەوت',
citizenCountTitle: 'ژمارەی کورد لە جیهان',
citizenCountDesc: 'هاوڵاتییانی تۆمارکراو لە PezkuwiChain',
beCitizen: 'ببە هاوڵاتی',
@@ -154,7 +149,6 @@ const ckb: Translations = {
claimPeriod: 'ماوەی داواکردن',
epochClosed: 'داخراوە',
epochOpen: 'کراوەیە',
scoreRecorded: 'تۆمارکرا',
claimablePez: 'PEZ ی داواکراو',
claim: 'داوا بکە',
claimAll: 'هەموو داوا بکە',
-6
View File
@@ -140,11 +140,6 @@ const en: Translations = {
startingTracking: 'Starting tracking...',
trackingStarted: 'Tracking started!',
trackingFailed: 'Failed to start tracking',
recordTrustScore: 'Record Trust Score',
recordTrustDesc: "Record your score for this epoch's rewards",
recordingTrustScore: 'Recording trust score...',
trustScoreRecorded: 'Trust score recorded!',
trustScoreRecordFailed: 'Failed to record score',
citizenCountTitle: 'Kurds in the World',
citizenCountDesc: 'Citizens registered on PezkuwiChain',
beCitizen: 'Be Citizen',
@@ -153,7 +148,6 @@ const en: Translations = {
claimPeriod: 'Claim Period',
epochClosed: 'Closed',
epochOpen: 'Open',
scoreRecorded: 'Recorded',
claimablePez: 'Claimable PEZ',
claim: 'Claim',
claimAll: 'Claim All',
-6
View File
@@ -140,11 +140,6 @@ const fa: Translations = {
startingTracking: 'در حال شروع ردیابی...',
trackingStarted: 'ردیابی شروع شد!',
trackingFailed: 'شروع ردیابی ناموفق بود',
recordTrustScore: 'ثبت امتیاز اعتماد',
recordTrustDesc: 'امتیاز خود را برای پاداش‌های این دوره ثبت کنید',
recordingTrustScore: 'در حال ثبت امتیاز اعتماد...',
trustScoreRecorded: 'امتیاز اعتماد ثبت شد!',
trustScoreRecordFailed: 'ثبت امتیاز ناموفق بود',
citizenCountTitle: 'کوردها در جهان',
citizenCountDesc: 'شهروندان ثبت‌شده در PezkuwiChain',
beCitizen: 'شهروند شوید',
@@ -153,7 +148,6 @@ const fa: Translations = {
claimPeriod: 'دوره مطالبه',
epochClosed: 'بسته',
epochOpen: 'باز',
scoreRecorded: 'ثبت شد',
claimablePez: 'PEZ قابل مطالبه',
claim: 'مطالبه',
claimAll: 'مطالبه همه',
-6
View File
@@ -145,11 +145,6 @@ const krd: Translations = {
startingTracking: 'Şopandin tê destpêkirin...',
trackingStarted: 'Şopandin dest pê kir!',
trackingFailed: 'Destpêkirina şopandinê biserneket',
recordTrustScore: 'Pûana Pêbaweriyê Tomar Bike',
recordTrustDesc: 'Pûana xwe ji bo xelatên vê serdemê tomar bike',
recordingTrustScore: 'Pûan tê tomarkirin...',
trustScoreRecorded: 'Pûana pêbaweriyê hat tomarkirin!',
trustScoreRecordFailed: 'Tomarkirina pûanê biserneket',
citizenCountTitle: 'Hejmara Kurd Le Cîhanê',
citizenCountDesc: 'Welatiyên ku li ser PezkuwiChain qeyd bûne',
beCitizen: 'Bibe Welatî',
@@ -158,7 +153,6 @@ const krd: Translations = {
claimPeriod: 'Dema Daxwazkirinê',
epochClosed: 'Girtî',
epochOpen: 'Vekirî',
scoreRecorded: 'Hat tomarkirin',
claimablePez: 'PEZ yên Daxwazkir',
claim: 'Daxwaz Bike',
claimAll: 'Hemûyan Daxwaz Bike',
-6
View File
@@ -140,11 +140,6 @@ const tr: Translations = {
startingTracking: 'Takip başlatılıyor...',
trackingStarted: 'Takip başlatıldı!',
trackingFailed: 'Takip başlatılamadı',
recordTrustScore: 'Güven Puanını Kaydet',
recordTrustDesc: 'Bu dönemin ödülleri için puanınızı kaydedin',
recordingTrustScore: 'Güven puanı kaydediliyor...',
trustScoreRecorded: 'Güven puanı kaydedildi!',
trustScoreRecordFailed: 'Puan kaydedilemedi',
citizenCountTitle: 'Dünyadaki Kürt Sayısı',
citizenCountDesc: "PezkuwiChain'de kayıtlı vatandaşlar",
beCitizen: 'Vatandaş Ol',
@@ -153,7 +148,6 @@ const tr: Translations = {
claimPeriod: 'Talep Dönemi',
epochClosed: 'Kapalı',
epochOpen: 'Açık',
scoreRecorded: 'Kaydedildi',
claimablePez: 'Talep Edilebilir PEZ',
claim: 'Talep Et',
claimAll: 'Tümünü Talep Et',
-6
View File
@@ -142,11 +142,6 @@ export interface Translations {
startingTracking: string;
trackingStarted: string;
trackingFailed: string;
recordTrustScore: string;
recordTrustDesc: string;
recordingTrustScore: string;
trustScoreRecorded: string;
trustScoreRecordFailed: string;
citizenCountTitle: string;
citizenCountDesc: string;
beCitizen: string;
@@ -155,7 +150,6 @@ export interface Translations {
claimPeriod: string;
epochClosed: string;
epochOpen: string;
scoreRecorded: string;
claimablePez: string;
claim: string;
claimAll: string;
+43 -108
View File
@@ -41,8 +41,6 @@ export type EpochStatus = 'Open' | 'ClaimPeriod' | 'Closed';
export interface PezRewardInfo {
currentEpoch: number;
epochStatus: EpochStatus;
hasRecordedThisEpoch: boolean;
userScoreCurrentEpoch: number;
claimableRewards: { epoch: number; amount: string }[];
totalClaimable: string;
hasPendingClaim: boolean;
@@ -508,66 +506,6 @@ export async function startScoreTracking(
}
}
/**
* Record trust score for current epoch on People Chain.
* Calls pezRewards.recordTrustScore() - no arguments.
* Required for PEZ epoch rewards.
*/
export async function recordTrustScore(
peopleApi: ApiPromise,
keypair: KeyringPair
): Promise<{ success: boolean; error?: string }> {
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const tx = peopleApi.tx as any;
if (!tx?.pezRewards?.recordTrustScore) {
return { success: false, error: 'pezRewards pallet not available' };
}
const result = await new Promise<{ success: boolean; error?: string }>((resolve) => {
tx.pezRewards
.recordTrustScore()
.signAndSend(
keypair,
{ nonce: -1 },
({
status,
dispatchError,
}: {
status: {
isInBlock: boolean;
isFinalized: boolean;
};
dispatchError?: { isModule: boolean; asModule: unknown; toString: () => string };
}) => {
if (status.isInBlock || status.isFinalized) {
if (dispatchError) {
let errorMessage = 'recordTrustScore failed';
if (dispatchError.isModule) {
const decoded = peopleApi.registry.findMetaError(
dispatchError.asModule as Parameters<typeof peopleApi.registry.findMetaError>[0]
);
errorMessage = `${decoded.section}.${decoded.name}`;
}
resolve({ success: false, error: errorMessage });
return;
}
resolve({ success: true });
}
}
)
.catch((error: Error) => resolve({ success: false, error: error.message }));
});
return result;
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
}
// ========================================
// PEZ REWARDS (pezRewards pallet on People Chain)
// ========================================
@@ -616,66 +554,65 @@ export async function getPezRewards(
// Default to Open
}
// Check if user has recorded their score this epoch
let hasRecordedThisEpoch = false;
let userScoreCurrentEpoch = 0;
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const userScoreResult = await (peopleApi.query as any).pezRewards.userEpochScores(
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
// What a claim pays, worked out the way the chain does (`pezRewards::entitlement`). There
// is no score to record beforehand: the roll is held still from an epoch's finalisation
// to the end of its claim window, so the trust score now is the one the rate was set
// against, and a seat is paid if it is held now and was taken by the time of the roll.
const claimableRewards: { epoch: number; amount: string }[] = [];
let totalClaimable = BigInt(0);
const who = peopleApi.createType('AccountId', address).toString();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const query = peopleApi.query as any;
const trustScore = BigInt((await query.trust.trustScores(address)).toString());
// Seat: the Parlementer tiki, not expired, and the date the seat was taken.
let seatTakenAt: number | null = null;
try {
const tikis = ((await query.tiki.userTikis(address)).toJSON() ?? []) as string[];
if (tikis.includes('Parlementer')) {
const expiry = (await query.tiki.tikiExpiry(address, 'Parlementer')).toJSON() as
| number
| null;
const now = (await peopleApi.rpc.chain.getHeader()).number.toNumber();
if (expiry === null || now <= expiry) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const members = (await query.welati.parliamentMembers()).toJSON() as any[];
const member = members.find(
(m) => peopleApi.createType('AccountId', m.account).toString() === who
);
if (member) seatTakenAt = Number(member.electedAt ?? member.elected_at);
}
}
} catch {
// No seat
}
for (let i = Math.max(0, currentEpoch - 3); i < currentEpoch; i++) {
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const pastStatusResult = await (peopleApi.query as any).pezRewards.epochStatus(i);
const pastStatus = pastStatusResult.toString();
const pastStatus = (await query.pezRewards.epochStatus(i)).toString();
if (pastStatus !== 'ClaimPeriod') continue;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const claimedResult = await (peopleApi.query as any).pezRewards.claimedRewards(i, address);
const claimedResult = await query.pezRewards.claimedRewards(i, address);
if (claimedResult.isSome) continue;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const userScoreResult = await (peopleApi.query as any).pezRewards.userEpochScores(
i,
address
);
if (!userScoreResult.isSome) continue;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const epochPoolResult = await (peopleApi.query as any).pezRewards.epochRewardPools(i);
const epochPoolResult = await query.pezRewards.epochRewardPools(i);
if (!epochPoolResult.isSome) continue;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const epochPool = (epochPoolResult.unwrap() as any).toJSON();
const userScore = BigInt(
(userScoreResult.unwrap() as { toString: () => string }).toString()
);
const pool = (epochPoolResult.unwrap() as any).toJSON();
const rewardPerPoint = BigInt(
epochPool.rewardPerTrustPoint || epochPool.reward_per_trust_point || '0'
pool.rewardPerTrustPoint ?? pool.reward_per_trust_point ?? '0'
);
const seatShare = BigInt(pool.seatShare ?? pool.seat_share ?? '0');
const finalizedAt = Number(pool.finalizedAt ?? pool.finalized_at ?? 0);
const rewardAmount = userScore * rewardPerPoint;
const rewardFormatted = formatBalancePlanck(rewardAmount.toString());
let rewardAmount = trustScore * rewardPerPoint;
if (seatTakenAt !== null && seatTakenAt <= finalizedAt) rewardAmount += seatShare;
if (rewardAmount === BigInt(0)) continue;
if (parseFloat(rewardFormatted) > 0) {
claimableRewards.push({ epoch: i, amount: rewardFormatted });
totalClaimable += rewardAmount;
}
claimableRewards.push({ epoch: i, amount: formatBalancePlanck(rewardAmount.toString()) });
totalClaimable += rewardAmount;
} catch (err) {
console.warn(`Error checking epoch ${i} rewards:`, err);
}
@@ -684,8 +621,6 @@ export async function getPezRewards(
return {
currentEpoch,
epochStatus,
hasRecordedThisEpoch,
userScoreCurrentEpoch,
claimableRewards,
totalClaimable: formatBalancePlanck(totalClaimable.toString()),
hasPendingClaim: claimableRewards.length > 0,
+3 -47
View File
@@ -22,7 +22,6 @@ import {
Sparkles,
GraduationCap,
Play,
PenTool,
Globe2,
Clock,
Loader2,
@@ -38,7 +37,6 @@ import {
getAllScores,
getStakingScoreStatus,
startScoreTracking,
recordTrustScore,
getPezRewards,
claimPezReward,
formatDuration,
@@ -229,29 +227,6 @@ export function RewardsSection() {
}
};
const handleRecordTrustScore = async () => {
if (!peopleApi || !keypair) return;
setTrackingAnimationText(t('rewards.recordingTrustScore'));
setShowTrackingAnimation(true);
hapticImpact('medium');
try {
const result = await recordTrustScore(peopleApi, keypair);
if (result.success) {
hapticNotification('success');
showAlert(t('rewards.trustScoreRecorded'));
fetchUserScores();
} else {
hapticNotification('error');
showAlert(result.error || t('rewards.trustScoreRecordFailed'));
}
} catch (err) {
hapticNotification('error');
showAlert(err instanceof Error ? err.message : t('rewards.trustScoreRecordFailed'));
} finally {
setShowTrackingAnimation(false);
}
};
const handleClaimReward = async (epochIndex: number) => {
if (!peopleApi || !keypair) return;
setClaimingEpoch(epochIndex);
@@ -1055,7 +1030,8 @@ export function RewardsSection() {
</div>
</div>
{/* Start Tracking / Record Trust Score Button */}
{/* Start Tracking Button. Once tracking, there is nothing to do each month: the
chain holds every score still while an epoch is claimed. */}
{!stakingStatus?.isTracking ? (
<div className="bg-secondary/30 rounded-xl p-4 border border-border/50">
<button
@@ -1070,21 +1046,7 @@ export function RewardsSection() {
{t('rewards.startTrackingDesc')}
</p>
</div>
) : (
<div className="bg-secondary/30 rounded-xl p-4 border border-border/50">
<button
onClick={handleRecordTrustScore}
disabled={!keypair || !peopleApi}
className="w-full py-3 rounded-lg font-medium flex items-center justify-center gap-2 bg-gradient-to-r from-green-500 to-emerald-600 text-white hover:opacity-90 transition-all disabled:opacity-50"
>
<PenTool className="w-5 h-5" />
{t('rewards.recordTrustScore')}
</button>
<p className="text-xs text-muted-foreground text-center mt-2">
{t('rewards.recordTrustDesc')}
</p>
</div>
)}
) : null}
{/* PEZ Epoch Rewards */}
<div className="bg-secondary/30 rounded-xl p-4 border border-border/50">
@@ -1114,12 +1076,6 @@ export function RewardsSection() {
? t('rewards.epochClosed')
: t('rewards.epochOpen')}
</span>
{pezRewards?.hasRecordedThisEpoch && (
<span className="px-2 py-0.5 rounded-full text-xs font-medium bg-green-500/20 text-green-400">
<Check className="w-3 h-3 inline mr-1" />
{t('rewards.scoreRecorded')}
</span>
)}
</div>
{/* Claimable PEZ Rewards */}