From 7155848ba7f3541ff4f1d8435a481fa7efc5d1a1 Mon Sep 17 00:00:00 2001 From: Thibaut Sardan <33178835+Tbaut@users.noreply.github.com> Date: Wed, 29 May 2019 17:06:47 +0200 Subject: [PATCH] Redeemable and Unlocking in staking.info (#959) * init * init2 * redeemable * unlocking * address some comments * remove map and exit early * add test for unlocking * lint * comment and lint * use SubmittableResult * swap _stakingLedger and stakingLedger --- packages/api-derive/src/staking/info.ts | 89 +++++++++++++++++--- packages/api-derive/src/types.ts | 4 + packages/api-derive/test/e2e/promise.spec.ts | 49 ++++++++++- 3 files changed, 128 insertions(+), 14 deletions(-) diff --git a/packages/api-derive/src/staking/info.ts b/packages/api-derive/src/staking/info.ts index d5bae6042c..ce3a08a1ec 100644 --- a/packages/api-derive/src/staking/info.ts +++ b/packages/api-derive/src/staking/info.ts @@ -2,42 +2,107 @@ // This software may be modified and distributed under the terms // of the Apache-2.0 license. See the LICENSE file for details. -import { DerivedStaking } from '../types'; - -import { Observable, of } from 'rxjs'; -import { map, switchMap } from 'rxjs/operators'; import { ApiInterface$Rx } from '@polkadot/api/types'; -import { AccountId, Exposure, Option, StakingLedger, StructAny, ValidatorPrefs } from '@polkadot/types'; +import { AccountId, BlockNumber, Exposure, Option, StakingLedger,StructAny, ValidatorPrefs, UnlockChunk } from '@polkadot/types'; +import { DerivedStaking, DerivedUnlocking } from '../types'; +import BN from 'bn.js'; +import { combineLatest, Observable, of } from 'rxjs'; +import { map, switchMap } from 'rxjs/operators'; + +import { bestNumber } from '../chain/bestNumber'; import { drr } from '../util/drr'; +import { eraLength } from '../session/eraLength'; +import { isUndefined } from '@polkadot/util'; + +function calculateUnlocking (stakingLedger: StakingLedger | undefined, eraLength: BN, bestNumber: BlockNumber): DerivedUnlocking | undefined { + if (isUndefined(stakingLedger)) { + return undefined; + } + + // select the Unlockchunks that can't be redeemed yet. + const unlockingChunks = stakingLedger.unlocking.filter((chunk) => remainingBlocks(chunk.era, 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]) => ({ + value, + remainingBlocks: remainingBlocks(new BlockNumber(eraString), eraLength, bestNumber) + })); + + return results.length ? results : undefined; +} + +function groupByEra (list: UnlockChunk[]) { + return list.reduce((map, { era, value }) => { + const key = era.toString(); + + if (!map[key]) { + map[key] = value; + } else { + map[key] = map[key].add(value); + } + + return map; + }, {} as { [index: string]: BN }); +} + +function redeemableSum (stakingLedger: StakingLedger | undefined, eraLength: BN, bestNumber: BlockNumber) { + if (isUndefined(stakingLedger)) { + return new BN(0); + } + + return stakingLedger.unlocking + .filter((chunk) => remainingBlocks(chunk.era, eraLength, bestNumber).eqn(0)) + .reduce((curr, prev) => { + return curr.add(prev.value); + }, new BN(0)); +} + +function remainingBlocks (era: BN, eraLength: BN, bestNumber: BlockNumber) { + const remaining = eraLength.mul(era).sub(bestNumber); + + return remaining.lten(0) ? new BN(0) : remaining; +} function withStashController (api: ApiInterface$Rx, accountId: AccountId, controllerId: AccountId): Observable { const stashId = accountId; return ( + combineLatest([ + eraLength(api)(), + bestNumber(api)(), api.queryMulti([ [api.query.session.nextKeyFor, controllerId], [api.query.staking.ledger, controllerId], [api.query.staking.nominators, stashId], [api.query.staking.stakers, stashId], [api.query.staking.validators, stashId] - ]) as any as Observable<[Option, Option, [Array], Exposure, [ValidatorPrefs]]> + ]) + ]) as any as Observable<[BN, BlockNumber, [Option, Option, [Array], Exposure, [ValidatorPrefs]]]> ).pipe( - map(([nextKeyFor, stakingLedger, [nominators], stakers, [validatorPrefs]]) => - new StructAny({ + map(([eraLength, bestNumber,[nextKeyFor, _stakingLedger, [nominators], stakers, [validatorPrefs]]]) => { + const stakingLedger = _stakingLedger.isSome ? _stakingLedger.unwrap() : undefined; + + return new StructAny({ accountId, controllerId, nextSessionId: nextKeyFor.isSome ? nextKeyFor.unwrap() : undefined, nominators, + redeemable: redeemableSum(stakingLedger, eraLength, bestNumber), stakers, - stakingLedger: stakingLedger.isSome - ? stakingLedger.unwrap() - : undefined, + stakingLedger, stashId, + unlocking: calculateUnlocking(stakingLedger, eraLength, bestNumber), validatorPrefs - }) as DerivedStaking), + }) as DerivedStaking; + }), drr() ); } diff --git a/packages/api-derive/src/types.ts b/packages/api-derive/src/types.ts index d212241f56..edc3dc99ac 100644 --- a/packages/api-derive/src/types.ts +++ b/packages/api-derive/src/types.ts @@ -63,8 +63,12 @@ export interface DerivedStaking extends StructAny { controllerId?: AccountId; nextSessionId?: AccountId; nominators?: Array; + redeemable?: BN; stakers?: Exposure; stakingLedger?: StakingLedger; stashId?: AccountId; + unlocking?: DerivedUnlocking; validatorPrefs?: ValidatorPrefs; } + +export type DerivedUnlocking = Array<{remainingBlocks: BN, value: BN}>; diff --git a/packages/api-derive/test/e2e/promise.spec.ts b/packages/api-derive/test/e2e/promise.spec.ts index cac86b0ae5..f4c290f8b0 100644 --- a/packages/api-derive/test/e2e/promise.spec.ts +++ b/packages/api-derive/test/e2e/promise.spec.ts @@ -2,8 +2,12 @@ // This software may be modified and distributed under the terms // of the Apache-2.0 license. See the LICENSE file for details. -import ApiPromise from '@polkadot/api/promise/Api'; import { BlockNumber } from '@polkadot/types'; +import { DerivedStaking } from '../../src/types'; +import { SubmittableResult } from '../../../api/src'; + +import ApiPromise from '@polkadot/api/promise/Api'; +import testKeyring from '@polkadot/keyring/testing'; import { WsProvider } from '@polkadot/rpc-provider'; const WS = 'ws://127.0.0.1:9944/'; @@ -89,9 +93,13 @@ describe.skip('derive e2e', () => { it('retrieves all staking info (for stash)', (done) => { const accountId = '5GNJqTPyNqANBkUVMN1LPPrxXnFouWXoe2wNSmmEoLctxiZY'; - return api.derive.staking.info(accountId, (info) => { + return api.derive.staking.info(accountId, (info: DerivedStaking) => { console.error(JSON.stringify(info)); + if (!info.stashId || !info.controllerId || !info.stakingLedger) { + return done.fail(new Error('At leat one of info.stashId, info.controllerId or info.stakingLedger is undefined.')); + } + expect(info.accountId.eq(accountId)).toBe(true); expect(info.controllerId.eq('5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY')).toBe(true); expect(info.stashId.eq(accountId)).toBe(true); @@ -100,4 +108,41 @@ describe.skip('derive e2e', () => { done(); }); }); + + describe('verifies derive.staking.unlocking',() => { + const BOND_VALUE = 10; + const UNBOND_VALUE = 1; + const ALICE_STASH = '5GNJqTPyNqANBkUVMN1LPPrxXnFouWXoe2wNSmmEoLctxiZY'; + const ALICE = '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY'; + const keyring = testKeyring(); + const aliceStashPair = keyring.getPair(ALICE_STASH); + const alicePair = keyring.getPair(ALICE); + + it('bondsExtra funds for Alice Stash', (done) => { + return api.tx.staking.bondExtra(BOND_VALUE) + .signAndSend(aliceStashPair, (result: SubmittableResult) => { + if (result.status.isFinalized) { + + done(); + } + }); + }); + + it('unbonds dots for Alice (from Alice Stash)', (done) => { + return api.tx.staking.unbond(UNBOND_VALUE) + .signAndSend(alicePair, (result: SubmittableResult) => { + if (result.status.isFinalized) { + + done(); + } + }); + }); + + it('verifies that derive.staking.unlocking isn\'t empty/undefined', () => { + return api.derive.session.info(ALICE_STASH, (info: DerivedStaking) => { + expect(info.unlocking).toBeGreaterThan(0); + }); + }); + }); + });