@substrate/api-sidecar
Version:
REST service that makes it easy to interact with blockchain nodes built using Substrate's FRAME framework.
498 lines • 27.5 kB
JavaScript
;
// Copyright 2017-2025 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
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
Object.defineProperty(exports, "__esModule", { value: true });
exports.AccountsStakingInfoService = void 0;
const http_errors_1 = require("http-errors");
const badStakingBlocks_1 = require("../../util/badStakingBlocks");
const AbstractService_1 = require("../AbstractService");
class AccountsStakingInfoService extends AbstractService_1.AbstractService {
/**
* Fetch staking information for a _Stash_ account at a given block for asset hub.
*
* @param hash `BlockHash` to make call at (Only works for the head of the chain for now)
* @param includeClaimedRewards `boolean` to include claimed rewards (Only works for the head of the chain for now)
* @param stash address of the _Stash_ account to get the staking info of
*/
async fetchAccountStakingInfoAssetHub(hash, includeClaimedRewards, stash) {
var _a, _b;
const { api } = this;
const historicApi = await api.at(hash);
if (!historicApi.query.staking) {
throw new Error(`Staking not available in this runtime. Hash: ${hash.toHex()}`);
}
// Fetching initial data
const [header, controllerOption] = await Promise.all([
api.rpc.chain.getHeader(hash),
historicApi.query.staking.bonded(stash), // Option<AccountId> representing the controller
]).catch((err) => {
throw this.createHttpErrorForAddr(stash, err);
});
const at = {
hash: header.hash.toHex(),
height: header.number.unwrap().toString(10),
};
if ((0, badStakingBlocks_1.isBadStakingBlock)(this.specName, header.number.unwrap().toNumber())) {
let chainName = this.specName;
switch (this.specName) {
case 'westmint':
chainName = 'Westend Asset Hub';
break;
}
throw new Error(`Post migration, there were some interruptions to staking on ${chainName}, Block ${header.number.unwrap().toString(10)} is in the list of known bad staking blocks in ${chainName}`);
}
if (controllerOption.isNone) {
throw new http_errors_1.BadRequest(`The address ${stash} is not a stash address.`);
}
const controller = controllerOption.unwrap();
const [stakingLedgerOption, rewardDestination] = await Promise.all([
historicApi.query.staking.ledger(controller),
historicApi.query.staking.payee(stash),
]).catch((err) => {
throw this.createHttpErrorForAddr(stash, err);
});
const stakingLedger = stakingLedgerOption.unwrapOr(null);
if (stakingLedger === null) {
// should never throw because by time we get here we know we have a bonded pair
throw new http_errors_1.InternalServerError(`Staking ledger could not be found for controller address "${controller.toString()}"`);
}
const [validators, nominations, currentEraOption] = await Promise.all([
historicApi.query.staking.validators.entries(),
historicApi.query.staking.nominators ? (await historicApi.query.staking.nominators(stash)).unwrapOr(null) : null,
historicApi.query.staking.currentEra(),
]);
const isValidator = validators.map(([key, _]) => key.args[0].toString()).includes(stash);
if (includeClaimedRewards) {
// Initializing two arrays to store the status of claimed rewards per era for validators and for nominators.
let claimedRewards = [];
let claimedRewardsNom = [];
// `eraDepth`: the number of eras to check.
const eraDepth = Number(historicApi.consts.staking.historyDepth.toNumber());
const eraStart = this.fetchErasStart(currentEraOption, eraDepth);
for (let e = eraStart; e < eraStart + eraDepth; e++) {
// Type assertion to avoid type error
// AssetHub uses erasClaimedRewards to query claimed rewards, but pre-migration the relay-chain
// uses claimedRewards
if (((_a = historicApi.query.staking) === null || _a === void 0 ? void 0 : _a.claimedRewards) ||
((_b = historicApi.query.staking) === null || _b === void 0 ? void 0 : _b.erasClaimedRewards)) {
if (currentEraOption.isNone) {
throw new http_errors_1.InternalServerError('CurrentEra is None when Some was expected');
}
if (isValidator) {
claimedRewards = await this.fetchErasStatusForValidatorAssetHub(historicApi, e, stash, claimedRewards);
}
else {
// To verify the reward status `claimed` of an era for a Nominator's account,
// we need to check the status of that era in one of their associated Validators' accounts.
const validatorsTargets = nominations === null || nominations === void 0 ? void 0 : nominations.targets.toHuman();
if (validatorsTargets) {
const [era, claimedRewardsNom1] = await this.fetchErasStatusForNominator(historicApi, e, eraDepth, eraStart, claimedRewardsNom, validatorsTargets, stash, currentEraOption, true);
e = era;
claimedRewardsNom = claimedRewardsNom1;
}
}
}
else {
break;
}
}
return {
at,
controller,
rewardDestination,
numSlashingSpans: 0,
nominations,
staking: {
stash: stakingLedger.stash,
total: stakingLedger.total,
active: stakingLedger.active,
unlocking: stakingLedger.unlocking,
claimedRewards: isValidator ? claimedRewards : claimedRewardsNom,
},
};
}
return {
at,
controller,
rewardDestination,
numSlashingSpans: 0,
nominations,
staking: {
stash: stakingLedger.stash,
total: stakingLedger.total,
active: stakingLedger.active,
unlocking: stakingLedger.unlocking,
},
};
}
async fetchErasStatusForValidatorAssetHub(historicApi, e, stash, claimedRewards) {
var _a, _b;
const [erasStakersOverview, claimedRewardsPerEra] = await Promise.all([
((_a = historicApi.query.staking) === null || _a === void 0 ? void 0 : _a.erasStakersOverview)
? (_b = historicApi.query.staking) === null || _b === void 0 ? void 0 : _b.erasStakersOverview(e, stash)
: null,
historicApi.query.staking.claimedRewards
? historicApi.query.staking.claimedRewards(e, stash)
: historicApi.query.staking.erasClaimedRewards(e, stash),
]);
if (erasStakersOverview === null || erasStakersOverview === void 0 ? void 0 : erasStakersOverview.isSome) {
const pageCount = erasStakersOverview.unwrap().pageCount.toNumber();
const eraStatus = claimedRewardsPerEra.length === 0
? 'unclaimed'
: claimedRewardsPerEra.length === pageCount
? 'claimed'
: claimedRewardsPerEra.length != pageCount
? 'partially claimed'
: 'undefined';
claimedRewards.push({ era: e, status: eraStatus });
}
return claimedRewards;
}
/**
* Fetch staking information for a _Stash_ account at a given block.
*
* @param hash `BlockHash` to make call at
* @param stash address of the _Stash_ account to get the staking info of
*/
async fetchAccountStakingInfo(hash, includeClaimedRewards, stash) {
var _a;
const { api } = this;
const historicApi = await api.at(hash);
// Fetching initial data
const [header, controllerOption] = await Promise.all([
api.rpc.chain.getHeader(hash),
historicApi.query.staking.bonded(stash), // Option<AccountId> representing the controller
]).catch((err) => {
throw this.createHttpErrorForAddr(stash, err);
});
const at = {
hash,
height: header.number.unwrap().toString(10),
};
if (controllerOption.isNone) {
throw new http_errors_1.BadRequest(`The address ${stash} is not a stash address.`);
}
const controller = controllerOption.unwrap();
const [stakingLedgerOption, rewardDestination, slashingSpansOption] = await this.fetchStakingData(historicApi, controller, stash);
const stakingLedger = stakingLedgerOption.unwrapOr(null);
/**
* `isValidator`: checking if the account is a validator or not.
*
* `nominations`: fetching the list of validators that a nominator is nominating. This is only relevant for nominators.
* The stash account that we use as key is the nominator's stash account.
* https://polkadot.js.org/docs/substrate/storage/#nominatorsaccountid32-optionpalletstakingnominations
*
* `currentEra`: fetching the current era.
*/
const [isValidator, nominations, currentEraOption] = await Promise.all([
historicApi.query.session
? (await historicApi.query.session.validators()).toHuman().includes(stash)
: false,
historicApi.query.staking.nominators ? (await historicApi.query.staking.nominators(stash)).unwrapOr(null) : null,
historicApi.query.staking.currentEra(),
]);
if (stakingLedger === null) {
// should never throw because by time we get here we know we have a bonded pair
throw new http_errors_1.InternalServerError(`Staking ledger could not be found for controller address "${controller.toString()}"`);
}
const numSlashingSpans = slashingSpansOption.isSome ? slashingSpansOption.unwrap().prior.length + 1 : 0;
if (includeClaimedRewards == true) {
// Initializing two arrays to store the status of claimed rewards per era for validators and for nominators.
let claimedRewards = [];
let claimedRewardsNom = [];
// `eraDepth`: the number of eras to check.
const eraDepth = Number(api.consts.staking.historyDepth.toNumber());
const eraStart = this.fetchErasStart(currentEraOption, eraDepth);
let oldCallChecked = false;
// Checking each era one by one
for (let e = eraStart; e < eraStart + eraDepth; e++) {
let claimedRewardsEras = [];
[claimedRewardsEras, claimedRewards] = this.fetchClaimedInfoFromOldCalls(stakingLedger, claimedRewardsEras, claimedRewards);
[oldCallChecked, claimedRewards, e] = this.isOldCallsChecked(oldCallChecked, claimedRewardsEras, claimedRewards, eraStart, eraDepth, e);
claimedRewardsNom = claimedRewards;
/**
* If the old calls are checked (which means `oldCallChecked` flag is true) and the new call
* `query.staking.claimedRewards` is available then we go into this check.
*/
if (!!((_a = historicApi.query.staking) === null || _a === void 0 ? void 0 : _a.claimedRewards) && oldCallChecked) {
if (currentEraOption.isNone) {
throw new http_errors_1.InternalServerError('CurrentEra is None when Some was expected');
}
if (isValidator) {
claimedRewards = await this.fetchErasStatusForValidator(historicApi, e, stash, claimedRewards);
}
else {
// To verify the reward status `claimed` of an era for a Nominator's account,
// we need to check the status of that era in one of their associated Validators' accounts.
const validatorsTargets = nominations === null || nominations === void 0 ? void 0 : nominations.targets.toHuman();
if (validatorsTargets) {
const [era, claimedRewardsNom1] = await this.fetchErasStatusForNominator(historicApi, e, eraDepth, eraStart, claimedRewardsNom, validatorsTargets, stash, currentEraOption);
e = era;
claimedRewardsNom = claimedRewardsNom1;
}
}
}
else {
break;
}
}
return {
at,
controller,
rewardDestination,
numSlashingSpans,
nominations,
staking: {
stash: stakingLedger.stash,
total: stakingLedger.total,
active: stakingLedger.active,
unlocking: stakingLedger.unlocking,
claimedRewards: isValidator ? claimedRewards : claimedRewardsNom,
},
};
}
return {
at,
controller,
rewardDestination,
numSlashingSpans,
nominations,
staking: {
stash: stakingLedger.stash,
total: stakingLedger.total,
active: stakingLedger.active,
unlocking: stakingLedger.unlocking,
},
};
}
async fetchStakingData(historicApi, controller, stash) {
const [stakingLedgerOption, rewardDestination, slashingSpansOption] = await Promise.all([
historicApi.query.staking.ledger(controller),
historicApi.query.staking.payee(stash),
// Fetch staking data is only used for old calls, therefore we don't have to give asset hub migration
// support here since slashing spans will be queried from the relay chain after the migration.
historicApi.query.staking.slashingSpans(stash),
]).catch((err) => {
throw this.createHttpErrorForAddr(stash, err);
});
return [stakingLedgerOption, rewardDestination, slashingSpansOption];
}
async fetchErasStatusForValidator(historicApi, e, stash, claimedRewards) {
var _a, _b, _c;
const [erasStakersOverview, erasStakers, claimedRewardsPerEra] = await Promise.all([
((_a = historicApi.query.staking) === null || _a === void 0 ? void 0 : _a.erasStakersOverview) ? (_b = historicApi.query.staking) === null || _b === void 0 ? void 0 : _b.erasStakersOverview(e, stash) : null,
((_c = historicApi.query.staking) === null || _c === void 0 ? void 0 : _c.erasStakers) ? historicApi.query.staking.erasStakers(e, stash) : null,
historicApi.query.staking.claimedRewards(e, stash),
]);
if (erasStakersOverview === null || erasStakersOverview === void 0 ? void 0 : erasStakersOverview.isSome) {
const pageCount = erasStakersOverview.unwrap().pageCount.toNumber();
const eraStatus = claimedRewardsPerEra.length === 0
? 'unclaimed'
: claimedRewardsPerEra.length === pageCount
? 'claimed'
: claimedRewardsPerEra.length != pageCount
? 'partially claimed'
: 'undefined';
claimedRewards.push({ era: e, status: 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 ? 'claimed' : 'unclaimed';
claimedRewards.push({ era: e, status: eraStatus });
}
return claimedRewards;
}
async fetchErasStatusForNominator(historicApi, e, depth, eraStart, claimedRewardsNom, validatorsTargets, nominatorStash, currentEraOption, isAssetHubMigration = false) {
var _a, _b;
// Iterate through all validators that the nominator is nominating and
// check if the rewards are claimed or not.
for (const [idx, validatorStash] of validatorsTargets.entries()) {
let oldCallChecked = false;
if (claimedRewardsNom.length == 0 && !isAssetHubMigration) {
const [era, claimedRewardsOld, oldCallCheck] = await this.fetchErasFromOldCalls(historicApi, e, depth, eraStart, claimedRewardsNom, validatorStash, oldCallChecked);
claimedRewardsNom = claimedRewardsOld;
oldCallChecked = oldCallCheck;
e = era;
}
else {
oldCallChecked = true;
}
// Checking if the new call is available then I can check if rewards of nominator are claimed or not.
// If not available, I will set the status to 'undefined'.
if (!!((_a = historicApi.query.staking) === null || _a === void 0 ? void 0 : _a.claimedRewards) && oldCallChecked) {
if (currentEraOption.isNone) {
throw new http_errors_1.InternalServerError('CurrentEra is None when Some was expected');
}
// Doing similar checks as in fetchErasStatusForValidator function
// but with slight changes to adjust to nominator's case
const [erasStakersOverview, erasStakers, claimedRewardsPerEra] = await Promise.all([
historicApi.query.staking.erasStakersOverview(e, validatorStash),
((_b = historicApi.query.staking) === null || _b === void 0 ? void 0 : _b.erasStakers) ? historicApi.query.staking.erasStakers(e, validatorStash) : null,
historicApi.query.staking.claimedRewards(e, validatorStash),
]);
if (erasStakersOverview.isSome) {
const pageCount = erasStakersOverview.unwrap().pageCount.toNumber();
const eraStatus = claimedRewardsPerEra.length === 0
? 'unclaimed'
: claimedRewardsPerEra.length === pageCount
? 'claimed'
: claimedRewardsPerEra.length != pageCount
? await this.ErasStatusNominatorForValPartiallyClaimed(historicApi, e, validatorStash, pageCount, nominatorStash, claimedRewardsPerEra)
: 'undefined';
claimedRewardsNom.push({ era: e, status: eraStatus });
break;
}
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 ? 'claimed' : 'unclaimed';
claimedRewardsNom.push({ era: e, status: eraStatus });
break;
}
else {
if (idx === validatorsTargets.length - 1) {
claimedRewardsNom.push({ era: e, status: 'undefined' });
}
else {
continue;
}
}
}
}
return [e, claimedRewardsNom];
}
/**
* This function returns the era and its reward status information for a given stash account.
*/
async fetchErasFromOldCalls(historicApi, e, depth, eraStart, claimedRewards, validatorStash, oldCallChecked) {
let claimedRewardsEras = [];
const controllerOption = await historicApi.query.staking.bonded(validatorStash);
if (controllerOption.isNone) {
return [e, claimedRewards, oldCallChecked];
}
const controller = controllerOption.unwrap();
const [stakingLedgerOption, ,] = await this.fetchStakingData(historicApi, controller, validatorStash);
const stakingLedgerValNom = stakingLedgerOption.unwrapOr(null);
[claimedRewardsEras, claimedRewards] = this.fetchClaimedInfoFromOldCalls(stakingLedgerValNom, claimedRewardsEras, claimedRewards);
[oldCallChecked, claimedRewards, e] = this.isOldCallsChecked(oldCallChecked, claimedRewardsEras, claimedRewards, eraStart, depth, e);
return [e, claimedRewards, oldCallChecked];
}
async ErasStatusNominatorForValPartiallyClaimed(historicApi, e, validatorStash, pageCount, nominatorStash, claimedRewardsPerEra) {
var _a;
// If era is partially claimed from the side of the validator that means that the validator
// has more than one page of nominators. In this case, I need to check in which page the nominator is
// and if that page was claimed or not.
for (let page = 0; page < pageCount; page++) {
if ((_a = historicApi.query.staking) === null || _a === void 0 ? void 0 : _a.erasStakersPaged) {
const erasStakers = await historicApi.query.staking.erasStakersPaged(e, validatorStash, page);
const erasStakersPaged = erasStakers.unwrapOr(null);
if (erasStakersPaged === null || erasStakersPaged === void 0 ? void 0 : erasStakersPaged.others) {
for (const nominator of erasStakersPaged.others.entries()) {
if (nominatorStash === nominator[1].who.toString()) {
if (claimedRewardsPerEra.length > 0) {
const pageIncluded = claimedRewardsPerEra === null || claimedRewardsPerEra === void 0 ? void 0 : claimedRewardsPerEra.some((reward) => Number(reward) === Number(page));
if (pageIncluded) {
return 'claimed';
}
else {
return 'unclaimed';
}
}
break;
}
}
}
}
}
return 'undefined';
}
/**
* This function calculates the era from which we should start checking
* for claimed rewards.
*/
fetchErasStart(currentEraOption, eraDepth) {
if (currentEraOption.isNone) {
throw new http_errors_1.InternalServerError('CurrentEra is None when Some was expected');
}
const currentEraNumber = currentEraOption.unwrap().toNumber();
const eraStart = Math.max(0, currentEraNumber - eraDepth);
return eraStart;
}
/**
* This function verifies if the information from old calls has already been checked/used. If not,
* it proceeds to use it and populate the `claimedRewards` array with the eras that have been claimed.
* Note that data from old calls may also be empty (no results), in which case the `claimedRewards` array
* will only be populated with data from the new `query.staking?.claimedRewards` call
* (later in the main function's code).
*
* Returns a boolean flag `oldCallChecked` that indicates if the old calls have already been checked/used or not.
*
*/
isOldCallsChecked(oldCallChecked, claimedRewardsEras, claimedRewards, eraStart, depth, e) {
if (!oldCallChecked) {
if (claimedRewardsEras && claimedRewardsEras.length > 0) {
claimedRewards = claimedRewardsEras.map((element) => ({
era: element.toNumber(),
status: 'claimed',
}));
const claimedRewardsErasMax = claimedRewardsEras[claimedRewardsEras.length - 1].toNumber();
/**
* This check was added because old calls would sometimes return eras outside the intended range.
* In such cases, I need to verify if the era falls within the specific range I am checking.
*/
if (eraStart <= claimedRewardsErasMax) {
e = claimedRewardsErasMax + 1;
}
else if (depth == claimedRewardsEras.length) {
if (claimedRewardsEras[0].toNumber() == eraStart) {
e = eraStart + depth;
}
}
else {
claimedRewards = [];
}
}
oldCallChecked = true;
}
return [oldCallChecked, claimedRewards, e];
}
fetchClaimedInfoFromOldCalls(stakingLedger, claimedRewardsEras, claimedRewards) {
var _a;
// Checking first the old call of `lastReward` and setting as claimed only the era that
// is defined in the lastReward field. I do not make any assumptions for any other eras.
if (stakingLedger === null || stakingLedger === void 0 ? void 0 : stakingLedger.lastReward) {
const lastReward = stakingLedger.lastReward;
if (lastReward.isSome) {
const e = (_a = stakingLedger === null || stakingLedger === void 0 ? void 0 : stakingLedger.lastReward) === null || _a === void 0 ? void 0 : _a.unwrap().toNumber();
if (e) {
claimedRewards.push({ era: e, status: 'claimed' });
}
}
// Second check is another old call called `legacyClaimedRewards` from stakingLedger
}
else if (stakingLedger === null || stakingLedger === void 0 ? void 0 : stakingLedger.legacyClaimedRewards) {
claimedRewardsEras = stakingLedger === null || stakingLedger === void 0 ? void 0 : stakingLedger.legacyClaimedRewards;
// If none of the above are present, we try the `claimedRewards` from stakingLedger
}
else {
claimedRewardsEras = stakingLedger === null || stakingLedger === void 0 ? void 0 : stakingLedger.claimedRewards;
}
return [claimedRewardsEras, claimedRewards];
}
}
exports.AccountsStakingInfoService = AccountsStakingInfoService;
//# sourceMappingURL=AccountsStakingInfoService.js.map