UNPKG

@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
"use strict"; // 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