// 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 | Vec<[AccountId, Keys] & Codec>, Option, [AccountId[]], RewardDestination, Exposure, [ValidatorPrefs], Option?]; // groups the supplied chunks by era, i.e. { [era]: BN(total of values) } function groupByEra (list: UnlockChunk[]): Record { return list.reduce((map: Record, { era, value }): Record => { 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 | Vec<[AccountId, Keys] & Codec>, nextKeys: Option): { 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 { // 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 = [ 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; } function retrieveInfo (api: ApiInterfaceRx, accountId: AccountId, stashId: AccountId, controllerId: AccountId): Observable { 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', 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 { return (_accountId: Uint8Array | string): Observable => { 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>(accountId)` - in 2.x // the session.validators return the stashes (as expected) api.queryMulti<[Option, Option]>([ [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 => controllerId.isSome ? retrieveInfo(api, accountId, accountId, controllerId.unwrap()) : stakingLedger.isSome ? retrieveInfo(api, accountId, stakingLedger.unwrap().stash, accountId) : of({ accountId, nextSessionIds: [], sessionIds: [] }) ), drr() ); }; }