From 651b9f8628abc69da15ec32fa997bbd0795b0ccf Mon Sep 17 00:00:00 2001 From: kryzasada Date: Mon, 11 Nov 2024 20:02:14 +0100 Subject: [PATCH] Fix claim and payout --- src/contexts/Payouts/Utils.ts | 2 +- src/contexts/Payouts/index.tsx | 92 ++++++++++--------- src/contexts/Payouts/types.ts | 3 +- src/modals/ClaimPayouts/Forms.tsx | 30 ++++-- src/modals/ClaimPayouts/Item.tsx | 12 +-- src/modals/ClaimPayouts/Utils.tsx | 14 +++ src/modals/ClaimPayouts/types.ts | 2 +- .../Active/Status/UnclaimedPayoutsStatus.tsx | 6 +- src/workers/stakers.ts | 5 + 9 files changed, 99 insertions(+), 67 deletions(-) create mode 100644 src/modals/ClaimPayouts/Utils.tsx diff --git a/src/contexts/Payouts/Utils.ts b/src/contexts/Payouts/Utils.ts index 28bb1c53d4..af7bf8b048 100644 --- a/src/contexts/Payouts/Utils.ts +++ b/src/contexts/Payouts/Utils.ts @@ -72,7 +72,7 @@ export const setLocalUnclaimedPayouts = ( network: NetworkName, era: string, who: string, - unclaimdPayouts: Record, + unclaimdPayouts: Record, endEra: string ) => { const current = JSON.parse( diff --git a/src/contexts/Payouts/index.tsx b/src/contexts/Payouts/index.tsx index f6c552e4e4..fb8da1ae8d 100644 --- a/src/contexts/Payouts/index.tsx +++ b/src/contexts/Payouts/index.tsx @@ -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(uniqueValidators); const validatorControllers: Record = {}; 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( - Object.values(validatorControllers) - ); const unclaimedRewards: Record = {}; - 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(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]; + } } } @@ -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(era), @@ -217,6 +217,7 @@ export const PayoutsProvider = ({ ), ]) ); + } }); // Iterate calls and determine unclaimed payouts. @@ -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( @@ -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 diff --git a/src/contexts/Payouts/types.ts b/src/contexts/Payouts/types.ts index 3adfe39aeb..4010f04bb3 100644 --- a/src/contexts/Payouts/types.ts +++ b/src/contexts/Payouts/types.ts @@ -11,11 +11,12 @@ export type PayoutsContextInterface = { export type UnclaimedPayouts = Record | null; -export type EraUnclaimedPayouts = Record; +export type EraUnclaimedPayouts = Record; export interface LocalValidatorExposure { staked: string; total: string; share: string; isValidator: boolean; + exposedPage: number; } diff --git a/src/modals/ClaimPayouts/Forms.tsx b/src/modals/ClaimPayouts/Forms.tsx index eb4f7ae97b..4d327784a5 100644 --- a/src/modals/ClaimPayouts/Forms.tsx +++ b/src/modals/ClaimPayouts/Forms.tsx @@ -41,15 +41,25 @@ 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; @@ -57,12 +67,12 @@ export const Forms = forwardRef( // Store whether form is valid to submit transaction. const [valid, setValid] = useState( - 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] ); @@ -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]); } }); diff --git a/src/modals/ClaimPayouts/Item.tsx b/src/modals/ClaimPayouts/Item.tsx index 72ca382be9..9a1621119a 100644 --- a/src/modals/ClaimPayouts/Item.tsx +++ b/src/modals/ClaimPayouts/Item.tsx @@ -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, @@ -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 ( @@ -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); diff --git a/src/modals/ClaimPayouts/Utils.tsx b/src/modals/ClaimPayouts/Utils.tsx new file mode 100644 index 0000000000..1bed34a57b --- /dev/null +++ b/src/modals/ClaimPayouts/Utils.tsx @@ -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) + ); diff --git a/src/modals/ClaimPayouts/types.ts b/src/modals/ClaimPayouts/types.ts index e31995da68..5c13c59b4a 100644 --- a/src/modals/ClaimPayouts/types.ts +++ b/src/modals/ClaimPayouts/types.ts @@ -13,7 +13,7 @@ export interface ItemProps { export interface ActivePayout { era: string; payout: string; - validators: string[]; + paginatedValidators: [number, string][]; } export interface OverviewProps { diff --git a/src/pages/Nominate/Active/Status/UnclaimedPayoutsStatus.tsx b/src/pages/Nominate/Active/Status/UnclaimedPayoutsStatus.tsx index 4bf068bcd1..0d617dbcda 100644 --- a/src/pages/Nominate/Active/Status/UnclaimedPayoutsStatus.tsx +++ b/src/pages/Nominate/Active/Status/UnclaimedPayoutsStatus.tsx @@ -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) ); diff --git a/src/workers/stakers.ts b/src/workers/stakers.ts index 80beb59a5f..431ceea597 100644 --- a/src/workers/stakers.ts +++ b/src/workers/stakers.ts @@ -58,6 +58,7 @@ const processEraForExposure = (data: AnyJson) => { total, share, isValidator, + exposedPage: 0, }; exposed = true; @@ -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(); @@ -76,6 +80,7 @@ const processEraForExposure = (data: AnyJson) => { total, share, isValidator, + exposedPage, }; exposed = true; if (exitOnExposed) return false;