Update api.derive to use api.queryMulti (#911)

* session api.queryMulti

* Make a single subscription to retrieve all session info

* Adjustments

* WIP

* Seems mostly comprehensive, need to full test e2e

* e2e tests for operations

* Allow "create from empty"

* skip

* Remove comment

* Remove ancient ?'s

* Update types.ts
This commit is contained in:
Jaco Greeff
2019-05-16 11:36:41 +02:00
committed by GitHub
parent bafda6dc93
commit 2dfc55cb5c
16 changed files with 237 additions and 132 deletions
+42 -38
View File
@@ -12,8 +12,41 @@ import { idAndIndex } from '../accounts/idAndIndex';
import { DerivedBalances } from '../types';
import { drr } from '../util/drr';
type Result = [AccountId | undefined, BlockNumber | undefined, [Balance?, Balance?, Array<BalanceLock>?, Option<VestingSchedule>?]];
const EMPTY_ACCOUNT = new AccountId();
function calcBalances ([accountId = EMPTY_ACCOUNT, bestNumber = new BlockNumber(0), [freeBalance = new Balance(0), reservedBalance = new Balance(0), locks = [], vesting = new Option<VestingSchedule>(VestingSchedule, null)]]: Result): DerivedBalances {
let lockedBalance = new Balance(0);
if (Array.isArray(locks)) {
// only get the locks that are valid until passed the current block
const totals = locks.filter((value) => bestNumber && value.until.gt(bestNumber));
// get the maximum of the locks according to https://github.com/paritytech/substrate/blob/master/srml/balances/src/lib.rs#L699
lockedBalance = totals[0] ? bnMax(...totals.map(({ amount }) => amount)) as Balance : new Balance(0);
}
const { offset, perBlock } = vesting.unwrapOr({
offset: new Balance(0),
perBlock: new Balance(0)
} as VestingSchedule);
const vestedBalance = new Balance(perBlock.mul(bestNumber).add(freeBalance.sub(offset)));
const availableBalance = new Balance(vestedBalance.sub(lockedBalance));
return {
accountId,
freeBalance,
reservedBalance,
votingBalance: new Balance(
freeBalance.add(reservedBalance)
),
availableBalance,
lockedBalance,
vestedBalance: vestedBalance.lt(new Balance(0)) ? new Balance(0) : vestedBalance
};
}
/**
* @name all
* @param {( ccountIndex | AccountId | Address | string )} address - An accounts Id in different formats.
@@ -38,46 +71,17 @@ export function all (api: ApiInterface$Rx) {
? combineLatest([
of(accountId),
api.derive.chain.bestNumber(),
api.query.balances.freeBalance(accountId),
api.query.balances.reservedBalance(accountId),
api.query.balances.locks(accountId),
api.query.balances.vesting(accountId)
api.queryMulti([
[api.query.balances.freeBalance, accountId],
[api.query.balances.reservedBalance, accountId],
[api.query.balances.locks, accountId],
[api.query.balances.vesting, accountId]
])
])
: of([undefined, undefined, undefined, undefined, undefined, undefined])
) as Observable<[AccountId?, BlockNumber?, Balance?, Balance?, Array<BalanceLock>?, Option<VestingSchedule>?]>
: of([undefined, undefined, [undefined, undefined, undefined, undefined]])
) as any as Observable<Result>
),
map(([accountId = EMPTY_ACCOUNT, bestNumber = new BlockNumber(0), freeBalance = new Balance(0), reservedBalance = new Balance(0), locks = [], vesting = new Option<VestingSchedule>(VestingSchedule, null)]) => {
let lockedBalance = new Balance(0);
if (Array.isArray(locks)) {
// only get the locks that are valid until passed the current block
const totals = locks.filter((value) => bestNumber && value.until.gt(bestNumber));
// get the maximum of the locks according to https://github.com/paritytech/substrate/blob/master/srml/balances/src/lib.rs#L699
lockedBalance = totals[0] ? bnMax(...totals.map(({ amount }) => amount)) as Balance : new Balance(0);
}
const { offset, perBlock } = vesting.unwrapOr({
offset: new Balance(0),
perBlock: new Balance(0)
} as VestingSchedule);
const vestedBalance = new Balance(perBlock.mul(bestNumber).add(freeBalance.sub(offset)));
// const availableBalance = lockedBalance.eq(new BlockNumber('0xffffffffffffffffffffffffffffffff')) ? new Balance(0) : freeBalance && new Balance(freeBalance.sub(lockedBalance));
const availableBalance = new Balance(vestedBalance.sub(lockedBalance));
return {
accountId,
freeBalance,
reservedBalance,
votingBalance: new Balance(
freeBalance.add(reservedBalance)
),
availableBalance,
lockedBalance,
vestedBalance: vestedBalance.lt(new Balance(0)) ? new Balance(0) : vestedBalance
};
}),
map(calcBalances),
drr()
);
};
@@ -45,20 +45,26 @@ export class ReferendumInfoExtended extends ReferendumInfo {
}
}
export function constructInfo (index: BN | number, optionInfo?: Option<ReferendumInfo>): Option<ReferendumInfoExtended> {
const info = optionInfo
? optionInfo.unwrapOr(null)
: null;
return new Option<ReferendumInfoExtended>(
ReferendumInfoExtended,
isNull(info)
? null
: new ReferendumInfoExtended(info, index)
);
}
export function referendumInfo (api: ApiInterface$Rx) {
return (index: BN | number): Observable<Option<ReferendumInfoExtended>> => {
return (api.query.democracy.referendumInfoOf(index) as Observable<Option<ReferendumInfo>>)
.pipe(
map((optionInfo) => {
const info = optionInfo.unwrapOr(null);
return new Option<ReferendumInfoExtended>(
ReferendumInfoExtended,
isNull(info)
? null
: new ReferendumInfoExtended(info, index)
);
}),
map((optionInfo) =>
constructInfo(index, optionInfo)
),
drr()
);
};
@@ -3,22 +3,26 @@
// of the Apache-2.0 license. See the LICENSE file for details.
import BN from 'bn.js';
import { combineLatest, Observable, of } from 'rxjs';
import { Observable, of } from 'rxjs';
import { map } from 'rxjs/operators';
import { ApiInterface$Rx } from '@polkadot/api/types';
import { Option } from '@polkadot/types';
import { Option, ReferendumInfo } from '@polkadot/types';
import { drr } from '../util/drr';
import { referendumInfo, ReferendumInfoExtended } from './referendumInfo';
import { constructInfo, ReferendumInfoExtended } from './referendumInfo';
export function referendumInfos (api: ApiInterface$Rx) {
const referendumInfoOf = referendumInfo(api);
return (ids: Array<BN | number> = []): Observable<Array<Option<ReferendumInfoExtended>>> => {
return !ids || !ids.length
? of([]).pipe(drr())
: combineLatest(
ids.map((id) => referendumInfoOf(id))
).pipe(
return (
!ids || !ids.length
? of([])
: api.query.democracy.referendumInfoOf.multi(ids) as any as Observable<Array<Option<ReferendumInfo>>>
).pipe(
map((infos) =>
ids.map((id, index) =>
constructInfo(id, infos[index])
)
),
drr()
);
};
@@ -2,7 +2,7 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { combineLatest, Observable, of } from 'rxjs';
import { Observable, of } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import { ApiInterface$Rx } from '@polkadot/api/types';
import { Option, ReferendumIndex } from '@polkadot/types';
@@ -13,10 +13,10 @@ import { referendumInfos } from './referendumInfos';
export function referendums (api: ApiInterface$Rx) {
return (): Observable<Array<Option<ReferendumInfoExtended>>> =>
(combineLatest([
api.query.democracy.nextTally(),
api.query.democracy.referendumCount()
]) as Observable<[ReferendumIndex?, ReferendumIndex?]>).pipe(
(api.queryMulti([
api.query.democracy.nextTally,
api.query.democracy.referendumCount
]) as any as Observable<[ReferendumIndex?, ReferendumIndex?]>).pipe(
switchMap(([nextTally, referendumCount]) =>
referendumCount && nextTally && referendumCount.gt(nextTally) && referendumCount.gtn(0)
? referendumInfos(api)(
+4 -10
View File
@@ -3,23 +3,17 @@
// of the Apache-2.0 license. See the LICENSE file for details.
import BN from 'bn.js';
import { combineLatest, Observable } from 'rxjs';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { ApiInterface$Rx } from '@polkadot/api/types';
import { BlockNumber } from '@polkadot/types';
import { drr } from '../util/drr';
import { info } from './info';
export function eraLength (api: ApiInterface$Rx) {
return (): Observable<BN> =>
(combineLatest([
api.query.session.sessionLength(),
api.query.staking.sessionsPerEra()
]) as Observable<[BlockNumber?, BlockNumber?]>).pipe(
map(
([sessionLength, sessionsPerEra]) =>
(sessionLength || new BN(1)).mul(sessionsPerEra || new BN(1))
),
info(api)().pipe(
map(({ eraLength }) => eraLength),
drr()
);
}
+4 -20
View File
@@ -3,33 +3,17 @@
// of the Apache-2.0 license. See the LICENSE file for details.
import BN from 'bn.js';
import { combineLatest, Observable } from 'rxjs';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { ApiInterface$Rx } from '@polkadot/api/types';
import { BlockNumber } from '@polkadot/types';
import { sessionProgress } from './sessionProgress';
import { drr } from '../util/drr';
import { info } from './info';
export function eraProgress (api: ApiInterface$Rx) {
return (): Observable<BN> =>
(combineLatest([
sessionProgress(api)(),
api.query.session.currentIndex(),
api.query.session.sessionLength(),
api.query.staking.lastEraLengthChange(),
api.query.staking.sessionsPerEra()
]) as Observable<[BN, BlockNumber?, BlockNumber?, BlockNumber?, BlockNumber?]>).pipe(
map(
([sessionProgress, currentIndex, sessionLength, lastEraLengthChange, sessionsPerEra]) =>
(currentIndex || new BN(0))
.sub(lastEraLengthChange || new BN(0))
.mod(sessionsPerEra || new BN(1))
.mul(sessionLength || new BN(1))
.add(sessionProgress || new BN(0)
)
),
info(api)().pipe(
map(({ eraProgress }) => eraProgress),
drr()
);
}
+1
View File
@@ -4,4 +4,5 @@
export * from './eraLength';
export * from './eraProgress';
export * from './info';
export * from './sessionProgress';
+68
View File
@@ -0,0 +1,68 @@
// 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 { DerivedSessionInfo } from '../types';
import BN from 'bn.js';
import { Observable, combineLatest } from 'rxjs';
import { map } from 'rxjs/operators';
import { ApiInterface$Rx } from '@polkadot/api/types';
import { BlockNumber, Option } from '@polkadot/types';
import { drr } from '../util/drr';
import { bestNumber } from '../chain';
type Result = [BN, [BlockNumber, Option<BlockNumber>, BlockNumber, BlockNumber, BlockNumber]];
const ZERO = new BN(0);
// internal helper to just split the logic - take all inputs, do the calculations and combine
function createDerived ([bestNumber, [currentIndex, _lastLengthChange, sessionLength, lastEraLengthChange, sessionsPerEra]]: Result): DerivedSessionInfo {
const eraLength = sessionLength.mul(sessionsPerEra);
const lastLengthChange = _lastLengthChange
? _lastLengthChange.unwrapOr(ZERO)
: ZERO;
const sessionProgress = bestNumber
.sub(lastLengthChange)
.add(sessionLength)
.mod(sessionLength);
const eraProgress = (currentIndex)
.sub(lastEraLengthChange)
.mod(sessionsPerEra)
.mul(sessionLength)
.add(sessionProgress);
return {
currentIndex,
eraLength,
eraProgress,
lastEraLengthChange,
lastLengthChange,
sessionLength,
sessionsPerEra,
sessionProgress
};
}
/**
* @description Retrieves all the session and era info and calculates specific valus on it sunh as the length of the session and eras
*/
export function info (api: ApiInterface$Rx) {
return (): Observable<DerivedSessionInfo> =>
// This is a much more optimal way to calculate since we only make a single call to the RPC backend
// instead of making a subscription for each of the params (this means all others in session use)
(combineLatest([
bestNumber(api)(),
api.queryMulti([
api.query.session.currentIndex,
api.query.session.lastLengthChange,
api.query.session.sessionLength,
api.query.staking.lastEraLengthChange,
api.query.staking.sessionsPerEra
])
]) as any as Observable<Result>).pipe(
map(createDerived),
drr()
);
}
@@ -3,28 +3,17 @@
// of the Apache-2.0 license. See the LICENSE file for details.
import BN from 'bn.js';
import { combineLatest, Observable } from 'rxjs';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { ApiInterface$Rx } from '@polkadot/api/types';
import { BlockNumber, Option } from '@polkadot/types';
import { bestNumber } from '../chain';
import { drr } from '../util/drr';
import { info } from './info';
export function sessionProgress (api: ApiInterface$Rx) {
return (): Observable<BN> =>
combineLatest([
bestNumber(api)(),
api.query.session.sessionLength(),
api.query.session.lastLengthChange()
]).pipe(
map(
([bestNumber, sessionLength, lastLengthChange]) =>
(bestNumber || new BN(0))
.sub((lastLengthChange as Option<BlockNumber>).unwrapOr(new BN(0)))
.add(sessionLength as BlockNumber || new BN(1))
.mod(sessionLength as BlockNumber || new BN(1))
),
info(api)().pipe(
map(({ sessionProgress }) => sessionProgress),
drr()
);
}
+12 -1
View File
@@ -3,7 +3,7 @@
// of the Apache-2.0 license. See the LICENSE file for details.
import BN from 'bn.js';
import { AccountId, Balance, Vote } from '@polkadot/types';
import { AccountId, Balance, BlockNumber, Vote } from '@polkadot/types';
export type DerivedBalances = {
accountId: AccountId,
@@ -45,3 +45,14 @@ export type DerivedContractFees = {
transferFee: BN,
tombstoneDeposit: BN
};
export type DerivedSessionInfo = {
currentIndex: BlockNumber,
eraLength: BN,
eraProgress: BN,
lastEraLengthChange: BlockNumber,
lastLengthChange: BN,
sessionLength: BlockNumber,
sessionsPerEra: BlockNumber,
sessionProgress: BN
};
@@ -47,4 +47,27 @@ describe.skip('derive e2e', () => {
done();
});
});
it('retrieves all session info', (done) => {
let count = 0;
return api.derive.session.info((info) => {
console.error(JSON.stringify(info));
// 5 blocks only, then go our merry way
if (++count === 5) {
done();
}
});
});
it('retrieves the balances', (done) => {
return api.derive.balances.all('5FLSigC9HGRKVhB9FiEo4Y3koPsNmBmLJbpXg2mp1hXcS59Y', (balance) => {
console.error(JSON.stringify(balance));
if (balance.freeBalance.gtn(1)) {
done();
}
});
});
});
-1
View File
@@ -31,7 +31,6 @@ describe.skip('derive e2e', () => {
// These derive.accounts tests only work on localhost, not the poc-3 URL
// (and it is assuming it sent at least 1 tx)
describe('derive.accounts', () => {
describe('idAndIndex', () => {
it('looks up AccountId & AccountIndex from AccountId', async (done) => {
// @ts-ignore silence warning until we have static types here
+15 -12
View File
@@ -495,9 +495,10 @@ export default abstract class ApiBase<CodecResult, SubscriptionResult> implement
private decorateMulti<C, S> (onCall: OnCallDefinition<C, S>): QueryableStorageMulti<C, S> {
return ((calls: QueryableStorageMultiArgs<C, S>, callback?: CodecCallback): S => {
const mapped = calls.map((arg: QueryableStorageMultiArg<C, S>): [QueryableStorageFunction<CodecResult, SubscriptionResult>, ...Array<CodecArg>] =>
// the input is a QueryableStorageFunction, convert to StorageFunction
Array.isArray(arg)
? arg
: [arg] as any
? [arg[0].creator, ...arg.slice(1)]
: [arg.creator] as any
);
return onCall(
@@ -543,7 +544,7 @@ export default abstract class ApiBase<CodecResult, SubscriptionResult> implement
}, {} as QueryableStorage<C, S>);
}
private decorateStorageEntry<C, S> (method: StorageFunction, onCall: OnCallDefinition<C, S>): QueryableStorageFunction<C, S> {
private decorateStorageEntry<C, S> (creator: StorageFunction, onCall: OnCallDefinition<C, S>): QueryableStorageFunction<C, S> {
// These signatures are allowed and exposed here -
// (arg?: CodecArg): CodecResult;
// (arg: CodecArg, callback: CodecCallback): SubscriptionResult;
@@ -557,13 +558,13 @@ export default abstract class ApiBase<CodecResult, SubscriptionResult> implement
params = args.slice(0, args.length - 1);
}
if (method.headKey && params.length === 0) {
return this.decorateStorageEntryLinked(method, onCall, callback);
if (creator.headKey && params.length === 0) {
return this.decorateStorageEntryLinked(creator, onCall, callback);
}
return onCall(
(arg: CodecArg) => this._rpcRx.state
.subscribeStorage([[method, arg]])
.subscribeStorage([[creator, arg]])
.pipe(
// state_storage returns an array of values, since we have just subscribed to
// a single entry, we pull that from the array and return it as-is
@@ -576,9 +577,11 @@ export default abstract class ApiBase<CodecResult, SubscriptionResult> implement
);
}) as QueryableStorageFunction<C, S>;
decorated.creator = creator;
decorated.at = (hash: Hash | Uint8Array | string, arg?: CodecArg): C =>
onCall(
(arg: CodecArg) => this._rpcRx.state.getStorage([method, arg], hash),
(arg: CodecArg) => this._rpcRx.state.getStorage([creator, arg], hash),
[arg]
) as C;
@@ -586,27 +589,27 @@ export default abstract class ApiBase<CodecResult, SubscriptionResult> implement
// be converted from C to the actual result required
decorated.hash = (arg?: CodecArg): HashResult<C, S> =>
onCall(
(arg: CodecArg) => this._rpcRx.state.getStorageHash([method, arg]),
(arg: CodecArg) => this._rpcRx.state.getStorageHash([creator, arg]),
[arg]
) as unknown as HashResult<C, S>;
decorated.key = (arg?: CodecArg): string =>
u8aToHex(compactStripLength(method(arg))[1]);
u8aToHex(compactStripLength(creator(arg))[1]);
decorated.multi = (args: Array<CodecArg>, callback?: CodecCallback): S =>
onCall(
() => this._rpcRx.state.subscribeStorage(args.map((arg) => [method, arg])),
() => this._rpcRx.state.subscribeStorage(args.map((arg) => [creator, arg])),
[],
callback
) as unknown as S;
decorated.size = (arg?: CodecArg): U64Result<C, S> =>
onCall(
(arg: CodecArg) => this._rpcRx.state.getStorageSize([method, arg]),
(arg: CodecArg) => this._rpcRx.state.getStorageSize([creator, arg]),
[arg]
) as unknown as U64Result<C, S>;
return this.decorateFunctionMeta(method, decorated) as QueryableStorageFunction<C, S>;
return this.decorateFunctionMeta(creator, decorated) as QueryableStorageFunction<C, S>;
}
private decorateStorageEntryLinked<C, S> (method: StorageFunction, onCall: OnCallDefinition<C, S>, callback: CodecCallback | undefined): C | S {
+1
View File
@@ -50,6 +50,7 @@ export type U64Result<CodecResult, SubscriptionResult> =
export interface QueryableStorageFunctionBase<CodecResult, SubscriptionResult> extends StorageFunction {
(arg?: CodecArg): CodecResult;
at: (hash: Hash | Uint8Array | string, arg?: CodecArg) => CodecResult;
creator: StorageFunction;
hash: (arg?: CodecArg) => HashResult<CodecResult, SubscriptionResult>;
key: (arg?: CodecArg) => string;
multi: (args: Array<CodecArg>, callback?: CodecCallback) => SubscriptionResult;
+17 -2
View File
@@ -98,13 +98,28 @@ describe.skip('e2e queries', () => {
api.query.balances.freeBalance.multi([
keyring.alice.address(),
keyring.bob.address(),
keyring.ferdie.address(),
'5DTestUPts3kjeXSTMyerHihn1uwMfLj8vU8sqF7qYrFabHE'
'5FLSigC9HGRKVhB9FiEo4Y3koPsNmBmLJbpXg2mp1hXcS59Y',
keyring.ferdie.address()
], (balances) => {
console.error(balances);
expect(balances).toHaveLength(4);
done();
});
});
it('subscribes to multiple results (api.queryMulti)', (done) => {
return api.queryMulti([
[api.query.balances.freeBalance, keyring.alice.address()],
[api.query.balances.freeBalance, keyring.bob.address()],
[api.query.balances.freeBalance, '5FLSigC9HGRKVhB9FiEo4Y3koPsNmBmLJbpXg2mp1hXcS59Y'],
[api.query.balances.freeBalance, keyring.ferdie.address()]
], (balances) => {
console.error(balances);
expect(balances).toHaveLength(4);
done();
});
});
+11 -8
View File
@@ -4,7 +4,7 @@
import { AnyU8a } from '../types';
import { isFunction } from '@polkadot/util';
import { assert, isFunction, isString, isU8a } from '@polkadot/util';
import Bytes from './Bytes';
import { StorageFunctionMetadata as MetaV0 } from '../Metadata/v0/Storage';
@@ -29,25 +29,28 @@ export default class StorageKey extends Bytes {
private _meta: MetaV0 | MetaV4 | null;
private _outputType: string | null;
constructor (value: AnyU8a | StorageKey | StorageFunction | [StorageFunction, any]) {
constructor (value?: AnyU8a | StorageKey | StorageFunction | [StorageFunction, any]) {
super(StorageKey.decodeStorageKey(value));
this._meta = StorageKey.getMeta(value as StorageKey);
this._outputType = StorageKey.getType(value as StorageKey);
}
static decodeStorageKey (value: AnyU8a | StorageKey | StorageFunction | [StorageFunction, any]): Uint8Array {
if (isFunction(value)) {
static decodeStorageKey (value?: AnyU8a | StorageKey | StorageFunction | [StorageFunction, any]): Uint8Array | string | undefined {
if (!value || isU8a(value) || isString(value)) {
// let Bytes handle these inputs
return value;
} else if (isFunction(value)) {
return value();
} else if (Array.isArray(value)) {
const [fn, ...arg] = value;
if (isFunction(fn)) {
return fn(...arg);
}
assert(isFunction(fn), 'Expected function input for key construction');
return (fn as Function)(...arg);
}
return value as Uint8Array;
throw new Error(`Unable to convert input ${value} to StorageKey`);
}
static getMeta (value: StorageKey | StorageFunction | [StorageFunction, any]): MetaV0 | MetaV4 | null {