From 92044d471c6d98a277491dc380253b01f321ada3 Mon Sep 17 00:00:00 2001 From: Amaury Martiny Date: Thu, 1 Aug 2019 10:46:28 +0200 Subject: [PATCH] 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... --- packages/api-derive/src/chain/bestNumber.ts | 2 +- .../src/chain/bestNumberFinalized.ts | 2 +- packages/api-derive/src/chain/getHeader.ts | 4 +- packages/api/src/Base.ts | 37 ++-- packages/api/src/checkTypes.manual.ts | 20 +- packages/api/src/types.ts | 44 ++-- .../api/test/e2e/api/promise-queries.spec.ts | 2 +- .../api/test/e2e/rpc-core/alexander.spec.ts | 12 +- .../test/e2e/rpc-core/child-storage.spec.ts | 2 +- packages/api/test/e2e/rpc-core/state.spec.ts | 20 +- .../api/test/e2e/rpc-core/subscribe.spec.ts | 16 +- packages/rpc-core/src/cached.spec.ts | 21 +- packages/rpc-core/src/formatting.spec.ts | 8 +- packages/rpc-core/src/index.ts | 37 ++-- packages/rpc-core/src/jsonrpc.types.ts | 51 +++++ packages/rpc-core/src/replay.spec.ts | 2 + packages/rpc-core/src/types.ts | 9 - packages/types/src/scripts/interfacesTs.ts | 198 +++++++++++++++--- 18 files changed, 330 insertions(+), 157 deletions(-) create mode 100644 packages/rpc-core/src/jsonrpc.types.ts diff --git a/packages/api-derive/src/chain/bestNumber.ts b/packages/api-derive/src/chain/bestNumber.ts index 0725235867..28cd65ebb4 100644 --- a/packages/api-derive/src/chain/bestNumber.ts +++ b/packages/api-derive/src/chain/bestNumber.ts @@ -24,7 +24,7 @@ import { drr } from '../util/drr'; */ export function bestNumber (api: ApiInterfaceRx): () => Observable { return (): Observable => - (api.rpc.chain.subscribeNewHead() as Observable
) + api.rpc.chain.subscribeNewHead() .pipe( filter((header: Header): boolean => !!header && !!header.number), map((header: Header): BlockNumber => header.number.unwrap()), diff --git a/packages/api-derive/src/chain/bestNumberFinalized.ts b/packages/api-derive/src/chain/bestNumberFinalized.ts index a2f92bd29f..395672edd7 100644 --- a/packages/api-derive/src/chain/bestNumberFinalized.ts +++ b/packages/api-derive/src/chain/bestNumberFinalized.ts @@ -25,7 +25,7 @@ import { drr } from '../util/drr'; */ export function bestNumberFinalized (api: ApiInterfaceRx): () => Observable { return (): Observable => - (api.rpc.chain.subscribeFinalizedHeads() as Observable
) + api.rpc.chain.subscribeFinalizedHeads() .pipe( filter((header: Header): boolean => !!header && !!header.number), map((header: Header): BlockNumber => header.number.unwrap()), diff --git a/packages/api-derive/src/chain/getHeader.ts b/packages/api-derive/src/chain/getHeader.ts index 7cfc1259d9..f5b3c19cea 100644 --- a/packages/api-derive/src/chain/getHeader.ts +++ b/packages/api-derive/src/chain/getHeader.ts @@ -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 => // tslint:disable-next-line (combineLatest([ - api.rpc.chain.getHeader(hash) as Observable
, + api.rpc.chain.getHeader(hash), api.query.session ? api.query.session.validators.at(hash) : of([]) diff --git a/packages/api/src/Base.ts b/packages/api/src/Base.ts index aec755497c..753f0152dd 100644 --- a/packages/api/src/Base.ts +++ b/packages/api/src/Base.ts @@ -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 { private _queryMulti: QueryableStorageMulti; - private _rpc: DecoratedRpc; + private _rpc: DecoratedRpc; protected _rpcCore: RpcCore; @@ -318,7 +319,7 @@ export default abstract class ApiBase { * }); * ``` */ - public get rpc (): DecoratedRpc { + public get rpc (): DecoratedRpc { return this._rpc; } @@ -451,7 +452,7 @@ export default abstract class ApiBase { * implemented by transforming the Observable to Stream/Iterator/Kefir/Bacon * via `deocrateMethod`. */ - protected abstract decorateMethod (method: (...args: any[]) => Observable, options?: DecorateMethodOptions): any; + protected abstract decorateMethod(method: (...args: any[]) => Observable, options?: DecorateMethodOptions): any; private emit (type: ApiInterfaceEvents, ...args: any[]): void { this._eventemitter.emit(type, ...args); @@ -577,19 +578,22 @@ export default abstract class ApiBase { return output; } - private decorateRpc (rpc: RpcCore, decorateMethod: ApiBase['decorateMethod']): DecoratedRpc { - return ['author', 'chain', 'state', 'system'].reduce((result, _sectionName): DecoratedRpc => { - const sectionName = _sectionName as keyof DecoratedRpc; + private decorateRpc (rpc: RpcCore, decorateMethod: ApiBase['decorateMethod']): DecoratedRpc { + return ['author', 'chain', 'state', 'system'].reduce((result, _sectionName): DecoratedRpc => { + const sectionName = _sectionName as keyof DecoratedRpc; - result[sectionName] = Object.keys(rpc[sectionName]).reduce((section, methodName): DecoratedRpcSection => { + // @ts-ignore Hard to type these correctly, I don't understand the TS errors + result[sectionName] = Object.keys(rpc[sectionName]).reduce((section, methodName): DecoratedRpcSection => { + // @ts-ignore const method = rpc[sectionName][methodName]; + // @ts-ignore section[methodName] = decorateMethod(method, { methodName }); return section; - }, {} as unknown as DecoratedRpcSection); + }, {} as unknown as DecoratedRpcSection); return result; - }, {} as unknown as DecoratedRpc); + }, {} as unknown as DecoratedRpc); } private decorateMulti (decorateMethod: ApiBase['decorateMethod']): QueryableStorageMulti { @@ -652,10 +656,11 @@ export default abstract class ApiBase { (...args: any[]): Observable => { 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 { // 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 => { - return this._rpcCore.state.subscribeStorage([[creator, key]]) + return this._rpcCore.state.subscribeStorage<[[Codec, Linkage]]>([[creator, key]]) .pipe( switchMap(([data]: [[Codec, Linkage]]): Observable => { const linkage = data[1]; @@ -784,12 +789,12 @@ export default abstract class ApiBase { (...args: any[]): Observable]> => args.length ? this._rpcCore.state - .subscribeStorage([[creator, ...args]]) + .subscribeStorage<[[Codec, Linkage]]>([[creator, ...args]]) .pipe( map(([data]): [Codec, Linkage] => data) ) : this._rpcCore.state - .subscribeStorage([creator.headKey]) + .subscribeStorage<[LinkageResult]>([creator.headKey]) .pipe( switchMap(([key]): Observable => { head = key; diff --git a/packages/api/src/checkTypes.manual.ts b/packages/api/src/checkTypes.manual.ts index 4d2c26c942..c789168577 100644 --- a/packages/api/src/checkTypes.manual.ts +++ b/packages/api/src/checkTypes.manual.ts @@ -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 { multiUnsub(); }); - await api.query.staking.intentions((intentions): void => { - console.log('intentions:', intentions); - }); - - await api.rpc.chain.subscribeNewHead
((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 { 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(), diff --git a/packages/api/src/types.ts b/packages/api/src/types.ts index 384ee6fbfb..09d3ab5b13 100644 --- a/packages/api/src/types.ts +++ b/packages/api/src/types.ts @@ -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 = (...args: Parameters) => Observable>>; -// eslint-disable-next-line @typescript-eslint/prefer-interface -export type PromiseResult = { +export interface PromiseResult { (...args: Parameters): Promise>>; (...args: Push, Callback>>>): UnsubscribePromise; -}; + (...args: Parameters): Promise; + (...args: Push, Callback>): UnsubscribePromise; +} // FIXME The day TS has higher-kinded types, we can remove this hardcoded stuff export type MethodResult = ApiType extends 'rxjs' ? RxResult : PromiseResult; -type DecoratedRpcMethod = ApiType extends 'rxjs' - ? { - (arg1?: CodecArg, arg2?: CodecArg, arg3?: CodecArg): Observable; - (arg1?: CodecArg, arg2?: CodecArg, arg3?: CodecArg): Observable; - } - : { - // 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; - // (arg1: CodecArg, callback: Callback): UnsubscribePromise; - // (callback: Callback): UnsubscribePromise; - (arg1?: CodecArg, arg2?: CodecArg, arg3?: CodecArg): Promise; - (arg1?: CodecArg, arg2?: CodecArg, arg3?: CodecArg): Promise; - (callback: Callback): UnsubscribePromise; - (arg: CodecArg, callback: Callback): UnsubscribePromise; - }; - -// FIXME https://github.com/polkadot-js/api/issues/1009 -export interface DecoratedRpcSection { - [index: string]: DecoratedRpcMethod; +export type DecoratedRpcSection = { + [Method in keyof Section]: Section[Method] extends AnyFunction + ? MethodResult + : never } -// FIXME https://github.com/polkadot-js/api/issues/1009 -export interface DecoratedRpc { - author: DecoratedRpcSection; - chain: DecoratedRpcSection; - state: DecoratedRpcSection; - system: DecoratedRpcSection; +export type DecoratedRpc = { + [Section in keyof AllSections]: DecoratedRpcSection } 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; + signPayload(payload: SignerPayload): Promise; /** * @description Receives an update for the extrinsic signed by a `signer.sign` diff --git a/packages/api/test/e2e/api/promise-queries.spec.ts b/packages/api/test/e2e/api/promise-queries.spec.ts index a6cad6a9af..e06e3a6874 100644 --- a/packages/api/test/e2e/api/promise-queries.spec.ts +++ b/packages/api/test/e2e/api/promise-queries.spec.ts @@ -127,7 +127,7 @@ describeE2E()('Promise e2e queries', (wsUrl: string): void => { it('gets correct key', async (): Promise => { const key = api.query.session.currentIndex.key(); - const sessionIndexData = await api.rpc.state.getStorage(key) as Option; + const sessionIndexData = await api.rpc.state.getStorage>(key); const sessionIndexRPC = createType('SessionIndex', sessionIndexData.unwrapOr(undefined)); expect(sessionIndexRPC.toNumber()).toBeGreaterThanOrEqual(0); diff --git a/packages/api/test/e2e/rpc-core/alexander.spec.ts b/packages/api/test/e2e/rpc-core/alexander.spec.ts index e49d6b896f..2b57308bdf 100644 --- a/packages/api/test/e2e/rpc-core/alexander.spec.ts +++ b/packages/api/test/e2e/rpc-core/alexander.spec.ts @@ -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([ diff --git a/packages/api/test/e2e/rpc-core/child-storage.spec.ts b/packages/api/test/e2e/rpc-core/child-storage.spec.ts index 2d7f34acf1..f8350170bc 100644 --- a/packages/api/test/e2e/rpc-core/child-storage.spec.ts +++ b/packages/api/test/e2e/rpc-core/child-storage.spec.ts @@ -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(); }); diff --git a/packages/api/test/e2e/rpc-core/state.spec.ts b/packages/api/test/e2e/rpc-core/state.spec.ts index 2c6289ee34..99f86a6822 100644 --- a/packages/api/test/e2e/rpc-core/state.spec.ts +++ b/packages/api/test/e2e/rpc-core/state.spec.ts @@ -49,9 +49,9 @@ describeE2E({ describe('test-suite getStorage()', (): void => { it('retrieves code', (done): void => { rpc.state - .getStorage([ - storage.substrate.code - ]) + .getStorage([ + 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([ + 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([ + storage.timestamp.now + ]) .subscribe((moment: Moment): void => { expect(moment.toNumber()).not.toEqual(0); done(); diff --git a/packages/api/test/e2e/rpc-core/subscribe.spec.ts b/packages/api/test/e2e/rpc-core/subscribe.spec.ts index 2ebfd28f41..b585f4f2a3 100644 --- a/packages/api/test/e2e/rpc-core/subscribe.spec.ts +++ b/packages/api/test/e2e/rpc-core/subscribe.spec.ts @@ -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([[storage.timestamp.now]]) + .subscribe((data): void => { expect(data).toBeDefined(); if (++count === 3) { diff --git a/packages/rpc-core/src/cached.spec.ts b/packages/rpc-core/src/cached.spec.ts index 1e47207f53..f2634bdf3b 100644 --- a/packages/rpc-core/src/cached.spec.ts +++ b/packages/rpc-core/src/cached.spec.ts @@ -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); }); diff --git a/packages/rpc-core/src/formatting.spec.ts b/packages/rpc-core/src/formatting.spec.ts index d76fff8d3c..8265d36ea1 100644 --- a/packages/rpc-core/src/formatting.spec.ts +++ b/packages/rpc-core/src/formatting.spec.ts @@ -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([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); diff --git a/packages/rpc-core/src/index.ts b/packages/rpc-core/src/index.ts index 8a11050c73..5251fbdca6 100644 --- a/packages/rpc-core/src/index.ts +++ b/packages/rpc-core/src/index.ts @@ -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: 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: + // `` + // 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 { diff --git a/packages/rpc-core/src/jsonrpc.types.ts b/packages/rpc-core/src/jsonrpc.types.ts new file mode 100644 index 0000000000..f8ce0a3989 --- /dev/null +++ b/packages/rpc-core/src/jsonrpc.types.ts @@ -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>; + pendingExtrinsics(): Observable>; + submitExtrinsic(extrinsic: IExtrinsic): Observable; + submitAndWatchExtrinsic(extrinsic: IExtrinsic): Observable; + }; + chain: { + getBlock(hash?: Hash | Uint8Array | string): Observable; + getBlockHash(blockNumber?: BlockNumber | Uint8Array | number | string): Observable; + getFinalizedHead(): Observable; + getHeader(hash?: Hash | Uint8Array | string): Observable
; + getRuntimeVersion(hash?: Hash | Uint8Array | string): Observable; + subscribeFinalizedHeads(): Observable
; + subscribeRuntimeVersion(): Observable; + subscribeNewHead(): Observable
; + }; + state: { + call(method: Text | string, data: Bytes | Uint8Array | string, block?: Hash | Uint8Array | string): Observable; + getChildKeys(childStorageKey: any, prefix: any, block?: Hash | Uint8Array | string): Observable>; + getChildStorage(childStorageKey: any, key: any, block?: Hash | Uint8Array | string): Observable; + getChildStorageHash(childStorageKey: any, key: any, block?: Hash | Uint8Array | string): Observable; + getChildStorageSize(childStorageKey: any, key: any, block?: Hash | Uint8Array | string): Observable; + getKeys(prefix: any, block?: Hash | Uint8Array | string): Observable>; + getMetadata(block?: Hash | Uint8Array | string): Observable; + getRuntimeVersion(hash?: Hash | Uint8Array | string): Observable; + getStorage(key: any, block?: Hash | Uint8Array | string): Observable; + getStorageHash(key: any, block?: Hash | Uint8Array | string): Observable; + getStorageSize(key: any, block?: Hash | Uint8Array | string): Observable; + queryStorage(keys: (any)[], startBlock: Hash | Uint8Array | string, block?: Hash | Uint8Array | string): Observable>; + subscribeStorage(keys: any[]): Observable; + }; + system: { + chain(): Observable; + health(): Observable; + name(): Observable; + networkState(): Observable; + peers(): Observable>; + properties(): Observable; + version(): Observable; + }; +} diff --git a/packages/rpc-core/src/replay.spec.ts b/packages/rpc-core/src/replay.spec.ts index 0524d197c6..46f7bffce2 100644 --- a/packages/rpc-core/src/replay.spec.ts +++ b/packages/rpc-core/src/replay.spec.ts @@ -15,8 +15,10 @@ describe('replay', (): void => { }); it('subscribes via the rpc section', (done): void => { + // @ts-ignore rpc.chain.getBlockHash = jest.fn((): Observable => of(1)); + // @ts-ignore rpc.chain.getBlockHash(123, false).subscribe((): void => { expect( rpc.chain.getBlockHash diff --git a/packages/rpc-core/src/types.ts b/packages/rpc-core/src/types.ts index 295ae036e7..1fe96de7a0 100644 --- a/packages/rpc-core/src/types.ts +++ b/packages/rpc-core/src/types.ts @@ -7,12 +7,3 @@ import { Observable } from 'rxjs'; export interface RpcInterfaceMethod { (...params: any[]): Observable; } - -export type RpcInterfaceSection = Record; - -export interface RpcInterface { - readonly author: RpcInterfaceSection; - readonly chain: RpcInterfaceSection; - readonly state: RpcInterfaceSection; - readonly system: RpcInterfaceSection; -} diff --git a/packages/types/src/scripts/interfacesTs.ts b/packages/types/src/scripts/interfacesTs.ts index b63521803a..a45bdcac95 100644 --- a/packages/types/src/scripts/interfacesTs.ts +++ b/packages/types/src/scripts/interfacesTs.ts @@ -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; type LocalExist = Record; 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, Child: Constructor): boole function isCompactEncodable (Child: Constructor): 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 extends {} @@ -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 }): void { +interface Imports extends TypeImports { + + interfaces: [string, string][]; +} + +// Create an Imports object, prefilled with types (if necessary) +function createImports ({ types }: { types: Record } = { 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 }, {}); 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 }): 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, moduleName): Record => { - 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((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((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(key: any, block?: Hash | Uint8Array | string): Observable;`; + } else if (method.method === 'subscribeStorage') { + return ` subscribeStorage(keys: any[]): Observable;`; + } + + 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();