Skip to content

Commit

Permalink
Fix claim and payout
Browse files Browse the repository at this point in the history
  • Loading branch information
kryzasada committed Nov 11, 2024
1 parent 5565481 commit 651b9f8
Show file tree
Hide file tree
Showing 9 changed files with 99 additions and 67 deletions.
2 changes: 1 addition & 1 deletion src/contexts/Payouts/Utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export const setLocalUnclaimedPayouts = (
network: NetworkName,
era: string,
who: string,
unclaimdPayouts: Record<string, string>,
unclaimdPayouts: Record<string, [number, string]>,
endEra: string
) => {
const current = JSON.parse(
Expand Down
92 changes: 48 additions & 44 deletions src/contexts/Payouts/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,51 +146,47 @@ export const PayoutsProvider = ({
new BigNumber(b).minus(a).toNumber()
);

// Helper function to check which eras a validator was exposed in.
const validatorExposedEras = (validator: string) => {
const exposedEras: string[] = [];
for (const era of erasToCheck)
if (
Object.values(
Object.keys(getLocalEraExposure(network.name, era, activeAccount))
)?.[0] === validator
)
exposedEras.push(era);
return exposedEras;
};

// Fetch controllers in order to query ledgers.
const bondedResults =
await api.query.staking.bonded.multi<AnyApi>(uniqueValidators);
const validatorControllers: Record<string, string> = {};
for (let i = 0; i < bondedResults.length; i++) {
const ctlr = bondedResults[i].unwrapOr(null);
if (ctlr) validatorControllers[uniqueValidators[i]] = ctlr;
if (ctlr) {
validatorControllers[uniqueValidators[i]] = ctlr;
}
}

// Fetch ledgers to determine which eras have not yet been claimed per validator. Only includes
// eras that are in `erasToCheck`.
const ledgerResults = await api.query.staking.ledger.multi<AnyApi>(
Object.values(validatorControllers)
);
const unclaimedRewards: Record<string, string[]> = {};
for (const ledgerResult of ledgerResults) {
const ledger = ledgerResult.unwrapOr(null)?.toHuman();
if (ledger) {
// get claimed eras within `erasToCheck`.
const erasClaimed = ledger.legacyClaimedRewards
.map((e: string) => rmCommas(e))
.filter(
(e: string) =>
new BigNumber(e).isLessThanOrEqualTo(startEra) &&
new BigNumber(e).isGreaterThanOrEqualTo(endEra)
);

// filter eras yet to be claimed
unclaimedRewards[ledger.stash] = erasToCheck
.map((e) => e.toString())
.filter((r: string) => validatorExposedEras(ledger.stash).includes(r))
.filter((r: string) => !erasClaimed.includes(r));

// Accumulate calls to fetch unclaimed rewards for each era for all validators.
const unclaimedRewardsEntries = erasToCheck
.map((era) => uniqueValidators.map((v) => [era, v]))
.flat();

const results = await Promise.all(
unclaimedRewardsEntries.map(([era, v]) =>
api.query.staking.claimedRewards<AnyApi>(era, v)
)
);

for (let i = 0; i < results.length; i++) {
const pages = results[i].toHuman() || [];
const era = unclaimedRewardsEntries[i][0];
const validator = unclaimedRewardsEntries[i][1];
const exposure = getLocalEraExposure(network.name, era, activeAccount);
const exposedPage =
exposure?.[validator]?.exposedPage !== undefined
? String(exposure[validator].exposedPage)
: undefined;

// Add to `unclaimedRewards` if payout page has not yet been claimed.
if (!pages.includes(exposedPage)) {
if (unclaimedRewards?.[validator]) {
unclaimedRewards[validator].push(era);
} else {
unclaimedRewards[validator] = [era];
}
}
}

Expand All @@ -199,15 +195,19 @@ export const PayoutsProvider = ({
erasToCheck.forEach((era) => {
const eraValidators: string[] = [];
Object.entries(unclaimedRewards).forEach(([validator, eras]) => {
if (eras.includes(era)) eraValidators.push(validator);
if (eras.includes(era)) {
eraValidators.push(validator);
}
});
if (eraValidators.length > 0) unclaimedByEra[era] = eraValidators;
if (eraValidators.length > 0) {
unclaimedByEra[era] = eraValidators;
}
});

// Accumulate calls needed to fetch data to calculate rewards.
const calls: AnyApi[] = [];
Object.entries(unclaimedByEra).forEach(([era, validators]) => {
if (validators.length > 0)
if (validators.length > 0) {
calls.push(
Promise.all([
api.query.staking.erasValidatorReward<AnyApi>(era),
Expand All @@ -217,6 +217,7 @@ export const PayoutsProvider = ({
),
])
);
}
});

// Iterate calls and determine unclaimed payouts.
Expand Down Expand Up @@ -247,6 +248,7 @@ export const PayoutsProvider = ({
const staked = new BigNumber(localExposed?.staked || '0');
const total = new BigNumber(localExposed?.total || '0');
const isValidator = localExposed?.isValidator || false;
const exposedPage = localExposed?.exposedPage || 0;

// Calculate the validator's share of total era payout.
const totalRewardPoints = new BigNumber(
Expand All @@ -269,11 +271,13 @@ export const PayoutsProvider = ({
.dividedBy(total)
.plus(isValidator ? valCut : 0);

unclaimed[era] = {
...unclaimed[era],
[validator]: unclaimedPayout.toString(),
};
j++;
if (!unclaimedPayout.isZero()) {
unclaimed[era] = {
...unclaimed[era],
[validator]: [exposedPage, unclaimedPayout.toString()],
};
j++;
}
}

// This is not currently useful for preventing re-syncing. Need to know the eras that have
Expand Down
3 changes: 2 additions & 1 deletion src/contexts/Payouts/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ export type PayoutsContextInterface = {

export type UnclaimedPayouts = Record<string, EraUnclaimedPayouts> | null;

export type EraUnclaimedPayouts = Record<string, string>;
export type EraUnclaimedPayouts = Record<string, [number, string]>;

export interface LocalValidatorExposure {
staked: string;
total: string;
share: string;
isValidator: boolean;
exposedPage: number;
}
30 changes: 20 additions & 10 deletions src/modals/ClaimPayouts/Forms.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,28 +41,38 @@ export const Forms = forwardRef(
new BigNumber(0)
) || new BigNumber(0);

// Get the total number of validators to payout (the same validator can repeat for separate
// eras).
const totalPayoutValidators =
payouts?.reduce(
(prev, { paginatedValidators }) =>
prev + (paginatedValidators?.length || 0),
0
) || 0;

const getCalls = () => {
if (!api) return [];

const calls: AnyApi[] = [];
payouts?.forEach(({ era, validators }) => {
if (!validators) return [];

return validators.forEach((v) =>
calls.push(api.tx.staking.payoutStakers(v, era))
payouts?.forEach(({ era, paginatedValidators }) => {
if (!paginatedValidators) {
return [];
}
return paginatedValidators.forEach(([page, v]) =>
calls.push(api.tx.staking.payoutStakersByPage(v, era, page))
);
});
return calls;
};

// Store whether form is valid to submit transaction.
const [valid, setValid] = useState<boolean>(
totalPayout.isGreaterThan(0) && getCalls().length > 0
totalPayout.isGreaterThan(0) && totalPayoutValidators > 0
);

// Ensure payouts value is valid.
useEffect(
() => setValid(totalPayout.isGreaterThan(0) && getCalls().length > 0),
() => setValid(totalPayout.isGreaterThan(0) && totalPayoutValidators > 0),
[payouts]
);

Expand All @@ -85,9 +95,9 @@ export const Forms = forwardRef(
},
callbackInBlock: () => {
// Deduct from `unclaimedPayouts` in Payouts context.
payouts?.forEach(({ era, validators }) => {
for (const v of validators || []) {
removeEraPayout(era, v);
payouts?.forEach(({ era, paginatedValidators }) => {
for (const v of paginatedValidators || []) {
removeEraPayout(era, v[1]);
}
});

Expand Down
12 changes: 5 additions & 7 deletions src/modals/ClaimPayouts/Item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
import { ButtonSubmit } from '@polkadot-cloud/react';
import { useTranslation } from 'react-i18next';
import { useApi } from 'contexts/Api';
import BigNumber from 'bignumber.js';
import { planckToUnit } from '@polkadot-cloud/utils';
import { ItemWrapper } from './Wrappers';
import type { ItemProps } from './types';
import { getTotalPayout } from './Utils';

export const Item = ({
era,
Expand All @@ -18,11 +18,7 @@ export const Item = ({
const { t } = useTranslation('modals');
const { network } = useApi();

const totalPayout = Object.values(unclaimedPayout).reduce(
(acc: BigNumber, cur: string) => acc.plus(cur),
new BigNumber(0)
);

const totalPayout = getTotalPayout(unclaimedPayout);
const numPayouts = Object.values(unclaimedPayout).length;

return (
Expand Down Expand Up @@ -51,7 +47,9 @@ export const Item = ({
{
era,
payout: totalPayout.toString(),
validators: Object.keys(unclaimedPayout),
paginatedValidators: Object.entries(unclaimedPayout).map(
([v, [page]]) => [page, v]
),
},
]);
setSection(1);
Expand Down
14 changes: 14 additions & 0 deletions src/modals/ClaimPayouts/Utils.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright 2024 @polkadot-cloud/polkadot-staking-dashboard authors & contributors
// SPDX-License-Identifier: GPL-3.0-only

import BigNumber from 'bignumber.js';
import type { EraUnclaimedPayouts } from 'contexts/Payouts/types';

export const getTotalPayout = (
unclaimedPayout: EraUnclaimedPayouts
): BigNumber =>
Object.values(unclaimedPayout).reduce(
(acc: BigNumber, paginatedValidator: [number, string]) =>
acc.plus(paginatedValidator[1]),
new BigNumber(0)
);
2 changes: 1 addition & 1 deletion src/modals/ClaimPayouts/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export interface ItemProps {
export interface ActivePayout {
era: string;
payout: string;
validators: string[];
paginatedValidators: [number, string][];
}

export interface OverviewProps {
Expand Down
6 changes: 3 additions & 3 deletions src/pages/Nominate/Active/Status/UnclaimedPayoutsStatus.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ export const UnclaimedPayoutsStatus = () => {
const { activeAccount, isReadOnlyAccount } = useConnect();

const totalUnclaimed = Object.values(unclaimedPayouts || {}).reduce(
(total, validators) =>
Object.values(validators)
.reduce((amount, value) => amount.plus(value), new BigNumber(0))
(total, paginatedValidators) =>
Object.values(paginatedValidators)
.reduce((amount, [, value]) => amount.plus(value), new BigNumber(0))
.plus(total),
new BigNumber(0)
);
Expand Down
5 changes: 5 additions & 0 deletions src/workers/stakers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ const processEraForExposure = (data: AnyJson) => {
total,
share,
isValidator,
exposedPage: 0,
};

exposed = true;
Expand All @@ -67,6 +68,9 @@ const processEraForExposure = (data: AnyJson) => {
const inOthers = others.find((o: AnyJson) => o.who === who);

if (inOthers) {
const index = others.findIndex((o: any) => o.who === who);
const exposedPage = Math.floor(index / Number(1024));

const share = new BigNumber(inOthers.value).isZero()
? '0'
: new BigNumber(inOthers.value).dividedBy(total).toString();
Expand All @@ -76,6 +80,7 @@ const processEraForExposure = (data: AnyJson) => {
total,
share,
isValidator,
exposedPage,
};
exposed = true;
if (exitOnExposed) return false;
Expand Down

0 comments on commit 651b9f8

Please sign in to comment.