rewards: claim what the chain pays, without recording a score first (#61)

* rewards: claim what the chain pays, without recording a score first

The People chain has no pezRewards.recordTrustScore and no userEpochScores since the payroll
started holding the roll still through each claim window: a claim pays the trust score at
claim time times the epoch's rate, plus the seat share for a Parlementer seat taken by the
time of the roll. The app still asked for the recorded score, so the claimable list was
always empty, and three screens offered a Record Trust Score button that signed a call that
does not exist.

The buttons, their handlers and texts, and the messages for an error and a success that can
no longer happen are removed; the claimable amount is worked out as the chain's entitlement
does, and the empty state shows in every epoch status. Past recordTrustScore extrinsics in
the transaction history keep their label.

* backend: body-parser 2.4.0 and ip-address 10.7.5

npm audit on the production dependencies: body-parser 2.0.0-2.3.0 (GHSA-27r3-jxmh-p828,
high, denial of service by trailing bytes after a compressed body) and ip-address <=10.7.3
(GHSA-rxp7-6hxx-x8q5, moderate). Lockfile only; npm audit --omit=dev finds nothing after.

* ops: lower the typecheck baseline to 335

Four TS2339 errors in shared/lib/scores.ts went with the userEpochScores code.

* web: dompurify 3.4.17 and zod 4.6.7

npm audit on the production dependencies: dompurify <=3.4.16 (critical: GHSA-cg2g-xh64-v4q7,
GHSA-mrv2-3pqr-j5jr, GHSA-c6gr-qvv4-5v3f) and zod <4.6.6 (high, GHSA-mrhx-c7xc-m3w5, fixed
only in 4.x). No code imports zod; it is here for abitype, whose peer range is
^3.22.0 || ^4.0.0. Typecheck ratchet, lint and the build are unchanged.
This commit is contained in:
SatoshiQaziMuhammed authored and GitHub committed 2026-10-26 02:37:20 +04:00
1 parent ac730481d9
commit 03e6db8461
15 files changed
+57 -351

No files matched your search

-8
View File
@@ -47,10 +47,6 @@ const ERROR_MESSAGES: Record<string, ErrorMessage> = {
en: 'You have already claimed your reward for this epoch.',
kmr: 'We berê xelata xwe ji bo vê epoch-ê wergirtiye.',
},
'pezRewards.NoTrustScoreForEpoch': {
en: 'No trust score recorded for this epoch. You must record your score before claiming.',
kmr: 'Ji bo vê epoch-ê skora emîniyê tomar nebûye. Pêşî divê skora xwe tomar bikî.',
},
'pezRewards.NoRewardToClaim': {
en: 'No reward available to claim for this epoch.',
kmr: 'Ji bo vê epoch-ê xelateke ku were wergirtin tune ye.',
@@ -453,10 +449,6 @@ export const SUCCESS_MESSAGES: Record<string, SuccessMessage> = {
},
// PEZ Rewards
'pezRewards.recorded': {
en: 'Trust score recorded for this epoch. Your score will be used for reward calculation.',
kmr: 'Skora emîniyê ji bo vê epoch-ê tomar bû. Skora we dê ji bo hesabkirina xelatê were bikaranîn.',
},
'pezRewards.claimed': {
en: '{{amount}} PEZ reward claimed successfully!',
kmr: '{{amount}} PEZ xelat bi serkeftî hate wergirtin!',
+26 -65
View File
@@ -45,8 +45,6 @@ export interface EpochRewardPool {
export interface PezRewardInfo {
currentEpoch: number;
epochStatus: EpochStatus;
hasRecordedThisEpoch: boolean;
userScoreCurrentEpoch: number;
claimableRewards: { epoch: number; amount: string }[];
totalClaimable: string;
hasPendingClaim: boolean;
@@ -512,7 +510,6 @@ export async function getPerwerdeScore(
* Uses correct storage query names from live People Chain metadata:
* - epochInfo() → epoch info (ValueQuery)
* - epochStatus(epoch) → Open | ClaimPeriod | Closed
* - userEpochScores(epoch, addr) → user's recorded score (OptionQuery)
* - claimedRewards(epoch, addr) → claimed reward amount (OptionQuery)
* - epochRewardPools(epoch) → reward pool info (OptionQuery)
*/
@@ -548,18 +545,27 @@ export async function getPezRewards(
// Default to Open if query fails
}
// Check if user has recorded their score this epoch
let hasRecordedThisEpoch = false;
let userScoreCurrentEpoch = 0;
// 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 who = peopleApi.createType('AccountId', address).toString();
const trustScore = BigInt((await peopleApi.query.trust.trustScores(address)).toString());
let seatTakenAt: number | null = null;
try {
const userScoreResult = await peopleApi.query.pezRewards.userEpochScores(currentEpoch, address);
if (userScoreResult.isSome) {
hasRecordedThisEpoch = true;
const scoreCodec = userScoreResult.unwrap() as { toString: () => string };
userScoreCurrentEpoch = Number(scoreCodec.toString());
const tikis = ((await peopleApi.query.tiki.userTikis(address)).toJSON() ?? []) as string[];
if (tikis.includes('Parlementer')) {
const expiry = (await peopleApi.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 peopleApi.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 {
// User hasn't recorded
// No seat
}
// Check for claimable rewards from completed epochs
@@ -577,10 +583,6 @@ export async function getPezRewards(
const claimedResult = await peopleApi.query.pezRewards.claimedRewards(i, address);
if (claimedResult.isSome) continue;
// Check if user has a score for this epoch
const userScoreResult = await peopleApi.query.pezRewards.userEpochScores(i, address);
if (!userScoreResult.isSome) continue;
// Get epoch reward pool
const epochPoolResult = await peopleApi.query.pezRewards.epochRewardPools(i);
if (!epochPoolResult.isSome) continue;
@@ -588,17 +590,16 @@ export async function getPezRewards(
const epochPoolCodec = epochPoolResult.unwrap() as { toJSON: () => unknown };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const epochPool = epochPoolCodec.toJSON() as any;
const userScoreCodec = userScoreResult.unwrap() as { toString: () => string };
const userScore = BigInt(userScoreCodec.toString());
const rewardPerPoint = BigInt(epochPool.rewardPerTrustPoint || epochPool.reward_per_trust_point || '0');
const rewardPerPoint = BigInt(epochPool.rewardPerTrustPoint ?? epochPool.reward_per_trust_point ?? '0');
const seatShare = BigInt(epochPool.seatShare ?? epochPool.seat_share ?? '0');
const finalizedAt = Number(epochPool.finalizedAt ?? epochPool.finalized_at ?? 0);
const rewardAmount = userScore * rewardPerPoint;
const rewardFormatted = formatBalance(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: formatBalance(rewardAmount.toString()) });
totalClaimable += rewardAmount;
} catch (err) {
console.warn(`Error checking epoch ${i} rewards:`, err);
}
@@ -607,8 +608,6 @@ export async function getPezRewards(
return {
currentEpoch,
epochStatus,
hasRecordedThisEpoch,
userScoreCurrentEpoch,
claimableRewards,
totalClaimable: formatBalance(totalClaimable.toString()),
hasPendingClaim: claimableRewards.length > 0,
@@ -619,44 +618,6 @@ export async function getPezRewards(
}
}
/**
* Record trust score for the current epoch
* Calls: pezRewards.recordTrustScore()
*/
export async function recordTrustScore(
peopleApi: ApiPromise,
address: string,
signer: any
): Promise<{ success: boolean; error?: string }> {
try {
if (!peopleApi?.tx?.pezRewards?.recordTrustScore) {
return { success: false, error: 'pezRewards pallet not available' };
}
const tx = peopleApi.tx.pezRewards.recordTrustScore();
return new Promise((resolve) => {
tx.signAndSend(address, { signer }, ({ status, dispatchError }: any) => {
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}` });
} else {
resolve({ success: false, error: dispatchError.toString() });
}
} else {
resolve({ success: true });
}
}
});
});
} catch (error) {
console.error('Error recording trust score:', error);
return { success: false, error: error instanceof Error ? error.message : 'Unknown error' };
}
}
/**
* Claim PEZ reward for a specific epoch
* Calls: pezRewards.claimReward(epochIndex)