Make api.rpc.*.* typed (#1237)

* Generate correctly

* Fix rpc-core

* Flexible types

* Make types work!

* Lint

* Fix grumble

* Re generate

* Fix lint

* Fix small bugs

* Maybe really fix lint?

* Fix lint again

* Fix lint

* Small ordering & formatting  changes while playing...
This commit is contained in:
Amaury Martiny
2019-08-01 10:46:28 +02:00
committed by GitHub
parent 3290f14a75
commit 92044d471c
18 changed files with 330 additions and 157 deletions
+1 -1
View File
@@ -24,7 +24,7 @@ import { drr } from '../util/drr';
*/
export function bestNumber (api: ApiInterfaceRx): () => Observable<BlockNumber> {
return (): Observable<BlockNumber> =>
(api.rpc.chain.subscribeNewHead() as Observable<Header>)
api.rpc.chain.subscribeNewHead()
.pipe(
filter((header: Header): boolean => !!header && !!header.number),
map((header: Header): BlockNumber => header.number.unwrap()),
@@ -25,7 +25,7 @@ import { drr } from '../util/drr';
*/
export function bestNumberFinalized (api: ApiInterfaceRx): () => Observable<BlockNumber> {
return (): Observable<BlockNumber> =>
(api.rpc.chain.subscribeFinalizedHeads() as Observable<Header>)
api.rpc.chain.subscribeFinalizedHeads()
.pipe(
filter((header: Header): boolean => !!header && !!header.number),
map((header: Header): BlockNumber => header.number.unwrap()),
+1 -3
View File
@@ -2,8 +2,6 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { Header } from '@polkadot/types/interfaces';
import { Observable, combineLatest, of } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
import { ApiInterfaceRx } from '@polkadot/api/types';
@@ -30,7 +28,7 @@ export function getHeader (api: ApiInterfaceRx): (hash: Uint8Array | string) =>
return (hash: Uint8Array | string): Observable<HeaderExtended | undefined> =>
// tslint:disable-next-line
(combineLatest([
api.rpc.chain.getHeader(hash) as Observable<Header>,
api.rpc.chain.getHeader(hash),
api.query.session
? api.query.session.validators.at(hash)
: of([])
+21 -16
View File
@@ -2,6 +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 { RpcInterface } from '@polkadot/rpc-core/jsonrpc.types';
import { ProviderInterface } from '@polkadot/rpc-provider/types';
import { Hash, RuntimeVersion, SignedBlock } from '@polkadot/types/interfaces';
import { AnyFunction, CallFunction, Codec, CodecArg, ModulesWithCalls, RegistryTypes } from '@polkadot/types/types';
@@ -87,7 +88,7 @@ export default abstract class ApiBase<ApiType> {
private _queryMulti: QueryableStorageMulti<ApiType>;
private _rpc: DecoratedRpc<ApiType>;
private _rpc: DecoratedRpc<ApiType, RpcInterface>;
protected _rpcCore: RpcCore;
@@ -318,7 +319,7 @@ export default abstract class ApiBase<ApiType> {
* });
* ```
*/
public get rpc (): DecoratedRpc<ApiType> {
public get rpc (): DecoratedRpc<ApiType, RpcInterface> {
return this._rpc;
}
@@ -451,7 +452,7 @@ export default abstract class ApiBase<ApiType> {
* implemented by transforming the Observable to Stream/Iterator/Kefir/Bacon
* via `deocrateMethod`.
*/
protected abstract decorateMethod (method: (...args: any[]) => Observable<any>, options?: DecorateMethodOptions): any;
protected abstract decorateMethod(method: (...args: any[]) => Observable<any>, options?: DecorateMethodOptions): any;
private emit (type: ApiInterfaceEvents, ...args: any[]): void {
this._eventemitter.emit(type, ...args);
@@ -577,19 +578,22 @@ export default abstract class ApiBase<ApiType> {
return output;
}
private decorateRpc<ApiType> (rpc: RpcCore, decorateMethod: ApiBase<ApiType>['decorateMethod']): DecoratedRpc<ApiType> {
return ['author', 'chain', 'state', 'system'].reduce((result, _sectionName): DecoratedRpc<ApiType> => {
const sectionName = _sectionName as keyof DecoratedRpc<ApiType>;
private decorateRpc<ApiType> (rpc: RpcCore, decorateMethod: ApiBase<ApiType>['decorateMethod']): DecoratedRpc<ApiType, RpcInterface> {
return ['author', 'chain', 'state', 'system'].reduce((result, _sectionName): DecoratedRpc<ApiType, RpcInterface> => {
const sectionName = _sectionName as keyof DecoratedRpc<ApiType, RpcInterface>;
result[sectionName] = Object.keys(rpc[sectionName]).reduce((section, methodName): DecoratedRpcSection<ApiType> => {
// @ts-ignore Hard to type these correctly, I don't understand the TS errors
result[sectionName] = Object.keys(rpc[sectionName]).reduce((section, methodName): DecoratedRpcSection<ApiType, RpcInterface[typeof sectionName]> => {
// @ts-ignore
const method = rpc[sectionName][methodName];
// @ts-ignore
section[methodName] = decorateMethod(method, { methodName });
return section;
}, {} as unknown as DecoratedRpcSection<ApiType>);
}, {} as unknown as DecoratedRpcSection<ApiType, RpcInterface[typeof sectionName]>);
return result;
}, {} as unknown as DecoratedRpc<ApiType>);
}, {} as unknown as DecoratedRpc<ApiType, RpcInterface>);
}
private decorateMulti<ApiType> (decorateMethod: ApiBase<ApiType>['decorateMethod']): QueryableStorageMulti<ApiType> {
@@ -652,10 +656,11 @@ export default abstract class ApiBase<ApiType> {
(...args: any[]): Observable<Codec> => {
return this._rpcCore.state
// Unfortunately for one-shot calls we also use .subscribeStorage here
.subscribeStorage([
creator.meta.type.isDoubleMap
? [creator, args]
: [creator, ...args]])
.subscribeStorage<[Codec]>([
creator.meta.type.isDoubleMap
? [creator, args]
: [creator, ...args]
])
.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
@@ -720,7 +725,7 @@ export default abstract class ApiBase<ApiType> {
// entries can be re-linked in the middle of a list, we subscribe here to make
// sure we catch any updates, no matter the list position
const getNext = (key: Codec): Observable<LinkageResult> => {
return this._rpcCore.state.subscribeStorage([[creator, key]])
return this._rpcCore.state.subscribeStorage<[[Codec, Linkage<Codec>]]>([[creator, key]])
.pipe(
switchMap(([data]: [[Codec, Linkage<Codec>]]): Observable<LinkageResult> => {
const linkage = data[1];
@@ -784,12 +789,12 @@ export default abstract class ApiBase<ApiType> {
(...args: any[]): Observable<LinkageResult | [Codec, Linkage<Codec>]> =>
args.length
? this._rpcCore.state
.subscribeStorage([[creator, ...args]])
.subscribeStorage<[[Codec, Linkage<Codec>]]>([[creator, ...args]])
.pipe(
map(([data]): [Codec, Linkage<Codec>] => data)
)
: this._rpcCore.state
.subscribeStorage([creator.headKey])
.subscribeStorage<[LinkageResult]>([creator.headKey])
.pipe(
switchMap(([key]): Observable<LinkageResult> => {
head = key;
+11 -9
View File
@@ -4,7 +4,7 @@
// Simple non-runnable checks to test type definitions in the editor itself
import { Header, Index } from '@polkadot/types/interfaces';
import { Balance, Header, Index } from '@polkadot/types/interfaces';
import { ApiPromise } from '@polkadot/api';
import { HeaderExtended } from '@polkadot/api-derive';
@@ -32,18 +32,16 @@ export default async function test (): Promise<void> {
multiUnsub();
});
await api.query.staking.intentions((intentions): void => {
console.log('intentions:', intentions);
});
await api.rpc.chain.subscribeNewHead<Header>((header): void => {
console.log('current blockNumber:', header.number);
});
// rpc section
await api.rpc.chain.subscribeNewHead((header: Header): void => {
console.log('current blockNumber:', header.number);
});
await api.rpc.state.subscribeStorage<[Balance]>(['my_balance_key'], ([balance]): void => {
console.log('current balance:', balance.toString());
});
// derives
await api.derive.chain.subscribeNewHead((header: HeaderExtended): void => {
console.log('current author:', header.author);
});
@@ -52,6 +50,10 @@ export default async function test (): Promise<void> {
console.log('current author:', header.author);
});
await api.query.staking.intentions((intentions): void => {
console.log('intentions:', intentions);
});
// constants has actual value & metadata
console.log(
api.consts.balances.creationFee.toHex(),
+13 -31
View File
@@ -9,6 +9,7 @@ import BN from 'bn.js';
import { Observable } from 'rxjs';
import { DeriveCustom } from '@polkadot/api-derive';
import { Constants } from '@polkadot/api-metadata/consts/types';
import { RpcInterface } from '@polkadot/rpc-core/jsonrpc.types';
import { ProviderInterface, ProviderInterfaceEmitted } from '@polkadot/rpc-provider/types';
import { u64 } from '@polkadot/types';
import { StorageEntry } from '@polkadot/types/primitive/StorageKey';
@@ -57,45 +58,26 @@ export interface DecorateMethodOptions {
// Also use these for api.rpc.* https://github.com/polkadot-js/api/issues/1009
export type RxResult<F extends AnyFunction> = (...args: Parameters<F>) => Observable<ObsInnerType<ReturnType<F>>>;
// eslint-disable-next-line @typescript-eslint/prefer-interface
export type PromiseResult<F extends AnyFunction> = {
export interface PromiseResult<F extends AnyFunction> {
(...args: Parameters<F>): Promise<ObsInnerType<ReturnType<F>>>;
(...args: Push<Parameters<F>, Callback<ObsInnerType<ReturnType<F>>>>): UnsubscribePromise;
};
<T>(...args: Parameters<F>): Promise<T>;
<T>(...args: Push<Parameters<F>, Callback<T>>): UnsubscribePromise;
}
// FIXME The day TS has higher-kinded types, we can remove this hardcoded stuff
export type MethodResult<ApiType, F extends AnyFunction> = ApiType extends 'rxjs'
? RxResult<F>
: PromiseResult<F>;
type DecoratedRpcMethod<ApiType> = ApiType extends 'rxjs'
? {
(arg1?: CodecArg, arg2?: CodecArg, arg3?: CodecArg): Observable<Codec>;
<T extends Codec>(arg1?: CodecArg, arg2?: CodecArg, arg3?: CodecArg): Observable<T>;
}
: {
// These signatures are allowed and exposed here (bit or a stoopid way, but checked
// RPCs and we have 3 max args, with subs max one arg... YMMV) -
// (arg1?: CodecArg, arg2?: CodecArg, arg3?: CodecArg): Promise<Codec>;
// (arg1: CodecArg, callback: Callback<Codec>): UnsubscribePromise;
// (callback: Callback<Codec>): UnsubscribePromise;
(arg1?: CodecArg, arg2?: CodecArg, arg3?: CodecArg): Promise<Codec>;
<T extends Codec>(arg1?: CodecArg, arg2?: CodecArg, arg3?: CodecArg): Promise<T>;
<T extends Codec>(callback: Callback<T>): UnsubscribePromise;
<T extends Codec>(arg: CodecArg, callback: Callback<T>): UnsubscribePromise;
};
// FIXME https://github.com/polkadot-js/api/issues/1009
export interface DecoratedRpcSection<ApiType> {
[index: string]: DecoratedRpcMethod<ApiType>;
export type DecoratedRpcSection<ApiType, Section> = {
[Method in keyof Section]: Section[Method] extends AnyFunction
? MethodResult<ApiType, Section[Method]>
: never
}
// FIXME https://github.com/polkadot-js/api/issues/1009
export interface DecoratedRpc<ApiType> {
author: DecoratedRpcSection<ApiType>;
chain: DecoratedRpcSection<ApiType>;
state: DecoratedRpcSection<ApiType>;
system: DecoratedRpcSection<ApiType>;
export type DecoratedRpc<ApiType, AllSections> = {
[Section in keyof AllSections]: DecoratedRpcSection<ApiType, AllSections[Section]>
}
export interface StorageEntryObservable {
@@ -216,7 +198,7 @@ export interface ApiInterfaceRx {
runtimeVersion: RuntimeVersion;
query: QueryableStorage<'rxjs'>;
queryMulti: QueryableStorageMulti<'rxjs'>;
rpc: DecoratedRpc<'rxjs'>;
rpc: DecoratedRpc<'rxjs', RpcInterface>;
tx: SubmittableExtrinsics<'rxjs'>;
signer?: Signer;
}
@@ -299,7 +281,7 @@ export interface Signer {
/**
* @description signs an extrinsic payload from a serialized form
*/
signPayload (payload: SignerPayload): Promise<SignerResult>;
signPayload(payload: SignerPayload): Promise<SignerResult>;
/**
* @description Receives an update for the extrinsic signed by a `signer.sign`
@@ -127,7 +127,7 @@ describeE2E()('Promise e2e queries', (wsUrl: string): void => {
it('gets correct key', async (): Promise<void> => {
const key = api.query.session.currentIndex.key();
const sessionIndexData = await api.rpc.state.getStorage(key) as Option<any>;
const sessionIndexData = await api.rpc.state.getStorage<Option<any>>(key);
const sessionIndexRPC = createType('SessionIndex', sessionIndexData.unwrapOr(undefined));
expect(sessionIndexRPC.toNumber()).toBeGreaterThanOrEqual(0);
@@ -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 { SignedBlock, StorageChangeSet } from '@polkadot/types/interfaces';
import { BlockNumber, Index, SignedBlock } from '@polkadot/types/interfaces';
import storage from '@polkadot/api-metadata/storage/static';
import WsProvider from '@polkadot/rpc-provider/ws';
@@ -28,11 +28,11 @@ describeE2E({
it('subscribes to storage', (done): void => {
rpc.state
.subscribeStorage([
[storage.system.accountNonce, randomAccount],
[storage.session.currentIndex]
])
.subscribe((data: StorageChangeSet): void => {
.subscribeStorage<[BlockNumber, Index]>([
[storage.system.accountNonce, randomAccount],
[storage.session.currentIndex]
])
.subscribe((data): void => {
expect(data).toHaveLength(2);
expect(data).toEqual(
expect.arrayContaining([
@@ -139,7 +139,7 @@ describeE2E({
rpc.state
.getChildStorageSize(storageKeys[0], childStorageKeys[0])
.subscribe((storage: StorageData): void => {
.subscribe((storage): void => {
expect(storage.toString()).toBe('4');
done();
});
+10 -10
View File
@@ -49,9 +49,9 @@ describeE2E({
describe('test-suite getStorage()', (): void => {
it('retrieves code', (done): void => {
rpc.state
.getStorage([
storage.substrate.code
])
.getStorage<Bytes>([
storage.substrate.code
])
.subscribe((code: Bytes): void => {
expect(code).toBeDefined();
expect(code).toBeInstanceOf(Bytes);
@@ -61,10 +61,10 @@ describeE2E({
it('retrieves balances', (done): void => {
rpc.state
.getStorage([
storage.balances.freeBalance, BOB_STASH
])
.subscribe((balance: Balance): void => {
.getStorage<Balance>([
storage.balances.freeBalance, BOB_STASH
])
.subscribe((balance): void => {
expect(balance.isZero()).not.toEqual(true);
done();
});
@@ -72,9 +72,9 @@ describeE2E({
it('retrieves timestamp', (done): void => {
rpc.state
.getStorage([
storage.timestamp.now
])
.getStorage<Moment>([
storage.timestamp.now
])
.subscribe((moment: Moment): void => {
expect(moment.toNumber()).not.toEqual(0);
done();
@@ -6,7 +6,7 @@ import testingPairs from '@polkadot/keyring/testingPairs';
import WsProvider from '@polkadot/rpc-provider/ws';
import storage from '@polkadot/api-metadata/storage/static';
import Rpc from '@polkadot/rpc-core';
import { StorageChangeSet } from '@polkadot/types/interfaces';
import { Index, Moment, SessionIndex } from '@polkadot/types/interfaces';
import { ClassOf } from '@polkadot/types';
import { describeE2E } from '../../util';
@@ -27,11 +27,11 @@ describeE2E({
it('subscribes to storage', (done): void => {
rpc.state
.subscribeStorage([
[storage.system.accountNonce, keyring.eve.address],
[storage.session.currentIndex]
])
.subscribe((data: StorageChangeSet): void => {
.subscribeStorage<[Index, SessionIndex]>([
[storage.system.accountNonce, keyring.eve.address],
[storage.session.currentIndex]
])
.subscribe((data): void => {
expect(data).toHaveLength(2);
expect(data).toEqual(
expect.arrayContaining([
@@ -48,8 +48,8 @@ describeE2E({
let count = 0;
rpc.state
.subscribeStorage([[storage.timestamp.now]])
.subscribe((data: StorageChangeSet): void => {
.subscribeStorage<Moment>([[storage.timestamp.now]])
.subscribe((data): void => {
expect(data).toBeDefined();
if (++count === 3) {
+11 -10
View File
@@ -17,8 +17,8 @@ describe('Cached Observables', (): void => {
});
it('creates a single observable for subscriptions (multiple calls)', (): void => {
const observable1 = rpc.chain.subscribeNewHead(123);
const observable2 = rpc.chain.subscribeNewHead(123);
const observable1 = rpc.state.subscribeStorage([123]);
const observable2 = rpc.state.subscribeStorage([123]);
expect(observable2).toBe(observable1);
});
@@ -31,15 +31,15 @@ describe('Cached Observables', (): void => {
});
it('creates a single observable (multiple calls, different arguments that should be cached together)', (): void => {
const observable1 = rpc.chain.subscribeNewHead(keyring.alice.address);
const observable2 = rpc.chain.subscribeNewHead(createType('AccountId', keyring.alice.address));
const observable1 = rpc.state.subscribeStorage([keyring.alice.address]);
const observable2 = rpc.state.subscribeStorage([createType('AccountId', keyring.alice.address)]);
expect(observable2).toBe(observable1);
});
it('creates multiple observables for different values', (): void => {
const observable1 = rpc.chain.subscribeNewHead(123);
const observable2 = rpc.chain.subscribeNewHead(456);
const observable1 = rpc.chain.getBlockHash(123);
const observable2 = rpc.chain.getBlockHash(456);
expect(observable2).not.toBe(observable1);
});
@@ -63,15 +63,16 @@ describe('Cached Observables', (): void => {
});
it('creates different observables for different methods but same arguments', (): void => {
const observable1 = rpc.chain.subscribeNewHead(123);
const observable2 = rpc.state.subscribeStorage(123);
// @ts-ignore
const observable1 = rpc.chain.subscribeNewHead([123]);
const observable2 = rpc.state.subscribeStorage([123]);
expect(observable2).not.toBe(observable1);
});
it('creates multiple observables for one-shots', (): void => {
const observable1 = rpc.system.chain(123);
const observable2 = rpc.system.chain(123);
const observable1 = rpc.chain.getBlockHash(123);
const observable2 = rpc.chain.getBlockHash(123);
expect(observable2).not.toBe(observable1);
});
+4 -4
View File
@@ -79,7 +79,7 @@ function formattingTests (version: string, storage: Storage, encodedValues: [str
it('encodes key (with params), decoding response', (done): void => {
api.state
.getStorage([storage.balances.freeBalance, ADDR_ONE])
.getStorage<Balance>([storage.balances.freeBalance, ADDR_ONE])
.subscribe((value): void => {
expect(
provider.send
@@ -102,12 +102,12 @@ function formattingTests (version: string, storage: Storage, encodedValues: [str
});
it('encodes multiple keys, decoding multiple results', (done): void => {
api.state.subscribeStorage(
api.state.subscribeStorage<[Balance, Balance]>(
[
[storage.balances.freeBalance, ADDR_ONE],
[storage.balances.freeBalance, ADDR_TWO]
]
).subscribe((value: Balance[]): void => {
).subscribe((value): void => {
// console.error(value);
expect(
@@ -173,7 +173,7 @@ function formattingTests (version: string, storage: Storage, encodedValues: [str
api.state
.subscribeStorage([[storage.staking.validators, '0x00']])
.subscribe((value: Codec): void => {
.subscribe(([value]: Codec[]): void => {
expect(value).toBeDefined();
// console.error('linked falklback', value);
+23 -14
View File
@@ -3,10 +3,11 @@
// of the Apache-2.0 license. See the LICENSE file for details.
import { ProviderInterface } from '@polkadot/rpc-provider/types';
import { RpcSection, RpcMethod } from '@polkadot/jsonrpc/types';
import { RpcMethod } from '@polkadot/jsonrpc/types';
import { StorageChangeSet } from '@polkadot/types/interfaces';
import { AnyJson, Codec } from '@polkadot/types/types';
import { RpcInterface, RpcInterfaceMethod, RpcInterfaceSection } from './types';
import { RpcInterface } from './jsonrpc.types';
import { RpcInterfaceMethod } from './types';
import memoizee from 'memoizee';
import { combineLatest, from, Observable, Observer, of, throwError } from 'rxjs';
@@ -55,13 +56,13 @@ export default class Rpc implements RpcInterface {
public readonly provider: ProviderInterface;
public readonly author: RpcInterfaceSection;
public readonly author: RpcInterface['author'];
public readonly chain: RpcInterfaceSection;
public readonly chain: RpcInterface['chain'];
public readonly state: RpcInterfaceSection;
public readonly state: RpcInterface['state'];
public readonly system: RpcInterfaceSection;
public readonly system: RpcInterface['system'];
/**
* @constructor
@@ -73,10 +74,10 @@ export default class Rpc implements RpcInterface {
this.provider = provider;
this.author = this.createInterface(interfaces.author);
this.chain = this.createInterface(interfaces.chain);
this.state = this.createInterface(interfaces.state);
this.system = this.createInterface(interfaces.system);
this.author = this.createInterface('author');
this.chain = this.createInterface('chain');
this.state = this.createInterface('state');
this.system = this.createInterface('system');
}
/**
@@ -113,18 +114,26 @@ export default class Rpc implements RpcInterface {
return `${Rpc.signature(method)}:: ${error.message}`;
}
private createInterface ({ methods }: RpcSection): RpcInterfaceSection {
private createInterface<Section extends keyof RpcInterface> (section: Section): RpcInterface[Section] {
const { methods } = interfaces[section];
return Object
.keys(methods)
.reduce((exposed, methodName): RpcInterfaceSection => {
.reduce((exposed, methodName): RpcInterface[Section] => {
const def = methods[methodName];
exposed[methodName] = def.isSubscription
// FIXME Remove any here
// To do so, remove `RpcInterfaceMethod` from './types.ts', and refactor
// every method inside this class to take:
// `<S extends keyof RpcInterface, M extends keyof RpcInterface[S]>`
// Not doing so, because it makes this class a little bit less readable,
// and leaving it as-is doesn't harm much
(exposed as any)[methodName] = def.isSubscription
? this.createMethodSubscribe(def)
: this.createMethodSend(def);
return exposed;
}, {} as unknown as RpcInterfaceSection);
}, {} as unknown as RpcInterface[Section]);
}
private createMethodSend (method: RpcMethod): RpcInterfaceMethod {
+51
View File
@@ -0,0 +1,51 @@
// Auto-generated via `yarn build:interfaces`, do not edit
import { Observable } from 'rxjs';
import { Vec } from '@polkadot/types/codec';
import { Bytes, Metadata, StorageData, StorageKey, Text, u64 } from '@polkadot/types';
import { BlockNumber, Extrinsic, Hash, Header, SignedBlock } from '@polkadot/types/interfaces/runtime';
import { ChainProperties, ExtrinsicOrHash, ExtrinsicStatus, Health, NetworkState, PeerInfo, RuntimeVersion, StorageChangeSet } from '@polkadot/types/interfaces/rpc';
import { Codec, IExtrinsic } from '@polkadot/types/types';
export interface RpcInterface {
author: {
removeExtrinsic(bytesOrHash: (ExtrinsicOrHash)[]): Observable<Vec<Hash>>;
pendingExtrinsics(): Observable<Vec<Extrinsic>>;
submitExtrinsic(extrinsic: IExtrinsic): Observable<Hash>;
submitAndWatchExtrinsic(extrinsic: IExtrinsic): Observable<ExtrinsicStatus>;
};
chain: {
getBlock(hash?: Hash | Uint8Array | string): Observable<SignedBlock>;
getBlockHash(blockNumber?: BlockNumber | Uint8Array | number | string): Observable<Hash>;
getFinalizedHead(): Observable<Hash>;
getHeader(hash?: Hash | Uint8Array | string): Observable<Header>;
getRuntimeVersion(hash?: Hash | Uint8Array | string): Observable<RuntimeVersion>;
subscribeFinalizedHeads(): Observable<Header>;
subscribeRuntimeVersion(): Observable<RuntimeVersion>;
subscribeNewHead(): Observable<Header>;
};
state: {
call(method: Text | string, data: Bytes | Uint8Array | string, block?: Hash | Uint8Array | string): Observable<Bytes>;
getChildKeys(childStorageKey: any, prefix: any, block?: Hash | Uint8Array | string): Observable<Vec<StorageKey>>;
getChildStorage(childStorageKey: any, key: any, block?: Hash | Uint8Array | string): Observable<StorageData>;
getChildStorageHash(childStorageKey: any, key: any, block?: Hash | Uint8Array | string): Observable<Hash>;
getChildStorageSize(childStorageKey: any, key: any, block?: Hash | Uint8Array | string): Observable<u64>;
getKeys(prefix: any, block?: Hash | Uint8Array | string): Observable<Vec<StorageKey>>;
getMetadata(block?: Hash | Uint8Array | string): Observable<Metadata>;
getRuntimeVersion(hash?: Hash | Uint8Array | string): Observable<RuntimeVersion>;
getStorage<T = Codec>(key: any, block?: Hash | Uint8Array | string): Observable<T>;
getStorageHash(key: any, block?: Hash | Uint8Array | string): Observable<Hash>;
getStorageSize(key: any, block?: Hash | Uint8Array | string): Observable<u64>;
queryStorage(keys: (any)[], startBlock: Hash | Uint8Array | string, block?: Hash | Uint8Array | string): Observable<Vec<StorageChangeSet>>;
subscribeStorage<T = Codec[]>(keys: any[]): Observable<T>;
};
system: {
chain(): Observable<Text>;
health(): Observable<Health>;
name(): Observable<Text>;
networkState(): Observable<NetworkState>;
peers(): Observable<Vec<PeerInfo>>;
properties(): Observable<ChainProperties>;
version(): Observable<Text>;
};
}
+2
View File
@@ -15,8 +15,10 @@ describe('replay', (): void => {
});
it('subscribes via the rpc section', (done): void => {
// @ts-ignore
rpc.chain.getBlockHash = jest.fn((): Observable<number> => of(1));
// @ts-ignore
rpc.chain.getBlockHash(123, false).subscribe((): void => {
expect(
rpc.chain.getBlockHash
-9
View File
@@ -7,12 +7,3 @@ import { Observable } from 'rxjs';
export interface RpcInterfaceMethod {
(...params: any[]): Observable<any>;
}
export type RpcInterfaceSection = Record<string, RpcInterfaceMethod>;
export interface RpcInterface {
readonly author: RpcInterfaceSection;
readonly chain: RpcInterfaceSection;
readonly state: RpcInterfaceSection;
readonly system: RpcInterfaceSection;
}
+165 -33
View File
@@ -3,16 +3,19 @@
// of the Apache-2.0 license. See the LICENSE file for details.
import { TypeDef, TypeDefInfo, TypeDefExtVecFixed } from '../codec/types';
import { Constructor } from '../types';
import fs from 'fs';
import { isString, stringCamelCase, stringUpperFirst } from '@polkadot/util';
import { getTypeDef } from '../codec/createType';
import interfaces from '../../../type-jsonrpc/src';
import { ClassOfUnsafe, getTypeDef } from '../codec/createType';
import * as codecClasses from '../codec';
import AbstractInt from '../codec/AbstractInt';
import { COMPACT_ENCODABLE } from '../codec/Compact';
import * as primitiveClasses from '../primitive';
import Vec from '../codec/Vec';
import * as definitions from '../interfaces/definitions';
import { Constructor } from '../types';
import * as primitiveClasses from '../primitive';
// these map all the codec and primitive types for import, see the TypeImports below. If
// we have an unseen type, it is `undefined`/`false`, if we need to import it, it is `true`
@@ -23,10 +26,11 @@ type TypeExist = Record<string, boolean>;
type LocalExist = Record<string, TypeExist>;
interface TypeImports {
codecTypes: TypeExist;
codecTypes: TypeExist; // `import {} from '@polkadot/types/codec`
localTypes: LocalExist;
ownTypes: string[];
primitiveTypes: TypeExist;
primitiveTypes: TypeExist; // `import {} from '@polkadot/types/primitive`
typesTypes: TypeExist; // `import {} from '@polkadot/types/types`
}
const HEADER = '// Auto-generated via `yarn build:interfaces`, do not edit\n\n';
@@ -34,13 +38,15 @@ const FOOTER = '\n';
// Maps the types as found to the source location. This is used to generate the
// imports in the output file, dep-duped and sorted
function setImports ({ codecTypes, localTypes, ownTypes, primitiveTypes }: TypeImports, types: string[]): void {
function setImports ({ codecTypes, localTypes, ownTypes, primitiveTypes, typesTypes }: TypeImports, types: string[]): void {
types.forEach((type): void => {
if (ownTypes.includes(type)) {
// do nothing
} else if (['Codec', 'IExtrinsic'].includes(type)) {
typesTypes[type] = true;
} else if ((codecClasses as any)[type]) {
codecTypes[type] = true;
} else if ((primitiveClasses as any)[type]) {
} else if ((primitiveClasses as any)[type] || type === 'Metadata') {
primitiveTypes[type] = true;
} else {
// find this module inside the exports from the rest
@@ -67,7 +73,9 @@ function isChildClass (Parent: Constructor<any>, Child: Constructor<any>): boole
function isCompactEncodable (Child: Constructor<any>): boolean {
// @ts-ignore AbstractInt is abstract, we shouldn't isChildClass it here, but it works
return Object.values(COMPACT_ENCODABLE).some((CompactEncodable): boolean => isChildClass(CompactEncodable, Child));
return Object.values(COMPACT_ENCODABLE).some((CompactEncodable): boolean =>
isChildClass(CompactEncodable, Child)
);
}
// helper to generate a `export interface <Name> extends <Base> {<Body>}
@@ -257,11 +265,14 @@ function createImportCode (header: string, checks: { file: string; types: string
function getDerivedTypes (type: string, primitiveName: string, imports: TypeImports, indent: number = 2): string {
// `primitiveName` represents the actual primitive type our type is mapped to
const isCompact = isCompactEncodable((primitiveClasses as any)[primitiveName]);
setImports(imports, ['Option', 'Vec', isCompact ? 'Compact' : '']);
return [
`${type}: ${type};`,
isCompact ? `'Compact<${type}>': Compact<${type}>;` : undefined,
isCompact
? `'Compact<${type}>': Compact<${type}>;`
: undefined,
`'Option<${type}>': Option<${type}>;`,
`'Vec<${type}>': Vec<${type}>;`
]
@@ -270,7 +281,13 @@ function getDerivedTypes (type: string, primitiveName: string, imports: TypeImpo
.join('\n');
}
function generateTsDef (defName: string, { types }: { types: Record<string, any> }): void {
interface Imports extends TypeImports {
interfaces: [string, string][];
}
// Create an Imports object, prefilled with types (if necessary)
function createImports ({ types }: { types: Record<string, any> } = { types: {} }): Imports {
// handlers are defined externally to use - this means that when we do a
// `generators[typedef.info](...)` TS will show any unhandled types. Rather
// we are being explicit in having no handlers where we do not support (yet)
@@ -297,29 +314,38 @@ function generateTsDef (defName: string, { types }: { types: Record<string, any>
}, {});
const ownTypes = Object.keys(types);
const primitiveTypes: TypeExist = {};
const typesTypes: TypeExist = {};
const interfaces = Object.entries(types).map(([name, type]): [string, string] => {
const def = getTypeDef(isString(type) ? type.toString() : JSON.stringify(type), name);
return [name, generators[def.info](def, { codecTypes, localTypes, ownTypes, primitiveTypes })];
return [name, generators[def.info](def, { codecTypes, localTypes, ownTypes, primitiveTypes, typesTypes })];
});
const sortedDefs = interfaces.sort((a, b): number => a[0].localeCompare(b[0])).map(([, definition]): string => definition).join('\n\n');
const imports = { codecTypes, localTypes, ownTypes, primitiveTypes, typesTypes, interfaces };
return imports;
}
function generateTsDef (defName: string, { types }: { types: Record<string, any> }): void {
const imports = createImports({ types });
const sortedDefs = imports.interfaces.sort((a, b): number => a[0].localeCompare(b[0])).map(([, definition]): string => definition).join('\n\n');
const header = createImportCode(HEADER, [
{
file: '../../types',
types: codecTypes['Tuple'] ? ['Codec'] : []
types: imports.codecTypes['Tuple'] ? ['Codec'] : []
},
{
file: '../../codec',
types: Object.keys(codecTypes).filter((name): boolean => name !== 'Tuple')
types: Object.keys(imports.codecTypes).filter((name): boolean => name !== 'Tuple')
},
{
file: '../../primitive',
types: Object.keys(primitiveTypes)
types: Object.keys(imports.primitiveTypes)
},
...Object.keys(localTypes).map((moduleName): { file: string; types: string[] } => ({
...Object.keys(imports.localTypes).map((moduleName): { file: string; types: string[] } => ({
file: `../${moduleName}`,
types: Object.keys(localTypes[moduleName])
types: Object.keys(imports.localTypes[moduleName])
}))
]);
@@ -338,18 +364,9 @@ console.log(`Writing interfaces/types.ts`);
fs.writeFileSync(`packages/types/src/interfaces/types.ts`, HEADER.concat(Object.keys(definitions).map((moduleName): string => `export * from './${moduleName}/types';`).join('\n')).concat(FOOTER), { flag: 'w' });
fs.writeFileSync(`packages/types/src/interfaces/index.ts`, HEADER.concat(`export * from './types';`).concat(FOOTER), { flag: 'w' });
// Generate `packages/types/src/interfaceRegistry.ts`, the registry of all interfaces
function generateInterfaceRegistry (): void {
const codecTypes: TypeExist = {};
const localTypes: LocalExist = Object.keys(definitions).reduce((localTypes: Record<string, TypeExist>, moduleName): Record<string, TypeExist> => {
localTypes[moduleName] = {};
return localTypes;
}, {});
const ownTypes: string[] = [];
const primitiveTypes: TypeExist = {};
const substrateTypes: TypeExist = {};
const imports = { codecTypes, localTypes, ownTypes, primitiveTypes, substrateTypes };
const imports = createImports();
const primitives = Object
.keys(primitiveClasses)
@@ -371,22 +388,24 @@ function generateInterfaceRegistry (): void {
return [
accumulator,
...Object.keys(types).map((type): string => getDerivedTypes(type, (types as any)[type], imports, 2))
...Object.keys(types).map((type): string =>
getDerivedTypes(type, (types as any)[type], imports, 2)
)
].join('\n');
}, '');
const header = createImportCode(HEADER, [
{
file: './codec',
types: Object.keys(codecTypes).filter((name): boolean => name !== 'Tuple')
types: Object.keys(imports.codecTypes).filter((name): boolean => name !== 'Tuple')
},
{
file: './primitive',
types: Object.keys(primitiveTypes)
types: Object.keys(imports.primitiveTypes)
},
...Object.keys(localTypes).map((moduleName): { file: string; types: string[] } => ({
...Object.keys(imports.localTypes).map((moduleName): { file: string; types: string[] } => ({
file: `./interfaces/${moduleName}`,
types: Object.keys(localTypes[moduleName])
types: Object.keys(imports.localTypes[moduleName])
}))
]);
@@ -403,3 +422,116 @@ function generateInterfaceRegistry (): void {
console.log(`Writing interfaceRegistry.ts`);
generateInterfaceRegistry();
// Make types a little bit more flexible
// - if param instanceof AbstractInt, then param: u64 | Uint8array | string | number
// etc
function getSimilarTypes (imports: Imports, type: string): string[] {
const possibleTypes = [type];
if (type === 'Extrinsic') {
setImports(imports, ['IExtrinsic']);
return ['IExtrinsic'];
}
if (isChildClass(Vec, ClassOfUnsafe(type))) {
return [`(${getSimilarTypes(imports, ((getTypeDef(type).sub) as TypeDef).type).join(' | ')})[]`];
}
// FIXME This is a hack, it's hard to correctly type StorageKeys in the
// current state
if (type === 'StorageKey') {
return ['any'];
}
// @ts-ignore Cannot get isChildClass of abstract class, but it works
if (isChildClass(AbstractInt, ClassOfUnsafe(type))) {
possibleTypes.push('Uint8Array', 'number', 'string');
} else if (isChildClass(Uint8Array, ClassOfUnsafe(type))) {
possibleTypes.push('Uint8Array', 'string');
} else if (isChildClass(String, ClassOfUnsafe(type))) {
possibleTypes.push('string');
}
return possibleTypes;
}
// Generate `packages/types-jsonrpc/src/jsonrpc.types.ts`
function generateRpcTypes (): void {
// Inject all types so that types-jsonrpc can use them
require('../injector');
// Get all imported types by types-jsonrpc
const allImportedTypes = Object.keys(interfaces).reduce<string[]>((allSections, section): string[] => {
return allSections.concat(
...Object.values(interfaces[section].methods).map((method): string[] =>
[...method.params.map(({ type }): string => type), method.type])
);
}, []);
const imports = createImports({ types: allImportedTypes });
const body = Object.keys(interfaces).reduce<string[]>((allSections, section): string[] => {
const allMethods = Object.values(interfaces[section].methods).map((method): string => {
// FIXME These 2 are too hard to type, I give up
if (method.method === 'getStorage') {
setImports(imports, ['Codec']);
return ` getStorage<T = Codec>(key: any, block?: Hash | Uint8Array | string): Observable<T>;`;
} else if (method.method === 'subscribeStorage') {
return ` subscribeStorage<T = Codec[]>(keys: any[]): Observable<T>;`;
}
const args = method.params.map((param): string => {
const similarTypes = getSimilarTypes(imports, param.type);
setImports(imports, similarTypes);
return `${param.name}${param.isOptional ? '?' : ''}: ${similarTypes.join(' | ')}`;
});
return ` ${method.method}(${args.join(', ')}): Observable<${method.type}>;`;
});
return allSections.concat(
[
` ${section}: {`,
...allMethods,
` };`
].join('\n')
);
}, []).join('\n');
const header = createImportCode(HEADER, [
{
file: 'rxjs',
types: ['Observable']
},
{
file: '@polkadot/types/codec',
types: Object.keys(imports.codecTypes).filter((name): boolean => name !== 'Tuple')
},
{
file: '@polkadot/types',
types: Object.keys(imports.primitiveTypes)
},
...Object.keys(imports.localTypes).map((moduleName): { file: string; types: string[] } => ({
file: `@polkadot/types/interfaces/${moduleName}`,
types: Object.keys(imports.localTypes[moduleName])
})),
{
file: '@polkadot/types/types',
types: Object.keys(imports.typesTypes)
}
]);
const interfaceStart = 'export interface RpcInterface {\n';
const interfaceEnd = '\n}';
fs.writeFileSync(
`packages/rpc-core/src/jsonrpc.types.ts`,
header.concat(interfaceStart).concat(body).concat(interfaceEnd).concat(FOOTER)
, { flag: 'w' }
);
}
console.log(`Writing packages/rpc-core/jsonrpc.types.ts`);
generateRpcTypes();