Files
api/packages/api-derive/src/staking/info.ts
T

191 lines
7.0 KiB
TypeScript

// Copyright 2017-2019 @polkadot/api-derive authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { AccountId, Balance, BlockNumber } from '@polkadot/types/interfaces/runtime';
import { Keys } from '@polkadot/types/interfaces/session';
import { Exposure, RewardDestination, StakingLedger, UnlockChunk, ValidatorPrefs } from '@polkadot/types/interfaces/staking';
import { Codec } from '@polkadot/types/types';
import { ApiInterfaceRx } from '@polkadot/api/types';
import { DerivedStaking, DerivedUnlocking } from '../types';
import BN from 'bn.js';
import { combineLatest, Observable, of } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';
import { createType, Option, Vec } from '@polkadot/types';
import { isUndefined } from '@polkadot/util';
import { bestNumber } from '../chain/bestNumber';
import { eraLength } from '../session/eraLength';
import { recentlyOffline } from './recentlyOffline';
import { drr } from '../util/drr';
type MultiResult = [Option<AccountId> | Vec<[AccountId, Keys] & Codec>, Option<StakingLedger>, [AccountId[]], RewardDestination, Exposure, [ValidatorPrefs], Option<Keys>?];
// groups the supplied chunks by era, i.e. { [era]: BN(total of values) }
function groupByEra (list: UnlockChunk[]): Record<string, BN> {
return list.reduce((map: Record<string, BN>, { era, value }): Record<string, BN> => {
const key = era.toString();
map[key] = !map[key]
? value.unwrap()
: map[key].add(value.unwrap());
return map;
}, {});
}
// calculate the remining blocks in a specific unlock era
function remainingBlocks (era: BN, eraLength: BN, bestNumber: BlockNumber): BlockNumber {
const remaining = eraLength.mul(era).sub(bestNumber);
return createType('BlockNumber', remaining.lten(0)
? new BN(0)
: remaining
);
}
function calculateUnlocking (stakingLedger: StakingLedger | undefined, eraLength: BN, bestNumber: BlockNumber): DerivedUnlocking[] | undefined {
if (isUndefined(stakingLedger)) {
return undefined;
}
const unlockingChunks = stakingLedger.unlocking.filter(({ era }): boolean =>
remainingBlocks(era.unwrap(), eraLength, bestNumber).gtn(0)
);
if (!unlockingChunks.length) {
return undefined;
}
// group the Unlockchunks that have the same era and sum their values
const groupedResult = groupByEra(unlockingChunks);
const results = Object.entries(groupedResult).map(([eraString, value]): DerivedUnlocking => ({
value: createType('Balance', value),
remainingBlocks: remainingBlocks(createType('BlockNumber', eraString), eraLength, bestNumber)
}));
return results.length ? results : undefined;
}
function redeemableSum (stakingLedger: StakingLedger | undefined, eraLength: BN, bestNumber: BlockNumber): Balance {
if (isUndefined(stakingLedger)) {
return createType('Balance');
}
return createType('Balance', stakingLedger.unlocking.reduce((total, { era, value }): BN => {
return remainingBlocks(era.unwrap(), eraLength, bestNumber).eqn(0)
? total.add(value.unwrap())
: total;
}, new BN(0)));
}
function unwrapSessionIds (stashId: AccountId, queuedKeys: Option<AccountId> | Vec<[AccountId, Keys] & Codec>, nextKeys: Option<Keys>): { nextSessionIds: AccountId[]; nextSessionId?: AccountId; sessionIds: AccountId[]; sessionId?: AccountId } {
// for 2.x we have a Vec<(ValidatorId,Keys)> of the keys
if (Array.isArray(queuedKeys)) {
const sessionIds = (queuedKeys.find(([currentId]): boolean =>
currentId.eq(stashId)
) || [undefined, [] as AccountId[]])[1];
const nextSessionIds = nextKeys.unwrapOr([] as AccountId[]);
return {
nextSessionId: nextSessionIds[0],
nextSessionIds,
sessionId: sessionIds[0],
sessionIds
};
}
// substrate 1.x
const nextSessionIds = queuedKeys.isSome
? [queuedKeys.unwrap()]
: [];
return {
nextSessionId: nextSessionIds[0],
nextSessionIds,
sessionId: nextSessionIds[0],
sessionIds: nextSessionIds
};
}
function retrieveMulti (api: ApiInterfaceRx, stashId: AccountId, controllerId: AccountId): Observable<MultiResult> {
// A bit horrible, since we push below to forge everything into 1 query - we do this we we end up with one
// query to retrieve everything
const queries: Array<any> = [
api.query.session.queuedKeys
? [api.query.session.queuedKeys]
: [api.query.session.nextKeyFor, controllerId],
[api.query.staking.ledger, controllerId],
[api.query.staking.nominators, stashId],
[api.query.staking.payee, stashId],
[api.query.staking.stakers, stashId],
[api.query.staking.validators, stashId]
];
//
if (api.query.session.nextKeys) {
queries.push([api.query.session.nextKeys, [api.consts.session.dedupKeyPrefix, stashId]]);
}
return api.queryMulti(queries) as Observable<MultiResult>;
}
function retrieveInfo (api: ApiInterfaceRx, accountId: AccountId, stashId: AccountId, controllerId: AccountId): Observable<DerivedStaking> {
return combineLatest([
eraLength(api)(),
bestNumber(api)(),
recentlyOffline(api)(),
retrieveMulti(api, stashId, controllerId)
]).pipe(
map(([eraLength, bestNumber, recentlyOffline, [queuedKeys, _stakingLedger, [nominators], rewardDestination, stakers, [validatorPrefs], nextKeys = createType('Option<Keys>', null)]]): DerivedStaking => {
const stakingLedger = _stakingLedger.unwrapOr(undefined);
return {
accountId,
controllerId,
nominators,
offline: recentlyOffline[stashId.toString()],
redeemable: redeemableSum(stakingLedger, eraLength, bestNumber),
rewardDestination,
stakers,
stakingLedger,
stashId,
unlocking: calculateUnlocking(stakingLedger, eraLength, bestNumber),
validatorPrefs,
...unwrapSessionIds(stashId, queuedKeys, nextKeys)
};
})
);
}
/**
* @description From a stash, retrieve the controllerId and fill in all the relevant staking details
*/
export function info (api: ApiInterfaceRx): (_accountId: Uint8Array | string) => Observable<DerivedStaking> {
return (_accountId: Uint8Array | string): Observable<DerivedStaking> => {
const accountId = createType('AccountId', _accountId);
return (
// NOTE For 2.x-only support, only the first path is required, therefore we
// can replace this with `.bonded<Option<AccountId>>(accountId)` - in 2.x
// the session.validators return the stashes (as expected)
api.queryMulti<[Option<AccountId>, Option<StakingLedger>]>([
[api.query.staking.bonded, accountId], // try to map to controller
[api.query.staking.ledger, accountId] // try to map to stash (1.x only)
])
).pipe(
switchMap(([controllerId, stakingLedger]): Observable<DerivedStaking> =>
controllerId.isSome
? retrieveInfo(api, accountId, accountId, controllerId.unwrap())
: stakingLedger.isSome
? retrieveInfo(api, accountId, stakingLedger.unwrap().stash, accountId)
: of({ accountId, nextSessionIds: [], sessionIds: [] })
),
drr()
);
};
}