210 lines
9.9 KiB
TypeScript
210 lines
9.9 KiB
TypeScript
// Copyright 2017-2024 @polkadot/api-derive authors & contributors
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
import type { Observable } from 'rxjs';
|
|
import type { Option, StorageKey, u32, Vec } from '@polkadot/types';
|
|
import type { AccountId, EraIndex } from '@polkadot/types/interfaces';
|
|
import type { PalletStakingNominations, PalletStakingRewardDestination, PalletStakingStakingLedger, PalletStakingValidatorPrefs, SpStakingExposure, SpStakingExposurePage, SpStakingPagedExposureMetadata } from '@polkadot/types/lookup';
|
|
import type { AnyNumber } from '@polkadot/types-codec/types';
|
|
import type { DeriveApi, DeriveStakingQuery, StakingQueryFlags } from '../types.js';
|
|
|
|
import { combineLatest, map, of, switchMap } from 'rxjs';
|
|
|
|
import { firstMemo, memo } from '../util/index.js';
|
|
|
|
// handle compatibility between generations of structures
|
|
function rewardDestinationCompat (rewardDestination: PalletStakingRewardDestination | Option<PalletStakingRewardDestination>): PalletStakingRewardDestination | null {
|
|
// We ensure the type is an Option by checking if isSome is a boolean. When isSome doesn't exist it will always return undefined.
|
|
return typeof (rewardDestination as Option<PalletStakingRewardDestination>).isSome === 'boolean'
|
|
? (rewardDestination as Option<PalletStakingRewardDestination>).unwrapOr(null)
|
|
: (rewardDestination as PalletStakingRewardDestination);
|
|
}
|
|
|
|
function filterClaimedRewards (api: DeriveApi, cl: number[]): Vec<u32> {
|
|
return api.registry.createType('Vec<u32>', cl.filter((c) => c !== -1));
|
|
}
|
|
|
|
function filterRewards (stashIds: AccountId[], eras: number[], claimedRewards: [StorageKey<[u32, AccountId]>, Vec<u32>][], stakersOverview: [StorageKey<[u32, AccountId]>, Option<SpStakingPagedExposureMetadata>][]): number[][] {
|
|
const claimedData: Record<string, Map<number, u32[]>> = {};
|
|
const overviewData: Record<string, Map<number, u32>> = {};
|
|
const ids = stashIds.map((i) => i.toString());
|
|
|
|
claimedRewards.forEach(([keys, rewards]) => {
|
|
const id = keys.args[1].toString();
|
|
const era = keys.args[0].toNumber();
|
|
|
|
if (ids.includes(id)) {
|
|
if (claimedData[id]) {
|
|
claimedData[id].set(era, rewards.toArray());
|
|
} else {
|
|
claimedData[id] = new Map();
|
|
claimedData[id].set(era, rewards.toArray());
|
|
}
|
|
}
|
|
});
|
|
|
|
stakersOverview.forEach(([keys, overview]) => {
|
|
const id = keys.args[1].toString();
|
|
const era = keys.args[0].toNumber();
|
|
|
|
if (ids.includes(id) && overview.isSome) {
|
|
if (overviewData[id]) {
|
|
overviewData[id].set(era, overview.unwrap().pageCount);
|
|
} else {
|
|
overviewData[id] = new Map();
|
|
overviewData[id].set(era, overview.unwrap().pageCount);
|
|
}
|
|
}
|
|
});
|
|
|
|
return stashIds.map((id) => {
|
|
const rewardsPerEra = claimedData[id.toString()];
|
|
const overviewPerEra = overviewData[id.toString()];
|
|
|
|
return eras.map((era) => {
|
|
if (rewardsPerEra && rewardsPerEra.has(era) && overviewPerEra && overviewPerEra.has(era)) {
|
|
const rewards = rewardsPerEra.get(era) as unknown as u32[];
|
|
const pageCount = overviewPerEra.get(era) as unknown as u32;
|
|
|
|
return rewards.length === pageCount.toNumber()
|
|
? era
|
|
: -1;
|
|
}
|
|
|
|
return -1;
|
|
});
|
|
});
|
|
}
|
|
|
|
function parseDetails (api: DeriveApi, stashId: AccountId, controllerIdOpt: Option<AccountId> | null, nominatorsOpt: Option<PalletStakingNominations>, rewardDestinationOpts: Option<PalletStakingRewardDestination> | PalletStakingRewardDestination, validatorPrefs: PalletStakingValidatorPrefs, exposure: Option<SpStakingExposurePage>, stakingLedgerOpt: Option<PalletStakingStakingLedger>, exposureMeta: Option<SpStakingPagedExposureMetadata>, claimedRewards: number[], exposureEraStakers: SpStakingExposure): DeriveStakingQuery {
|
|
return {
|
|
accountId: stashId,
|
|
claimedRewardsEras: filterClaimedRewards(api, claimedRewards),
|
|
controllerId: controllerIdOpt?.unwrapOr(null) || null,
|
|
exposureEraStakers,
|
|
exposureMeta,
|
|
exposurePaged: exposure,
|
|
nominators: nominatorsOpt.isSome
|
|
? nominatorsOpt.unwrap().targets
|
|
: [],
|
|
rewardDestination: rewardDestinationCompat(rewardDestinationOpts),
|
|
stakingLedger: stakingLedgerOpt.unwrapOrDefault(),
|
|
stashId,
|
|
validatorPrefs
|
|
};
|
|
}
|
|
|
|
function getLedgers (api: DeriveApi, optIds: (Option<AccountId> | null)[], { withLedger = false }: StakingQueryFlags): Observable<Option<PalletStakingStakingLedger>[]> {
|
|
const ids = optIds
|
|
.filter((o): o is Option<AccountId> => withLedger && !!o && o.isSome)
|
|
.map((o) => o.unwrap());
|
|
const emptyLed = api.registry.createType<Option<PalletStakingStakingLedger>>('Option<StakingLedger>');
|
|
|
|
return (
|
|
ids.length
|
|
? combineLatest(ids.map((s) => api.query.staking.ledger(s)))
|
|
: of([])
|
|
).pipe(
|
|
map((optLedgers): Option<PalletStakingStakingLedger>[] => {
|
|
let offset = -1;
|
|
|
|
return optIds.map((o): Option<PalletStakingStakingLedger> =>
|
|
o && o.isSome
|
|
? optLedgers[++offset] || emptyLed
|
|
: emptyLed
|
|
);
|
|
})
|
|
);
|
|
}
|
|
|
|
function getStashInfo (api: DeriveApi, stashIds: AccountId[], activeEra: EraIndex, { withClaimedRewardsEras, withController, withDestination, withExposure, withExposureErasStakersLegacy, withExposureMeta, withLedger, withNominations, withPrefs }: StakingQueryFlags, page: u32 | AnyNumber): Observable<[(Option<AccountId> | null)[], Option<PalletStakingNominations>[], Option<PalletStakingRewardDestination>[], PalletStakingValidatorPrefs[], Option<SpStakingExposurePage>[], Option<SpStakingPagedExposureMetadata>[], number[][], SpStakingExposure[]]> {
|
|
const emptyNoms = api.registry.createType<Option<PalletStakingNominations>>('Option<Nominations>');
|
|
const emptyRewa = api.registry.createType<Option<PalletStakingRewardDestination>>('RewardDestination');
|
|
const emptyExpoEraStakers = api.registry.createType<SpStakingExposure>('Exposure');
|
|
const emptyPrefs = api.registry.createType<PalletStakingValidatorPrefs>('ValidatorPrefs');
|
|
// The reason we don't explicitly make the actual types is for compatibility. If the chain doesn't have the noted type it will fail
|
|
// on construction. Therefore we just make an empty option.
|
|
const emptyExpo = api.registry.createType<Option<SpStakingExposurePage>>('Option<Null>');
|
|
const emptyExpoMeta = api.registry.createType<Option<SpStakingPagedExposureMetadata>>('Option<Null>');
|
|
const emptyClaimedRewards = [-1];
|
|
|
|
const depth = Number(api.consts.staking.historyDepth.toNumber());
|
|
const eras = new Array(depth).fill(0).map((_, idx) => {
|
|
if (idx === 0) {
|
|
return activeEra.toNumber() - 1;
|
|
}
|
|
|
|
return activeEra.toNumber() - idx - 1;
|
|
});
|
|
|
|
return combineLatest([
|
|
withController || withLedger
|
|
? combineLatest(stashIds.map((s) => api.query.staking.bonded(s)))
|
|
: of(stashIds.map(() => null)),
|
|
withNominations
|
|
? combineLatest(stashIds.map((s) => api.query.staking.nominators(s)))
|
|
: of(stashIds.map(() => emptyNoms)),
|
|
withDestination
|
|
? combineLatest(stashIds.map((s) => api.query.staking.payee(s)))
|
|
: of(stashIds.map(() => emptyRewa)),
|
|
withPrefs
|
|
? combineLatest(stashIds.map((s) => api.query.staking.validators(s)))
|
|
: of(stashIds.map(() => emptyPrefs)),
|
|
withExposure && api.query.staking.erasStakersPaged
|
|
? combineLatest(stashIds.map((s) => api.query.staking.erasStakersPaged<Option<SpStakingExposurePage>>(activeEra, s, page)))
|
|
: of(stashIds.map(() => emptyExpo)),
|
|
withExposureMeta && api.query.staking.erasStakersOverview
|
|
? combineLatest(stashIds.map((s) => api.query.staking.erasStakersOverview(activeEra, s)))
|
|
: of(stashIds.map(() => emptyExpoMeta)),
|
|
withClaimedRewardsEras && api.query.staking.claimedRewards
|
|
? combineLatest([
|
|
api.query.staking.claimedRewards.entries<Vec<u32>>(),
|
|
api.query.staking.erasStakersOverview.entries<Option<SpStakingPagedExposureMetadata>>()
|
|
]).pipe(
|
|
map(([rewardsStorageVec, overviewStorageVec]) => filterRewards(stashIds, eras, rewardsStorageVec, overviewStorageVec))
|
|
)
|
|
: of(stashIds.map(() => emptyClaimedRewards)),
|
|
withExposureErasStakersLegacy && api.query.staking.erasStakers
|
|
? combineLatest(stashIds.map((s) => api.query.staking.erasStakers(activeEra, s)))
|
|
: of(stashIds.map(() => emptyExpoEraStakers))
|
|
]);
|
|
}
|
|
|
|
function getBatch (api: DeriveApi, activeEra: EraIndex, stashIds: AccountId[], flags: StakingQueryFlags, page: u32 | AnyNumber): Observable<DeriveStakingQuery[]> {
|
|
return getStashInfo(api, stashIds, activeEra, flags, page).pipe(
|
|
switchMap(([controllerIdOpt, nominatorsOpt, rewardDestination, validatorPrefs, exposure, exposureMeta, claimedRewardsEras, exposureEraStakers]): Observable<DeriveStakingQuery[]> =>
|
|
getLedgers(api, controllerIdOpt, flags).pipe(
|
|
map((stakingLedgerOpts) =>
|
|
stashIds.map((stashId, index) =>
|
|
parseDetails(api, stashId, controllerIdOpt[index], nominatorsOpt[index], rewardDestination[index], validatorPrefs[index], exposure[index], stakingLedgerOpts[index], exposureMeta[index], claimedRewardsEras[index], exposureEraStakers[index])
|
|
)
|
|
)
|
|
)
|
|
)
|
|
);
|
|
}
|
|
|
|
//
|
|
/**
|
|
* @description From a stash, retrieve the controllerId and all relevant details
|
|
*/
|
|
export const query = /*#__PURE__*/ firstMemo(
|
|
(api: DeriveApi, accountId: Uint8Array | string, flags: StakingQueryFlags, page?: u32) =>
|
|
api.derive.staking.queryMulti([accountId], flags, page)
|
|
);
|
|
|
|
export function queryMulti (instanceId: string, api: DeriveApi): (accountIds: (Uint8Array | string)[], flags: StakingQueryFlags, page?: u32 | AnyNumber) => Observable<DeriveStakingQuery[]> {
|
|
return memo(instanceId, (accountIds: (Uint8Array | string)[], flags: StakingQueryFlags, page?: u32 | AnyNumber): Observable<DeriveStakingQuery[]> =>
|
|
api.derive.session.indexes().pipe(
|
|
switchMap(({ activeEra }): Observable<DeriveStakingQuery[]> => {
|
|
const stashIds = accountIds.map((a) => api.registry.createType('AccountId', a));
|
|
const p = page || 0;
|
|
|
|
return stashIds.length
|
|
? getBatch(api, activeEra, stashIds, flags, p)
|
|
: of([]);
|
|
})
|
|
)
|
|
);
|
|
}
|