@substrate/api-sidecar
Version:
REST service that makes it easy to interact with blockchain nodes built using Substrate's FRAME framework.
734 lines • 37.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/>.
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AccountsStakingPayoutsService = void 0;
const calc_1 = require("@substrate/calc");
const http_errors_1 = require("http-errors");
const apiRegistry_1 = require("../../apiRegistry");
const AbstractService_1 = require("../AbstractService");
const kusamaEarlyErasBlockInfo_json_1 = __importDefault(require("./kusamaEarlyErasBlockInfo.json"));
const MIGRATION_BOUNDARIES = {
westmint: {
relayChainLastEra: 9297,
assetHubFirstEra: 9297,
assetHubMigrationStartedAt: 11716733,
assetHubMigrationEndedAt: 11736597,
relayMigrationStartedAt: 26041702,
relayMigrationEndedAt: 26071771,
},
};
class AccountsStakingPayoutsService extends AbstractService_1.AbstractService {
/**
* Fetch and derive payouts for `address`.
*
* @param hash `BlockHash` to make call at
* @param address address of the _Stash_ account to get the payouts of
* @param depth number of eras to query at and below the specified era
* @param era the most recent era to query
* @param unclaimedOnly whether or not to only show unclaimed payouts
* @param currentEra The current era
* @param historicApi Historic api for querying past blocks
*/
async fetchAccountStakingPayout(hash, address, depth, era, unclaimedOnly, currentEra, historicApi) {
const { api } = this;
const sanitizedEra = era < 0 ? 0 : era;
const [{ number }, runtimeInfo] = await Promise.all([
api.rpc.chain.getHeader(hash),
this.api.rpc.state.getRuntimeVersion(hash),
]);
const at = {
height: number.unwrap().toString(10),
hash,
};
// User friendly - we don't error if the user specified era & depth combo <= 0, instead just start at 0
const startEra = Math.max(0, sanitizedEra - (depth - 1));
const isKusama = runtimeInfo.specName.toString().toLowerCase() === 'kusama';
/**
* Given https://github.com/polkadot-js/api/issues/5232,
* polkadot-js, and substrate treats historyDepth as a consts. In order
* to maintain historical integrity we need to make a check to cover both the
* storage query and the consts.
*/
let historyDepth = api.registry.createType('u32', 84);
if (historicApi.consts.staking.historyDepth) {
historyDepth = historicApi.consts.staking.historyDepth;
}
else if (historicApi.query.staking.historyDepth) {
historyDepth = await historicApi.query.staking.historyDepth();
}
else if (currentEra < 518 && isKusama) {
historyDepth = api.registry.createType('u32', 0);
}
// Information is kept for eras in `[current_era - history_depth; current_era]`
if (historyDepth.toNumber() !== 0 && depth > historyDepth.toNumber()) {
throw new http_errors_1.BadRequest('Must specify a depth less than history_depth');
}
if (era - (depth - 1) < currentEra - historyDepth.toNumber() && historyDepth.toNumber() !== 0) {
// In scenarios where depth is not > historyDepth, but the user specifies an era
// and historyDepth combo that would lead to querying eras older than history depth
throw new http_errors_1.BadRequest('Must specify era and depth such that era - (depth - 1) is less ' +
'than or equal to current_era - history_depth.');
}
// ARRIVED HERE
// Fetch general data about the era
const allErasGeneral = await this.fetchAllErasGeneral(historicApi, startEra, sanitizedEra, at, isKusama);
// With the general data, we can now fetch the commission of each validator `address` nominates
const allErasCommissions = await this.fetchAllErasCommissions(historicApi, address, startEra,
// Create an array of `DeriveEraExposure`
allErasGeneral.map((eraGeneral) => eraGeneral[0]), isKusama).catch((err) => {
throw this.createHttpErrorForAddr(address, err);
});
// Group together data by Era so we can easily associate parts that are used congruently downstream
const allEraData = allErasGeneral.map(([deriveEraExposure, eraRewardPoints, erasValidatorRewardOption], idx) => {
const eraCommissions = allErasCommissions[idx];
const nominatedExposures = this.deriveNominatedExposures(address, deriveEraExposure);
// Zip the `validatorId` with its associated `commission`, making the data easier to reason
// about downstream. In this object we added the `nominatorIndex` to account for the rare cases
// where a nominator has multiple nominations (with different stakes) on the same validator and
// at the same era.
const exposuresWithCommission = [];
if (nominatedExposures) {
for (let idx = 0; idx < nominatedExposures.length; idx++) {
let index = 0;
const { validatorId } = nominatedExposures[idx];
const nominatorInstances = nominatedExposures.filter((exposure) => exposure.validatorId.toString() === validatorId).length;
const exposuresValidatorLen = exposuresWithCommission.filter((exposure) => exposure.validatorId.toString() === validatorId).length;
if (nominatorInstances > 1) {
index = exposuresValidatorLen;
}
if (eraCommissions[idx]) {
exposuresWithCommission.push({
validatorId,
...eraCommissions[idx],
nominatorIndex: index,
});
}
}
}
return {
deriveEraExposure,
eraRewardPoints,
erasValidatorRewardOption,
exposuresWithCommission,
eraIndex: historicApi.registry.createType('EraIndex', idx + startEra),
};
});
return {
at,
erasPayouts: allEraData.map((eraData) => this.deriveEraPayouts(address, unclaimedOnly, eraData, isKusama)),
};
}
/**
* Fetch and derive payouts for `address` on AssetHub, handling migration boundary.
*
* This method splits era queries across the migration boundary:
* - Pre-migration eras: Query relay chain
* - Post-migration eras: Query AssetHub
*
* @param hash `BlockHash` to make call at
* @param address address of the _Stash_ account to get the payouts of
* @param depth number of eras to query at and below the specified era
* @param era the most recent era to query
* @param unclaimedOnly whether or not to only show unclaimed payouts
* @param currentEra The current era
* @param historicApi Historic api for querying past blocks
*/
async fetchAccountStakingPayoutAssetHub(hash, address, depth, era, unclaimedOnly, currentEra, historicApi) {
const { api } = this;
const specName = this.getSpecName().toLowerCase();
// Get migration boundaries for this chain
const migrationBoundaries = MIGRATION_BOUNDARIES[specName];
if (!migrationBoundaries) {
// Fallback to regular method if no migration boundaries defined
return this.fetchAccountStakingPayout(hash, address, depth, era, unclaimedOnly, currentEra, historicApi);
}
const sanitizedEra = era < 0 ? 0 : era;
const startEra = Math.max(0, sanitizedEra - (depth - 1));
const { number } = await api.rpc.chain.getHeader(hash);
const historyDepth = historicApi.consts.staking.historyDepth;
// Information is kept for eras in `[current_era - history_depth; current_era]`
if (historyDepth.toNumber() !== 0 && depth > historyDepth.toNumber()) {
throw new http_errors_1.BadRequest('Must specify a depth less than history_depth');
}
if (era - (depth - 1) < currentEra - historyDepth.toNumber() && historyDepth.toNumber() !== 0) {
// In scenarios where depth is not > historyDepth, but the user specifies an era
// and historyDepth combo that would lead to querying eras older than history depth
throw new http_errors_1.BadRequest('Must specify era and depth such that era - (depth - 1) is less ' +
'than or equal to current_era - history_depth.');
}
const at = {
height: number.unwrap().toString(10),
hash,
};
// Split era range at migration boundary
const preStartEra = startEra;
const preEndEra = Math.min(sanitizedEra, migrationBoundaries.relayChainLastEra);
const postStartEra = Math.max(startEra, migrationBoundaries.assetHubFirstEra);
const postEndEra = sanitizedEra;
const allEraPayouts = [];
// Query pre-migration eras from relay chain
if (preStartEra <= preEndEra) {
const relayChainPayouts = await this.fetchErasFromRelayChain(address, preStartEra, preEndEra, unclaimedOnly, migrationBoundaries);
allEraPayouts.push(...relayChainPayouts);
}
// Query post-migration eras from AssetHub
if (postStartEra <= postEndEra) {
const assetHubPayouts = await this.fetchErasFromAssetHub(historicApi, address, postStartEra, postEndEra, unclaimedOnly, at);
allEraPayouts.push(...assetHubPayouts);
}
return {
at,
erasPayouts: allEraPayouts.sort((a, b) => {
const aEra = typeof a.era === 'object' ? a.era.toNumber() : a.era;
const bEra = typeof b.era === 'object' ? b.era.toNumber() : b.era;
return aEra - bEra;
}),
};
}
/**
* Fetch general info about eras in the inclusive range `startEra` .. `era`.
*
* @param historicApi Historic api for querying past blocks
* @param startEra first era to get data for
* @param era the last era to get data for
* @param blockNumber block information to ensure compatibility with older eras
*/
async fetchAllErasGeneral(historicApi, startEra, era, blockNumber, isKusama) {
const allDeriveQuerys = [];
let nextEraStartBlock = Number(blockNumber.height);
let eraDurationInBlocks = 0;
const earlyErasBlockInfo = kusamaEarlyErasBlockInfo_json_1.default;
for (let e = startEra; e <= era; e += 1) {
const eraIndex = historicApi.registry.createType('EraIndex', e);
if (historicApi.query.staking.erasRewardPoints) {
const eraGeneralTuple = Promise.all([
this.deriveEraExposure(historicApi, eraIndex),
historicApi.query.staking.erasRewardPoints(eraIndex),
historicApi.query.staking.erasValidatorReward(eraIndex),
]);
allDeriveQuerys.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.
if (isKusama) {
// Retrieve the first block of the era following the given era in order
// to fetch the `Rewards` event at that block.
nextEraStartBlock = era === 0 ? earlyErasBlockInfo[era + 1].start : earlyErasBlockInfo[era].start;
}
else {
const sessionDuration = historicApi.consts.staking.sessionsPerEra.toNumber();
const epochDuration = historicApi.consts.babe.epochDuration.toNumber();
eraDurationInBlocks = sessionDuration * epochDuration;
}
const nextEraStartBlockHash = await this.api.rpc.chain.getBlockHash(nextEraStartBlock);
const currentEraEndBlockHash = era === 0
? await this.api.rpc.chain.getBlockHash(earlyErasBlockInfo[0].end)
: await this.api.rpc.chain.getBlockHash(earlyErasBlockInfo[era - 1].end);
let reward = historicApi.registry.createType('Option<u128>');
const [blockInfo, allRecords] = await Promise.all([
this.api.rpc.chain.getBlock(nextEraStartBlockHash),
historicApi.query.system.events(),
]);
blockInfo.block.extrinsics.forEach((index) => {
allRecords
.filter(({ phase }) => phase.isApplyExtrinsic && phase.asApplyExtrinsic.eq(index))
.forEach(({ event }) => {
if (event.method.toString() === 'Reward') {
const [dispatchInfo] = event.data;
reward = historicApi.registry.createType('Option<u128>', dispatchInfo.toString());
}
});
});
const points = this.fetchHistoricRewardPoints(currentEraEndBlockHash);
const rewardPromise = new Promise((resolve) => {
resolve(reward);
});
if (!isKusama) {
nextEraStartBlock = nextEraStartBlock - eraDurationInBlocks;
}
const eraGeneralTuple = Promise.all([this.deriveEraExposure(historicApi, eraIndex), points, rewardPromise]);
allDeriveQuerys.push(eraGeneralTuple);
}
}
return Promise.all(allDeriveQuerys);
}
async fetchHistoricRewardPoints(hash) {
const historicApi = await this.api.at(hash);
return historicApi.query.staking.currentEraPointsEarned();
}
/**
* Fetch the commission & staking ledger for each `validatorId` in `deriveErasExposures`.
*
* @param historicApi Historic api for querying past blocks
* @param address address of the _Stash_ account to get the payouts of
* @param startEra first era to get data for
* @param deriveErasExposures exposures per era for `address`
*/
fetchAllErasCommissions(historicApi, address, startEra, deriveErasExposures, isKusama) {
// Cache StakingLedger to reduce redundant queries to node
const validatorLedgerCache = {};
const allErasCommissions = deriveErasExposures.map((deriveEraExposure, idx) => {
const currEra = idx + startEra;
const nominatedExposures = this.deriveNominatedExposures(address, deriveEraExposure);
if (!nominatedExposures) {
return [];
}
const singleEraCommissions = nominatedExposures.map(({ validatorId }) => this.fetchCommissionAndLedger(historicApi, validatorId, currEra, validatorLedgerCache, isKusama));
return Promise.all(singleEraCommissions);
});
return Promise.all(allErasCommissions);
}
/**
* Derive all the payouts for `address` at `era`.
*
* @param address address of the _Stash_ account to get the payouts of
* @param era the era to query
* @param eraData data about the address and era we are calculating payouts for
*/
deriveEraPayouts(address, unclaimedOnly, { deriveEraExposure, eraRewardPoints, erasValidatorRewardOption, exposuresWithCommission, eraIndex }, isKusama) {
if (!exposuresWithCommission) {
return {
message: `${address} has no nominations for the era ${eraIndex.toString()}`,
};
}
if (erasValidatorRewardOption.isNone && eraIndex.toNumber() !== 0) {
const event = eraIndex.toNumber() > 517 ? 'ErasValidatorReward' : 'Reward';
return {
message: `No ${event} for the era ${eraIndex.toString()}`,
};
}
const totalEraRewardPoints = eraRewardPoints.total;
const totalEraPayout = eraIndex.toNumber() !== 0 ? erasValidatorRewardOption.unwrap() : this.api.registry.createType('BalanceOf', 0);
const calcPayout = calc_1.CalcPayout.from_params(totalEraRewardPoints.toNumber(), totalEraPayout.toString(10));
// Iterate through validators that this nominator backs and calculate payouts for the era
const payouts = [];
for (const { validatorId, commission: validatorCommission, validatorLedger, nominatorIndex, } of exposuresWithCommission) {
const totalValidatorRewardPoints = deriveEraExposure.validatorIndex
? this.extractTotalValidatorRewardPoints(eraRewardPoints, validatorId, deriveEraExposure.validatorIndex)
: this.extractTotalValidatorRewardPoints(eraRewardPoints, validatorId);
if (!totalValidatorRewardPoints || (totalValidatorRewardPoints === null || totalValidatorRewardPoints === void 0 ? void 0 : totalValidatorRewardPoints.toNumber()) === 0) {
// Nothing to do if there are no reward points for the validator
continue;
}
const { totalExposure, nominatorExposure } = this.extractExposure(address, validatorId, deriveEraExposure, nominatorIndex);
if (nominatorExposure === undefined) {
// This should not happen once at this point, but here for safety
continue;
}
if (!validatorLedger) {
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;
if (validatorLedger.legacyClaimedRewards) {
indexOfEra = validatorLedger.legacyClaimedRewards.indexOf(eraIndex);
}
else if (validatorLedger.claimedRewards) {
indexOfEra = validatorLedger.claimedRewards.indexOf(eraIndex);
}
else if (validatorLedger.lastReward) {
const lastReward = validatorLedger.lastReward;
if (lastReward.isSome) {
indexOfEra = lastReward.unwrap().toNumber();
}
else {
indexOfEra = -1;
}
}
else if (eraIndex.toNumber() < 518 && isKusama) {
indexOfEra = eraIndex.toNumber();
}
else {
indexOfEra = -1;
}
const claimed = Number.isInteger(indexOfEra) && indexOfEra !== -1;
if (unclaimedOnly && claimed) {
continue;
}
const nominatorStakingPayout = calcPayout.calc_payout(totalValidatorRewardPoints.toNumber(), validatorCommission.toNumber(), nominatorExposure.unwrap().toString(10), totalExposure.unwrap().toString(10), address === validatorId);
payouts.push({
validatorId,
nominatorStakingPayout,
claimed,
totalValidatorRewardPoints,
validatorCommission,
totalValidatorExposure: totalExposure.unwrap(),
nominatorExposure: nominatorExposure.unwrap(),
});
}
return {
era: eraIndex,
totalEraRewardPoints,
totalEraPayout,
payouts,
};
}
/**
* Fetch the `commission` and `StakingLedger` of `validatorId`.
*
* @param historicApi Historic api for querying past blocks
* @param validatorId accountId of a validator's _Stash_ account
* @param era the era to query
* @param validatorLedgerCache object mapping validatorId => StakingLedger to limit redundant queries
*/
async fetchCommissionAndLedger(historicApi, validatorId, era, validatorLedgerCache, isKusama) {
let commission;
let validatorLedger;
let commissionPromise;
const ancient = era < 518;
if (validatorId in validatorLedgerCache) {
validatorLedger = validatorLedgerCache[validatorId];
let prefs;
if (!ancient) {
prefs = await historicApi.query.staking.erasValidatorPrefs(era, validatorId);
commission = prefs.commission.unwrap();
}
else {
prefs = (await historicApi.query.staking.validators(validatorId));
commission = prefs[0].commission.unwrap();
}
}
else {
commissionPromise =
ancient && isKusama
? historicApi.query.staking.validators(validatorId)
: historicApi.query.staking.erasValidatorPrefs(era, validatorId);
const [prefs, validatorControllerOption] = await Promise.all([
commissionPromise,
historicApi.query.staking.bonded(validatorId),
]);
commission =
ancient && isKusama
? prefs[0].commission.unwrap()
: prefs.commission.unwrap();
if (validatorControllerOption.isNone) {
return {
commission,
};
}
const validatorLedgerOption = await historicApi.query.staking.ledger(validatorControllerOption.unwrap());
if (validatorLedgerOption.isNone) {
return {
commission,
};
}
validatorLedger = validatorLedgerOption.unwrap();
validatorLedgerCache[validatorId] = validatorLedger;
}
return { commission, validatorLedger };
}
/**
* Copyright 2025 via polkadot-js/api
* The following code was adopted by https://github.com/polkadot-js/api/blob/3bdf49b0428a62f16b3222b9a31bfefa43c1ca55/packages/api-derive/src/staking/erasExposure.ts.
*
* The original version uses the base ApiDerive implementation which does not include the ApiDecoration implementation.
* It is required in this version to query older blocks for their historic data.
*
* @param historicApi Historic api for querying past blocks
* @param eraIndex index of the era to query
*/
async deriveEraExposure(historicApi, eraIndex) {
function mapStakers(era, stakers, validatorIndex, validatorsOverviewEntries) {
const nominators = {};
const validators = {};
const validatorsOverview = {};
const validatorLookupMap = {};
if (validatorsOverviewEntries) {
for (const validator of validatorsOverviewEntries) {
const validatorKey = validator[0];
const valKey = validatorKey.toHuman();
if (valKey) {
validatorLookupMap[valKey[1].toString()] = validator[1];
}
}
}
stakers.forEach(([key, exposure]) => {
const validatorId = key.args[1].toString();
if (validatorLookupMap[validatorId]) {
validatorsOverview[validatorId] = validatorLookupMap[validatorId];
}
validators[validatorId] = exposure;
const individualExposure = exposure.others
? exposure.others
: exposure.isSome
? exposure.unwrap().others
: [];
individualExposure.forEach(({ who }, validatorIndex) => {
const nominatorId = who.toString();
nominators[nominatorId] = nominators[nominatorId] || [];
nominators[nominatorId].push({ validatorId, validatorIndex });
});
});
if (Object.keys(validatorIndex).length > 0) {
return { era, nominators, validators, validatorIndex, validatorsOverview };
}
else {
return { era, nominators, validators, validatorsOverview };
}
}
let storageKeys = [];
let validatorsOverviewEntries = [];
const validatorIndex = {};
if (historicApi.query.staking.erasStakersClipped) {
storageKeys = await historicApi.query.staking.erasStakersClipped.entries(eraIndex);
}
else if (historicApi.query.staking.currentElected) {
const validators = (await historicApi.query.staking.currentElected());
const validatorId = [];
validators.map((validator, index) => {
validatorIndex[validator.toString()] = index;
validatorId.push(validator);
});
let eraExposure = {};
for (const validator of validatorId) {
const storageKey = {
args: [eraIndex, validator],
};
eraExposure = (await historicApi.query.staking.stakers(validator));
storageKeys.push([storageKey, eraExposure]);
}
}
if (storageKeys.length === 0 && !!historicApi.query.staking.erasStakersPaged) {
storageKeys = await historicApi.query.staking.erasStakersPaged.entries(eraIndex);
validatorsOverviewEntries = await historicApi.query.staking.erasStakersOverview.entries(eraIndex);
}
return mapStakers(eraIndex, storageKeys, validatorIndex, validatorsOverviewEntries);
}
/**
* Extract the reward points of `validatorId` from `EraRewardPoints`.
*
* @param eraRewardPoints
* @param validatorId accountId of a validator's _Stash_ account
* @param validatorIndex index of the validator in relation to the `EraPoints`
* array
* */
extractTotalValidatorRewardPoints(eraRewardPoints, validatorId, validatorIndex) {
// Ideally we would just use the map's `get`, but that does not seem to be working here
if (validatorIndex === undefined) {
for (const [id, points] of eraRewardPoints.individual.entries()) {
if (id.toString() === validatorId) {
return points;
}
}
}
else {
for (const [id, points] of eraRewardPoints.individual.entries()) {
if (id.toString() === validatorIndex[validatorId.toString()].toString()) {
return points;
}
}
}
return;
}
/**
* Extract the exposure of `address` and `totalExposure`
* from polkadot-js's `deriveEraExposure`.
*
* @param address address of the _Stash_ account to get the exposure of behind `validatorId`
* @param validatorId accountId of a validator's _Stash_ account
* @param deriveEraExposure result of deriveEraExposure
*/
extractExposure(address, validatorId, deriveEraExposure, nominatorIndex) {
var _a;
// Get total stake behind validator
let totalExposure = {};
if (deriveEraExposure.validators[validatorId].total) {
totalExposure = deriveEraExposure.validators[validatorId].total;
}
else if (deriveEraExposure.validatorsOverview) {
totalExposure = deriveEraExposure.validatorsOverview[validatorId].isSome
? deriveEraExposure.validatorsOverview[validatorId].unwrap().total
: {};
}
// Get nominators stake behind validator
let exposureAllNominators = [];
if (deriveEraExposure.validators[validatorId].others) {
exposureAllNominators = deriveEraExposure.validators[validatorId].others;
}
else {
const exposure = deriveEraExposure.validators[validatorId];
exposureAllNominators = exposure.isSome
? exposure.unwrap()
.others
: [];
}
let nominatorExposure;
// check `address === validatorId` is when the validator is also the nominator we are getting payouts for
if (address === validatorId && deriveEraExposure.validators[address].own) {
nominatorExposure = deriveEraExposure.validators[address].own;
}
else if (address === validatorId && deriveEraExposure.validatorsOverview) {
nominatorExposure = deriveEraExposure.validatorsOverview[address].isSome
? deriveEraExposure.validatorsOverview[address].unwrap().own
: {};
}
else {
// We need to account for the rare cases where a nominator has multiple nominations (with different stakes)
// on the same validator and at the same era.
const nominatorInstancesLen = exposureAllNominators.filter((exposure) => exposure.who.toString() === address).length;
const nominatorInstances = exposureAllNominators.filter((exposure) => exposure.who.toString() === address);
if (nominatorInstancesLen > 1) {
nominatorExposure = nominatorInstances[nominatorIndex].value;
}
else {
nominatorExposure = (_a = exposureAllNominators.find((exposure) => exposure.who.toString() === address)) === null || _a === void 0 ? void 0 : _a.value;
}
}
return {
totalExposure,
nominatorExposure,
};
}
/**
* Derive the list of validators nominated by `address`. Note: we count validators as nominating
* themself.
*
* @param address address of the _Stash_ account to get the payouts of
* @param deriveEraExposure result of deriveEraExposure
*/
deriveNominatedExposures(address, deriveEraExposure) {
var _a;
let nominatedExposures = (_a = deriveEraExposure.nominators[address]) !== null && _a !== void 0 ? _a : [];
if (deriveEraExposure.validators[address]) {
// We treat an `address` that is a validator as nominating itself
nominatedExposures = nominatedExposures.concat({
validatorId: address,
// We put in an arbitrary number because we do not use the index
validatorIndex: 9999,
});
}
return nominatedExposures;
}
/**
* Fetch era payouts from relay chain for pre-migration eras
*/
async fetchErasFromRelayChain(address, startEra, endEra, unclaimedOnly, migrationBoundaries) {
const relayChainApis = apiRegistry_1.ApiPromiseRegistry.getApiByType('relay');
if (!(relayChainApis === null || relayChainApis === void 0 ? void 0 : relayChainApis.length)) {
throw new Error('Relay chain API not found for pre-migration era queries');
}
const relayChainApi = relayChainApis[0].api;
const isKusama = relayChainApi.runtimeVersion.specName.toString().toLowerCase() === 'kusama';
// Use a representative block from the migration period to create historic API
const migrationBlockHash = await relayChainApi.rpc.chain.getBlockHash(migrationBoundaries.relayMigrationStartedAt - 1);
const historicRelayApi = await relayChainApi.at(migrationBlockHash);
// Create block info for relay chain
const at = {
height: migrationBoundaries.relayMigrationEndedAt.toString(),
hash: migrationBlockHash,
};
// Fetch general era data from relay chain
const allErasGeneral = await this.fetchAllErasGeneral(historicRelayApi, startEra, endEra, at, isKusama);
// Fetch commissions from relay chain
const allErasCommissions = await this.fetchAllErasCommissions(historicRelayApi, address, startEra, allErasGeneral.map((eraGeneral) => eraGeneral[0]), isKusama).catch((err) => {
throw this.createHttpErrorForAddr(address, err);
});
// Process era data to create payouts
const allEraData = allErasGeneral.map(([deriveEraExposure, eraRewardPoints, erasValidatorRewardOption], idx) => {
const eraCommissions = allErasCommissions[idx];
const nominatedExposures = this.deriveNominatedExposures(address, deriveEraExposure);
const exposuresWithCommission = [];
if (nominatedExposures) {
for (let idx = 0; idx < nominatedExposures.length; idx++) {
let index = 0;
const { validatorId } = nominatedExposures[idx];
const nominatorInstances = nominatedExposures.filter((exposure) => exposure.validatorId.toString() === validatorId).length;
const exposuresValidatorLen = exposuresWithCommission.filter((exposure) => exposure.validatorId.toString() === validatorId).length;
if (nominatorInstances > 1) {
index = exposuresValidatorLen;
}
if (eraCommissions[idx]) {
exposuresWithCommission.push({
validatorId,
...eraCommissions[idx],
nominatorIndex: index,
});
}
}
}
return {
deriveEraExposure,
eraRewardPoints,
erasValidatorRewardOption,
exposuresWithCommission,
eraIndex: historicRelayApi.registry.createType('EraIndex', idx + startEra),
};
});
return allEraData
.map((eraData) => this.deriveEraPayouts(address, unclaimedOnly, eraData, isKusama))
.filter((payout) => !('message' in payout));
}
/**
* Fetch era payouts from AssetHub for post-migration eras
*/
async fetchErasFromAssetHub(historicApi, address, startEra, endEra, unclaimedOnly, at) {
const specName = this.getSpecName().toLowerCase();
const isKusama = specName === 'kusama';
// Fetch general era data from AssetHub
const allErasGeneral = await this.fetchAllErasGeneral(historicApi, startEra, endEra, at, isKusama);
// Fetch commissions from AssetHub
const allErasCommissions = await this.fetchAllErasCommissions(historicApi, address, startEra, allErasGeneral.map((eraGeneral) => eraGeneral[0]), isKusama).catch((err) => {
throw this.createHttpErrorForAddr(address, err);
});
// Process era data to create payouts
const allEraData = allErasGeneral.map(([deriveEraExposure, eraRewardPoints, erasValidatorRewardOption], idx) => {
const eraCommissions = allErasCommissions[idx];
const nominatedExposures = this.deriveNominatedExposures(address, deriveEraExposure);
const exposuresWithCommission = [];
if (nominatedExposures) {
for (let idx = 0; idx < nominatedExposures.length; idx++) {
let index = 0;
const { validatorId } = nominatedExposures[idx];
const nominatorInstances = nominatedExposures.filter((exposure) => exposure.validatorId.toString() === validatorId).length;
const exposuresValidatorLen = exposuresWithCommission.filter((exposure) => exposure.validatorId.toString() === validatorId).length;
if (nominatorInstances > 1) {
index = exposuresValidatorLen;
}
if (eraCommissions[idx]) {
exposuresWithCommission.push({
validatorId,
...eraCommissions[idx],
nominatorIndex: index,
});
}
}
}
return {
deriveEraExposure,
eraRewardPoints,
erasValidatorRewardOption,
exposuresWithCommission,
eraIndex: historicApi.registry.createType('EraIndex', idx + startEra),
};
});
return allEraData
.map((eraData) => this.deriveEraPayouts(address, unclaimedOnly, eraData, isKusama))
.filter((payout) => !('message' in payout));
}
}
exports.AccountsStakingPayoutsService = AccountsStakingPayoutsService;
//# sourceMappingURL=AccountsStakingPayoutsService.js.map