From 377ee2909ecc97b44a90dd07cb6bae3f1754ba71 Mon Sep 17 00:00:00 2001 From: Imod7 Date: Tue, 21 Jan 2025 13:47:28 +0100 Subject: [PATCH] staking payouts claimed field new logic --- .../AccountsStakingPayoutsService.spec.ts | 4 +- .../accounts/AccountsStakingPayoutsService.ts | 115 ++++++++++++------ src/types/responses/AccountStakingPayouts.ts | 11 ++ src/types/responses/Payout.ts | 4 +- 4 files changed, 92 insertions(+), 42 deletions(-) diff --git a/src/services/accounts/AccountsStakingPayoutsService.spec.ts b/src/services/accounts/AccountsStakingPayoutsService.spec.ts index c9ab33c7d..4a0f73212 100644 --- a/src/services/accounts/AccountsStakingPayoutsService.spec.ts +++ b/src/services/accounts/AccountsStakingPayoutsService.spec.ts @@ -93,7 +93,7 @@ describe('AccountsStakingPayoutsService', () => { era: '1039', payouts: [ { - claimed: true, + claimed: 'claimed', nominatorExposure: '0', nominatorStakingPayout: '1043968334900993560134832959396203124', totalValidatorExposure: '17302617747768368', @@ -129,7 +129,7 @@ describe('AccountsStakingPayoutsService', () => { era: '1039', payouts: [ { - claimed: true, + claimed: 'claimed', nominatorExposure: '21133134966048676', nominatorStakingPayout: '0', totalValidatorExposure: '21133134966048676', diff --git a/src/services/accounts/AccountsStakingPayoutsService.ts b/src/services/accounts/AccountsStakingPayoutsService.ts index b6a4431f1..4856ba854 100644 --- a/src/services/accounts/AccountsStakingPayoutsService.ts +++ b/src/services/accounts/AccountsStakingPayoutsService.ts @@ -48,6 +48,7 @@ import { CalcPayout } from '@substrate/calc'; import { BadRequest } from 'http-errors'; import type { IAccountStakingPayouts, IEraPayouts, IPayout } from '../../types/responses'; +import { IStatus, IStatusPerEra } from '../../types/responses/AccountStakingPayouts'; import { AbstractService } from '../AbstractService'; import kusamaEarlyErasBlockInfo from './kusamaEarlyErasBlockInfo.json'; @@ -82,11 +83,12 @@ interface IAdjustedDeriveEraExposure extends DeriveEraExposure { } /** - * Commission and staking ledger of a validator + * Commission, staking ledger & claimed information of a validator */ -interface ICommissionAndLedger { +interface ICommissionLedgerAndClaimed { commission: Perbill; validatorLedger?: PalletStakingStakingLedger; + claimedRewards?: IStatusPerEra; } /** @@ -96,7 +98,7 @@ interface IEraData { deriveEraExposure: IAdjustedDeriveEraExposure; eraRewardPoints: PalletStakingEraRewardPoints | EraPoints; erasValidatorRewardOption: Option; - exposuresWithCommission?: (ICommissionAndLedger & { + exposuresWithCommission?: (ICommissionLedgerAndClaimed & { validatorId: string; nominatorIndex: number; })[]; @@ -264,7 +266,7 @@ export class AccountsStakingPayoutsService extends AbstractService { blockNumber: IBlockInfo, isKusama: boolean, ): Promise { - const allDeriveQuerys: Promise[] = []; + const allDeriveQueries: Promise[] = []; let nextEraStartBlock: number = Number(blockNumber.height); let eraDurationInBlocks: number = 0; const earlyErasBlockInfo: IEarlyErasBlockInfo = kusamaEarlyErasBlockInfo; @@ -277,7 +279,7 @@ export class AccountsStakingPayoutsService extends AbstractService { historicApi.query.staking.erasRewardPoints(eraIndex), historicApi.query.staking.erasValidatorReward(eraIndex), ]); - allDeriveQuerys.push(eraGeneralTuple); + allDeriveQueries.push(eraGeneralTuple); } else { // We check if we are in the Kusama chain since currently we have // the block info for the early eras only for Kusama. @@ -290,6 +292,7 @@ export class AccountsStakingPayoutsService extends AbstractService { const epochDuration = historicApi.consts.babe.epochDuration.toNumber(); eraDurationInBlocks = sessionDuration * epochDuration; } + const nextEraStartBlockHash: BlockHash = await this.api.rpc.chain.getBlockHash(nextEraStartBlock); const currentEraEndBlockHash: BlockHash = era === 0 @@ -323,10 +326,10 @@ export class AccountsStakingPayoutsService extends AbstractService { const eraGeneralTuple = Promise.all([this.deriveEraExposure(historicApi, eraIndex), points, rewardPromise]); - allDeriveQuerys.push(eraGeneralTuple); + allDeriveQueries.push(eraGeneralTuple); } } - return Promise.all(allDeriveQuerys); + return Promise.all(allDeriveQueries); } private async fetchHistoricRewardPoints(hash: BlockHash): Promise { @@ -348,7 +351,7 @@ export class AccountsStakingPayoutsService extends AbstractService { startEra: number, deriveErasExposures: IAdjustedDeriveEraExposure[], isKusama: boolean, - ): Promise { + ): Promise { // Cache StakingLedger to reduce redundant queries to node const validatorLedgerCache: { [id: string]: PalletStakingStakingLedger } = {}; @@ -362,7 +365,7 @@ export class AccountsStakingPayoutsService extends AbstractService { } const singleEraCommissions = nominatedExposures.map(({ validatorId }) => - this.fetchCommissionAndLedger(historicApi, validatorId, currEra, validatorLedgerCache, isKusama), + this.fetchCommissionLedgerAndClaimed(historicApi, validatorId, currEra, validatorLedgerCache, isKusama), ); return Promise.all(singleEraCommissions); @@ -408,6 +411,7 @@ export class AccountsStakingPayoutsService extends AbstractService { commission: validatorCommission, validatorLedger, nominatorIndex, + claimedRewards, } of exposuresWithCommission) { const totalValidatorRewardPoints = deriveEraExposure.validatorIndex ? this.extractTotalValidatorRewardPoints(eraRewardPoints, validatorId, deriveEraExposure.validatorIndex) @@ -432,32 +436,16 @@ export class AccountsStakingPayoutsService extends AbstractService { continue; } - /** - * Check if the reward has already been claimed. - * - * It is important to note that the following examines types that are both current and historic. - * When going back far enough in certain chains types such as `StakingLedgerTo240` are necessary for grabbing - * any reward data. - */ - let indexOfEra: number; - if (validatorLedger.legacyClaimedRewards) { - indexOfEra = validatorLedger.legacyClaimedRewards.indexOf(eraIndex); - } else if ((validatorLedger as unknown as StakingLedger).claimedRewards) { - indexOfEra = (validatorLedger as unknown as StakingLedger).claimedRewards.indexOf(eraIndex); - } else if ((validatorLedger as unknown as StakingLedgerTo240).lastReward) { - const lastReward = (validatorLedger as unknown as StakingLedgerTo240).lastReward; - if (lastReward.isSome) { - indexOfEra = lastReward.unwrap().toNumber(); - } else { - indexOfEra = -1; - } + // Setting the value of `claimed` based on `claimedRewards` + let claimed; + if (claimedRewards && claimedRewards[eraIndex.toNumber()]) { + claimed = claimedRewards[eraIndex.toNumber()]; } else if (eraIndex.toNumber() < 518 && isKusama) { - indexOfEra = eraIndex.toNumber(); + claimed = IStatus.claimed; } else { - continue; + claimed = IStatus.undefined; } - const claimed: boolean = Number.isInteger(indexOfEra) && indexOfEra !== -1; - if (unclaimedOnly && claimed) { + if (unclaimedOnly && claimed === IStatus.claimed) { continue; } @@ -496,17 +484,18 @@ export class AccountsStakingPayoutsService extends AbstractService { * @param era the era to query * @param validatorLedgerCache object mapping validatorId => StakingLedger to limit redundant queries */ - private async fetchCommissionAndLedger( + private async fetchCommissionLedgerAndClaimed( historicApi: ApiDecoration<'promise'>, validatorId: string, era: number, validatorLedgerCache: { [id: string]: PalletStakingStakingLedger }, isKusama: boolean, - ): Promise { + ): Promise { let commission: Perbill; let validatorLedger; let commissionPromise; const ancient: boolean = era < 518; + const claimedRewards: IStatusPerEra = {}; if (validatorId in validatorLedgerCache) { validatorLedger = validatorLedgerCache[validatorId]; let prefs: PalletStakingValidatorPrefs | ValidatorPrefsWithCommission; @@ -544,13 +533,62 @@ export class AccountsStakingPayoutsService extends AbstractService { return { commission, }; + } else { + validatorLedger = validatorLedgerOption.unwrap(); + /** + * Check if the reward has already been claimed. + * + * It is important to note that the following examines types that are both current and historic. + * When going back far enough in certain chains types such as `StakingLedgerTo240` are necessary for grabbing + * any reward data. + */ + let claimedRewardsEras: u32[] = []; + if ((validatorLedger as unknown as StakingLedgerTo240)?.lastReward) { + const lastReward = (validatorLedger as unknown as StakingLedgerTo240).lastReward; + if (lastReward.isSome) { + const e = (validatorLedger as unknown as StakingLedgerTo240)?.lastReward?.unwrap().toNumber(); + if (e) { + claimedRewards[e] = IStatus.claimed; + } + } + } + if (validatorLedger?.legacyClaimedRewards) { + claimedRewardsEras = validatorLedger?.legacyClaimedRewards; + } else { + claimedRewardsEras = (validatorLedger as unknown as StakingLedger)?.claimedRewards as Vec; + } + if (claimedRewardsEras) { + claimedRewardsEras.forEach((era) => { + claimedRewards[era.toNumber()] = IStatus.claimed; + }); + } + if (historicApi.query.staking?.claimedRewards) { + const claimedRewardsPerEra = await historicApi.query.staking.claimedRewards(era, validatorId); + const erasStakersOverview = await historicApi.query.staking.erasStakersOverview(era, validatorId); + let erasStakers = null; + if (historicApi.query.staking?.erasStakers) { + erasStakers = await historicApi.query.staking.erasStakers(era, validatorId); + } + if (erasStakersOverview.isSome) { + const pageCount = erasStakersOverview.unwrap().pageCount.toNumber(); + const eraStatus = + claimedRewardsPerEra.length === 0 + ? IStatus.unclaimed + : claimedRewardsPerEra.length === pageCount + ? IStatus.claimed + : IStatus.partiallyClaimed; + claimedRewards[era] = eraStatus; + } else if (erasStakers && erasStakers.total.toBigInt() > 0) { + // if erasStakers.total > 0, then the pageCount is always 1 + // https://github.com/polkadot-js/api/issues/5859#issuecomment-2077011825 + const eraStatus = claimedRewardsPerEra.length === 1 ? IStatus.claimed : IStatus.unclaimed; + claimedRewards[era] = eraStatus; + } + } } - - validatorLedger = validatorLedgerOption.unwrap(); - validatorLedgerCache[validatorId] = validatorLedger; } - return { commission, validatorLedger }; + return { commission, validatorLedger, claimedRewards }; } /** @@ -576,7 +614,6 @@ export class AccountsStakingPayoutsService extends AbstractService { const nominators: DeriveEraNominatorExposure = {}; const validators: DeriveEraValidatorExposure = {}; const validatorsOverview: Record> = {}; - stakers.forEach(([key, exposure]): void => { const validatorId = key.args[1].toString(); diff --git a/src/types/responses/AccountStakingPayouts.ts b/src/types/responses/AccountStakingPayouts.ts index 25ddcc1e0..13e5836d9 100644 --- a/src/types/responses/AccountStakingPayouts.ts +++ b/src/types/responses/AccountStakingPayouts.ts @@ -16,6 +16,17 @@ import { IAt, IEraPayouts } from '.'; +export enum IStatus { + claimed = 'claimed', + partiallyClaimed = 'partially claimed', + unclaimed = 'unclaimed', + undefined = 'undefined', +} + +export interface IStatusPerEra { + [era: number]: IStatus; +} + export interface IAccountStakingPayouts { at: IAt; erasPayouts: (IEraPayouts | { message: string })[]; diff --git a/src/types/responses/Payout.ts b/src/types/responses/Payout.ts index e6120d6e1..959119c11 100644 --- a/src/types/responses/Payout.ts +++ b/src/types/responses/Payout.ts @@ -16,10 +16,12 @@ import { Balance, Perbill, RewardPoint } from '@polkadot/types/interfaces'; +import { IStatus } from './AccountStakingPayouts'; + export interface IPayout { validatorId: string; nominatorStakingPayout: string; - claimed: boolean; + claimed: IStatus; validatorCommission: Perbill; totalValidatorRewardPoints: RewardPoint; totalValidatorExposure: Balance;