diff --git a/packages/api-derive/src/staking/erasExposure.ts b/packages/api-derive/src/staking/erasExposure.ts index c4f37992db..76667c0d20 100644 --- a/packages/api-derive/src/staking/erasExposure.ts +++ b/packages/api-derive/src/staking/erasExposure.ts @@ -38,8 +38,8 @@ function mapStakers (stakers: [StorageKey, Exposure][]): Mapped { return { nominators, validators }; } -export function erasExposure (api: ApiInterfaceRx): (withActive?: boolean | BN) => Observable { - return memo((withActive?: boolean | BN): Observable => +export function erasExposure (api: ApiInterfaceRx): (withActive?: boolean | BN | number) => Observable { + return memo((withActive?: boolean | BN | number): Observable => api.derive.staking.erasHistoric(withActive).pipe( switchMap((eras): Observable<[EraIndex[], [StorageKey, Exposure][][]]> => combineLatest([ diff --git a/packages/api-derive/src/staking/erasHistoric.ts b/packages/api-derive/src/staking/erasHistoric.ts index fa759fa479..549000f2d6 100644 --- a/packages/api-derive/src/staking/erasHistoric.ts +++ b/packages/api-derive/src/staking/erasHistoric.ts @@ -9,12 +9,12 @@ import BN from 'bn.js'; import { Observable, of } from 'rxjs'; import { map } from 'rxjs/operators'; import { Option, u32 } from '@polkadot/types'; -import { isBoolean, isUndefined } from '@polkadot/util'; +import { isBoolean, isUndefined, bnToBn } from '@polkadot/util'; import { memo } from '../util'; -export function erasHistoric (api: ApiInterfaceRx): (withActive?: boolean | BN) => Observable { - return memo((withActive?: boolean | BN): Observable => +export function erasHistoric (api: ApiInterfaceRx): (withActive?: boolean | BN | number) => Observable { + return memo((withActive?: boolean | BN | number): Observable => api.query.staking?.activeEra ? api.queryMulti<[Option, u32]>([ api.query.staking.activeEra, @@ -36,13 +36,15 @@ export function erasHistoric (api: ApiInterfaceRx): (withActive?: boolean | BN) lastEra = lastEra.subn(1); } + const startEra = isUndefined(withActive) || isBoolean(withActive) + ? new BN(0) + : bnToBn(withActive); + // go from oldest to newest return result .filter((era): era is EraIndex => era - ? isUndefined(withActive) || isBoolean(withActive) - ? true - : era.gte(withActive) + ? era.gte(startEra) : false ) .reverse(); diff --git a/packages/api-derive/src/staking/erasPoints.ts b/packages/api-derive/src/staking/erasPoints.ts index 721c2f1a3f..7b3948b63c 100644 --- a/packages/api-derive/src/staking/erasPoints.ts +++ b/packages/api-derive/src/staking/erasPoints.ts @@ -22,8 +22,8 @@ function mapValidators ({ individual }: EraRewardPoints): DeriveEraValPoints { }, {}); } -export function erasPoints (api: ApiInterfaceRx): (withActive?: boolean | BN) => Observable { - return memo((withActive?: boolean): Observable => +export function erasPoints (api: ApiInterfaceRx): (withActive?: boolean | BN | number) => Observable { + return memo((withActive?: boolean | number): Observable => api.derive.staking.erasHistoric(withActive).pipe( switchMap((eras): Observable<[EraIndex[], EraRewardPoints[]]> => combineLatest([ diff --git a/packages/api-derive/src/staking/erasRewards.ts b/packages/api-derive/src/staking/erasRewards.ts index e89d34e207..d4df51b332 100644 --- a/packages/api-derive/src/staking/erasRewards.ts +++ b/packages/api-derive/src/staking/erasRewards.ts @@ -13,8 +13,8 @@ import { Option } from '@polkadot/types'; import { memo } from '../util'; -export function erasRewards (api: ApiInterfaceRx): (withActive?: boolean | BN) => Observable { - return memo((withActive?: boolean | BN): Observable => +export function erasRewards (api: ApiInterfaceRx): (withActive?: boolean | BN | number) => Observable { + return memo((withActive?: boolean | BN | number): Observable => api.derive.staking.erasHistoric(withActive).pipe( switchMap((eras): Observable<[EraIndex[], Option[]]> => combineLatest([ diff --git a/packages/api-derive/src/staking/index.ts b/packages/api-derive/src/staking/index.ts index 4272ef938d..13018906aa 100644 --- a/packages/api-derive/src/staking/index.ts +++ b/packages/api-derive/src/staking/index.ts @@ -13,4 +13,5 @@ export * from './overview'; export * from './query'; export * from './stakerExposure'; export * from './stakerPoints'; +export * from './stakerRewards'; export * from './validators'; diff --git a/packages/api-derive/src/staking/stakerExposure.ts b/packages/api-derive/src/staking/stakerExposure.ts index c70e63af56..14e98c7135 100644 --- a/packages/api-derive/src/staking/stakerExposure.ts +++ b/packages/api-derive/src/staking/stakerExposure.ts @@ -3,7 +3,7 @@ // of the Apache-2.0 license. See the LICENSE file for details. import { ApiInterfaceRx } from '@polkadot/api/types'; -import { DeriveStakerExpoure, DeriveEraValidatorExposure } from '../types'; +import { DeriveStakerExposure, DeriveEraValidatorExposure } from '../types'; import BN from 'bn.js'; import { Observable } from 'rxjs'; @@ -11,13 +11,13 @@ import { map } from 'rxjs/operators'; import { memo } from '../util'; -export function stakerExposure (api: ApiInterfaceRx): (accountId: Uint8Array | string, withActive?: boolean | BN) => Observable { - return memo((accountId: Uint8Array | string, withActive?: boolean | BN): Observable => { +export function stakerExposure (api: ApiInterfaceRx): (accountId: Uint8Array | string, withActive?: boolean | BN | number) => Observable { + return memo((accountId: Uint8Array | string, withActive?: boolean | BN | number): Observable => { const stakerId = api.registry.createType('AccountId', accountId).toString(); return api.derive.staking.erasExposure(withActive).pipe( - map((exposures): DeriveStakerExpoure[] => - exposures.map(({ era, nominators: allNominators, validators: allValidators }): DeriveStakerExpoure => { + map((exposures): DeriveStakerExposure[] => + exposures.map(({ era, nominators: allNominators, validators: allValidators }): DeriveStakerExposure => { const isValidator = !!allValidators[stakerId]; const validators: DeriveEraValidatorExposure = {}; let nominating: [string, number][] = []; diff --git a/packages/api-derive/src/staking/stakerPoints.ts b/packages/api-derive/src/staking/stakerPoints.ts index 3386d473eb..e9eb4c2666 100644 --- a/packages/api-derive/src/staking/stakerPoints.ts +++ b/packages/api-derive/src/staking/stakerPoints.ts @@ -11,8 +11,8 @@ import { map } from 'rxjs/operators'; import { memo } from '../util'; -export function stakerPoints (api: ApiInterfaceRx): (accountId: Uint8Array | string, withActive?: boolean | BN) => Observable { - return memo((accountId: Uint8Array | string, withActive?: boolean | BN): Observable => { +export function stakerPoints (api: ApiInterfaceRx): (accountId: Uint8Array | string, withActive?: boolean | BN | number) => Observable { + return memo((accountId: Uint8Array | string, withActive?: boolean | BN | number): Observable => { const stakerId = api.registry.createType('AccountId', accountId).toString(); return api.derive.staking.erasPoints(withActive).pipe( diff --git a/packages/api-derive/src/staking/stakerRewards.ts b/packages/api-derive/src/staking/stakerRewards.ts new file mode 100644 index 0000000000..da791876a1 --- /dev/null +++ b/packages/api-derive/src/staking/stakerRewards.ts @@ -0,0 +1,75 @@ +// Copyright 2017-2020 @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 { ApiInterfaceRx } from '@polkadot/api/types'; +import { Balance } from '@polkadot/types/interfaces'; +import { DeriveEraPoints, DeriveEraRewards, DeriveStakerExposure, DeriveStakerReward } from '../types'; + +import BN from 'bn.js'; +import { Observable, combineLatest } from 'rxjs'; +import { map } from 'rxjs/operators'; + +import { memo } from '../util'; + +type Result = [DeriveEraPoints[], DeriveEraRewards[], DeriveStakerExposure[]]; +const ZERO = new BN(0); + +function parseRewards (api: ApiInterfaceRx, stakerId: string, [erasPoints, erasRewards, exposures]: Result): DeriveStakerReward[] { + return exposures.map(({ era, isEmpty, isValidator, nominating, validators: eraValidators }): DeriveStakerReward => { + const { eraPoints, validators: allValPoints } = erasPoints.find((p) => p.era.eq(era)) || { eraPoints: new BN(0), validators: {} }; + const { eraReward } = erasRewards.find((r) => r.era.eq(era)) || { eraReward: new BN(0) }; + const validators: Record = {}; + let total = ZERO; + + Object.entries(eraValidators).forEach(([validatorId, exposure]): void => { + const valPoints = allValPoints[validatorId] || new BN(0); + const expTotal = exposure.total.unwrap(); + let value: BN | undefined; + + if (!expTotal.isZero() && !valPoints.isZero()) { + let staked: BN; + + if (validatorId === stakerId) { + staked = exposure.own.unwrap(); + } else { + const stakerExp = exposure.others.find(({ who }): boolean => who.eq(stakerId)); + + staked = stakerExp + ? stakerExp.value.unwrap() + : ZERO; + } + + value = eraReward.mul(valPoints).mul(staked).div(eraPoints).div(expTotal); + total = total.add(value); + } + + validators[validatorId] = api.registry.createType('Balance', value); + }); + + return { + era, + isEmpty, + isValidator, + nominating, + total: api.registry.createType('Balance', total), + validators + }; + }); +} + +export function stakerRewards (api: ApiInterfaceRx): (accountId: Uint8Array | string, startEra: BN | number) => Observable { + return memo((accountId: Uint8Array | string, startEra: BN | number): Observable => { + const stakerId = api.registry.createType('AccountId', accountId).toString(); + + return combineLatest([ + api.derive.staking.erasPoints(startEra), + api.derive.staking.erasRewards(startEra), + api.derive.staking.stakerExposure(stakerId, startEra) + ]).pipe( + map((result): DeriveStakerReward[] => + parseRewards(api, stakerId, result) + ) + ); + }); +} diff --git a/packages/api-derive/src/staking/types.ts b/packages/api-derive/src/staking/types.ts index e91fe27a3b..94a3c017f7 100644 --- a/packages/api-derive/src/staking/types.ts +++ b/packages/api-derive/src/staking/types.ts @@ -34,7 +34,7 @@ export interface DeriveEraExposure { validators: DeriveEraValidatorExposure; } -export interface DeriveStakerExpoure { +export interface DeriveStakerExposure { era: EraIndex; isEmpty: boolean; isValidator: boolean; @@ -42,6 +42,15 @@ export interface DeriveStakerExpoure { validators: DeriveEraValidatorExposure; } +export interface DeriveStakerReward { + era: EraIndex; + isEmpty: boolean; + isValidator: boolean; + nominating: [string, number][]; + validators: Record; + total: Balance; +} + export interface DerivedStakingElected { nextElected: AccountId[]; info: DerivedStakingQuery[];