mirror of
https://github.com/pezkuwichain/pezkuwi-telegram-miniapp.git
synced 2026-11-12 05:37:07 +00:00
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:
9 files changed
+46
-197
No files matched your search
@@ -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: 'طالب بالكل',
|
||||
|
||||
@@ -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: 'هەموو داوا بکە',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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: 'مطالبه همه',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
Reference in new issue
Block a user