diff --git a/packages/api-derive/src/staking/ownExposure.ts b/packages/api-derive/src/staking/ownExposure.ts index b20c057a9387..2b8ac71e1ac7 100644 --- a/packages/api-derive/src/staking/ownExposure.ts +++ b/packages/api-derive/src/staking/ownExposure.ts @@ -4,7 +4,7 @@ import type { Observable } from 'rxjs'; import type { Option, u32 } from '@polkadot/types'; import type { EraIndex } from '@polkadot/types/interfaces'; -import type { SpStakingExposurePage } from '@polkadot/types/lookup'; +import type { SpStakingExposure, SpStakingExposurePage, SpStakingPagedExposureMetadata } from '@polkadot/types/lookup'; import type { AnyNumber } from '@polkadot/types-codec/types'; import type { DeriveApi, DeriveOwnExposure } from '../types.js'; @@ -15,13 +15,27 @@ import { erasHistoricApplyAccount } from './util.js'; export function _ownExposures (instanceId: string, api: DeriveApi): (accountId: Uint8Array | string, eras: EraIndex[], withActive: boolean, page: u32 | AnyNumber) => Observable { return memo(instanceId, (accountId: Uint8Array | string, eras: EraIndex[], _withActive: boolean, page: u32 | AnyNumber): Observable => { + const emptyStakingExposure = api.registry.createType('Exposure'); + // 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 emptyOptionPage = api.registry.createType>('Option'); + const emptyOptionMeta = api.registry.createType>('Option'); + return eras.length ? combineLatest([ // Backwards and forward compat for historical integrity when using `erasHistoricApplyAccount` - combineLatest(eras.map((e) => api.query.staking.erasStakersClipped(e, accountId))), - combineLatest(eras.map((e) => api.query.staking.erasStakers(e, accountId))), - combineLatest(eras.map((e) => api.query.staking.erasStakersPaged>(e, accountId, page))), - combineLatest(eras.map((e) => api.query.staking.erasStakersOverview(e, accountId))) + api.query.staking.erasStakersClipped + ? combineLatest(eras.map((e) => api.query.staking.erasStakersClipped(e, accountId))) + : of(eras.map((_) => emptyStakingExposure)), + api.query.staking.erasStakers + ? combineLatest(eras.map((e) => api.query.staking.erasStakers(e, accountId))) + : of(eras.map((_) => emptyStakingExposure)), + api.query.staking.erasStakersPaged + ? combineLatest(eras.map((e) => api.query.staking.erasStakersPaged>(e, accountId, page))) + : of(eras.map((_) => emptyOptionPage)), + api.query.staking.erasStakersOverview + ? combineLatest(eras.map((e) => api.query.staking.erasStakersOverview(e, accountId))) + : of(eras.map((_) => emptyOptionMeta)) ]).pipe( map(([clp, exp, paged, expMeta]): DeriveOwnExposure[] => eras.map((era, index) => ({ clipped: clp[index], era, exposure: exp[index], exposureMeta: expMeta[index], exposurePaged: paged[index] })) diff --git a/packages/api-derive/src/staking/query.ts b/packages/api-derive/src/staking/query.ts index 0e65501ffcb2..6a5799b3f161 100644 --- a/packages/api-derive/src/staking/query.ts +++ b/packages/api-derive/src/staking/query.ts @@ -4,7 +4,7 @@ import type { Observable } from 'rxjs'; import type { Option, u32, Vec } from '@polkadot/types'; import type { AccountId, EraIndex } from '@polkadot/types/interfaces'; -import type { PalletStakingNominations, PalletStakingRewardDestination, PalletStakingStakingLedger, PalletStakingValidatorPrefs, SpStakingExposurePage, SpStakingPagedExposureMetadata } from '@polkadot/types/lookup'; +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'; @@ -24,11 +24,12 @@ function filterClaimedRewards (api: DeriveApi, cl: number[]): Vec { return api.registry.createType('Vec', cl.filter((c) => c !== -1)); } -function parseDetails (api: DeriveApi, stashId: AccountId, controllerIdOpt: Option | null, nominatorsOpt: Option, rewardDestinationOpts: Option | PalletStakingRewardDestination, validatorPrefs: PalletStakingValidatorPrefs, exposure: Option, stakingLedgerOpt: Option, exposureMeta: Option, claimedRewards: number[]): DeriveStakingQuery { +function parseDetails (api: DeriveApi, stashId: AccountId, controllerIdOpt: Option | null, nominatorsOpt: Option, rewardDestinationOpts: Option | PalletStakingRewardDestination, validatorPrefs: PalletStakingValidatorPrefs, exposure: Option, stakingLedgerOpt: Option, exposureMeta: Option, claimedRewards: number[], exposureEraStakers: SpStakingExposure): DeriveStakingQuery { return { accountId: stashId, claimedRewardsEras: filterClaimedRewards(api, claimedRewards), controllerId: controllerIdOpt?.unwrapOr(null) || null, + exposureEraStakers, exposureMeta, exposurePaged: exposure, nominators: nominatorsOpt.isSome @@ -64,12 +65,15 @@ function getLedgers (api: DeriveApi, optIds: (Option | null)[], { wit ); } -function getStashInfo (api: DeriveApi, stashIds: AccountId[], activeEra: EraIndex, { withClaimedRewardsEras, withController, withDestination, withExposure, withExposureMeta, withLedger, withNominations, withPrefs }: StakingQueryFlags, page: u32 | AnyNumber): Observable<[(Option | null)[], Option[], Option[], PalletStakingValidatorPrefs[], Option[], Option[], number[][]]> { +function getStashInfo (api: DeriveApi, stashIds: AccountId[], activeEra: EraIndex, { withClaimedRewardsEras, withController, withDestination, withExposure, withExposureErasStakersLegacy, withExposureMeta, withLedger, withNominations, withPrefs }: StakingQueryFlags, page: u32 | AnyNumber): Observable<[(Option | null)[], Option[], Option[], PalletStakingValidatorPrefs[], Option[], Option[], number[][], SpStakingExposure[]]> { const emptyNoms = api.registry.createType>('Option'); const emptyRewa = api.registry.createType>('RewardDestination'); - const emptyExpo = api.registry.createType>('Option'); + const emptyExpoEraStakers = api.registry.createType('Exposure'); const emptyPrefs = api.registry.createType('ValidatorPrefs'); - const emptyExpoMeta = api.registry.createType>('Option'); + // 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'); + const emptyExpoMeta = api.registry.createType>('Option'); const emptyClaimedRewards = [-1]; const depth = Number(api.consts.staking.historyDepth.toNumber()); @@ -94,13 +98,13 @@ function getStashInfo (api: DeriveApi, stashIds: AccountId[], activeEra: EraInde withPrefs ? combineLatest(stashIds.map((s) => api.query.staking.validators(s))) : of(stashIds.map(() => emptyPrefs)), - withExposure + withExposure && api.query.staking.erasStakersPaged ? combineLatest(stashIds.map((s) => api.query.staking.erasStakersPaged>(activeEra, s, page))) : of(stashIds.map(() => emptyExpo)), - withExposureMeta + withExposureMeta && api.query.staking.erasStakersOverview ? combineLatest(stashIds.map((s) => api.query.staking.erasStakersOverview(activeEra, s))) : of(stashIds.map(() => emptyExpoMeta)), - withClaimedRewardsEras + withClaimedRewardsEras && api.query.staking.claimedRewards ? combineLatest(stashIds.map((s) => combineLatest([ combineLatest(eras.map((e) => api.query.staking.claimedRewards(e, s))), @@ -122,17 +126,20 @@ function getStashInfo (api: DeriveApi, stashIds: AccountId[], activeEra: EraInde }); }) ) - : of(stashIds.map(() => emptyClaimedRewards)) + : 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 { return getStashInfo(api, stashIds, activeEra, flags, page).pipe( - switchMap(([controllerIdOpt, nominatorsOpt, rewardDestination, validatorPrefs, exposure, exposureMeta, claimedRewardsEras]): Observable => + switchMap(([controllerIdOpt, nominatorsOpt, rewardDestination, validatorPrefs, exposure, exposureMeta, claimedRewardsEras, exposureEraStakers]): Observable => 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]) + parseDetails(api, stashId, controllerIdOpt[index], nominatorsOpt[index], rewardDestination[index], validatorPrefs[index], exposure[index], stakingLedgerOpts[index], exposureMeta[index], claimedRewardsEras[index], exposureEraStakers[index]) ) ) ) diff --git a/packages/api-derive/src/staking/types.ts b/packages/api-derive/src/staking/types.ts index 02fa9297914a..02bbccef5e63 100644 --- a/packages/api-derive/src/staking/types.ts +++ b/packages/api-derive/src/staking/types.ts @@ -118,6 +118,8 @@ export interface DeriveStakingValidators { export interface DeriveStakingStash { controllerId: AccountId | null; + // Legacy Support for erasStakers + exposureEraStakers: SpStakingExposure; exposurePaged: Option; exposureMeta: Option; nominators: AccountId[]; @@ -162,6 +164,7 @@ export interface StakingQueryFlags { withController?: boolean; withDestination?: boolean; withExposure?: boolean; + withExposureErasStakersLegacy?: boolean, withLedger?: boolean; withNominations?: boolean; withPrefs?: boolean; diff --git a/packages/api-derive/src/staking/validators.ts b/packages/api-derive/src/staking/validators.ts index 90362f0129eb..c266776e0e4d 100644 --- a/packages/api-derive/src/staking/validators.ts +++ b/packages/api-derive/src/staking/validators.ts @@ -11,6 +11,7 @@ import { memo } from '../util/index.js'; export function nextElected (instanceId: string, api: DeriveApi): () => Observable { return memo(instanceId, (): Observable => + // Compatibility for future generation changes in staking. api.query.staking.erasStakersPaged ? api.derive.session.indexes().pipe( // only populate for next era in the last session, so track both here - entries are not @@ -18,7 +19,14 @@ export function nextElected (instanceId: string, api: DeriveApi): () => Observab switchMap(({ currentEra }) => api.query.staking.erasStakersPaged.keys(currentEra)), map((keys) => keys.map(({ args: [, accountId] }) => accountId)) ) - : api.query.staking['currentElected']() + : api.query.staking.erasStakers + ? api.derive.session.indexes().pipe( + // only populate for next era in the last session, so track both here - entries are not + // subscriptions, so we need a trigger - currentIndex acts as that trigger to refresh + switchMap(({ currentEra }) => api.query.staking.erasStakers.keys(currentEra)), + map((keys) => keys.map(({ args: [, accountId] }) => accountId)) + ) + : api.query.staking['currentElected']() ); }