Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: staking payouts claimed with new logic & statuses #1457

Open
wants to merge 2 commits into
base: anp-v14-claimed
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/services/accounts/AccountsStakingPayoutsService.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ describe('AccountsStakingPayoutsService', () => {
era: '1039',
payouts: [
{
claimed: true,
claimed: 'claimed',
nominatorExposure: '0',
nominatorStakingPayout: '1043968334900993560134832959396203124',
totalValidatorExposure: '17302617747768368',
Expand Down Expand Up @@ -131,7 +131,7 @@ describe('AccountsStakingPayoutsService', () => {
era: '1039',
payouts: [
{
claimed: true,
claimed: 'claimed',
nominatorExposure: '21133134966048676',
nominatorStakingPayout: '0',
totalValidatorExposure: '21133134966048676',
Expand Down
115 changes: 73 additions & 42 deletions src/services/accounts/AccountsStakingPayoutsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,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';

Expand Down Expand Up @@ -83,9 +84,10 @@ interface IAdjustedDeriveEraExposure extends DeriveEraExposure {
/**
* Commission and staking ledger of a validator
*/
interface ICommissionAndLedger {
interface ICommissionLedgerAndClaimed {
commission: Perbill;
validatorLedger?: PalletStakingStakingLedger;
claimedRewards?: IStatusPerEra;
}

/**
Expand All @@ -95,7 +97,7 @@ interface IEraData {
deriveEraExposure: IAdjustedDeriveEraExposure;
eraRewardPoints: PalletStakingEraRewardPoints | EraPoints;
erasValidatorRewardOption: Option<BalanceOf>;
exposuresWithCommission?: (ICommissionAndLedger & {
exposuresWithCommission?: (ICommissionLedgerAndClaimed & {
validatorId: string;
})[];
eraIndex: EraIndex;
Expand Down Expand Up @@ -327,7 +329,7 @@ export class AccountsStakingPayoutsService extends AbstractService {
startEra: number,
deriveErasExposures: IAdjustedDeriveEraExposure[],
isKusama: boolean,
): Promise<ICommissionAndLedger[][]> {
): Promise<ICommissionLedgerAndClaimed[][]> {
// Cache StakingLedger to reduce redundant queries to node
const validatorLedgerCache: { [id: string]: PalletStakingStakingLedger } = {};

Expand All @@ -341,7 +343,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);
Expand Down Expand Up @@ -382,7 +384,12 @@ export class AccountsStakingPayoutsService extends AbstractService {

// Iterate through validators that this nominator backs and calculate payouts for the era
const payouts: IPayout[] = [];
for (const { validatorId, commission: validatorCommission, validatorLedger } of exposuresWithCommission) {
for (const {
validatorId,
commission: validatorCommission,
validatorLedger,
claimedRewards,
} of exposuresWithCommission) {
const totalValidatorRewardPoints = deriveEraExposure.validatorIndex
? this.extractTotalValidatorRewardPoints(eraRewardPoints, validatorId, deriveEraExposure.validatorIndex)
: this.extractTotalValidatorRewardPoints(eraRewardPoints, validatorId);
Expand All @@ -401,32 +408,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;
}

Expand Down Expand Up @@ -465,17 +456,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<ICommissionAndLedger> {
): Promise<ICommissionLedgerAndClaimed> {
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;
Expand Down Expand Up @@ -515,22 +507,61 @@ export class AccountsStakingPayoutsService extends AbstractService {
};
} else {
validatorLedger = validatorLedgerOption.unwrap();
if (
historicApi.query.staking.claimedRewards &&
(await historicApi.query.staking.claimedRewards(era, validatorControllerOption.unwrap())).length ===
(await historicApi.query.staking.erasStakersOverview(era, validatorControllerOption.unwrap()))
.unwrap()
.pageCount.toNumber()
) {
const eraVal: u32 = historicApi.registry.createType('u32', era);
validatorLedger.legacyClaimedRewards.push(eraVal);
/**
* 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<u32>;
}
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;
}
}
}

validatorLedgerCache[validatorId] = validatorLedger;
}

return { commission, validatorLedger };
return { commission, validatorLedger, claimedRewards };
}

/**
Expand Down
13 changes: 12 additions & 1 deletion src/types/responses/AccountStakingPayouts.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2017-2022 Parity Technologies (UK) Ltd.
// Copyright 2017-2024 Parity Technologies (UK) Ltd.
// This file is part of Substrate API Sidecar.
//
// Substrate API Sidecar is free software: you can redistribute it and/or modify
Expand All @@ -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 })[];
Expand Down
4 changes: 3 additions & 1 deletion src/types/responses/Payout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down