Convert jsonrpc & typings to definitions (#2064)

* Convert jsonrpc & typings to definitions

* Handle empty interface generation (no types)

* Move jsonrpc/create to rpc-core/create

* Create imports

* Remove jsonrpc package, generate rpc-core from defs

* Debug :)

* Generate RPC definitions via interfaces

* Generate md docs

* Remove @polkadot/jsonrpc refs (at least packages)

* No empty types

* Add PAHNTOM "cleanup" gen

* *sigh* regex loop

* Update mock for no jsonrpc

* Add definition export types

* Cleanup linting

* Mocking 1 fix

* Mocking 2 fix

* rpc-core tests updates

* CHANGELOG & docs

* Add jsonrpc (already mapped) to interfaces

* Add aliasses for RPCs

* Fix rpc-core tests
This commit is contained in:
Jaco Greeff
2020-03-19 14:40:54 +01:00
committed by GitHub
parent 27e3fbfd0f
commit 044efbc6de
170 changed files with 1281 additions and 1346 deletions
-1
View File
@@ -27,7 +27,6 @@
"homepage": "https://github.com/polkadot-js/api/tree/master/packages/rpc-core#readme",
"dependencies": {
"@babel/runtime": "^7.8.7",
"@polkadot/jsonrpc": "1.8.0-beta.2",
"@polkadot/metadata": "1.8.0-beta.2",
"@polkadot/rpc-provider": "1.8.0-beta.2",
"@polkadot/types": "1.8.0-beta.2",
+7 -12
View File
@@ -24,37 +24,32 @@ describe('Api', (): void => {
Object.keys(rpc).filter((key): boolean => !key.startsWith('_'))
).toEqual([
'mapping', 'provider', 'registry', 'sections',
'account', 'author', 'chain', 'contracts', 'payment', 'rpc', 'state', 'system'
'account', 'author', 'chain', 'contracts', 'engine', 'payment', 'rpc', 'state', 'system'
]);
});
it('allows for the definition of user RPCs', (): void => {
const rpc = new Rpc(registry, new MockProvider(registry), {
testing: [
{
name: 'foo',
testing: {
foo: {
description: 'foo',
params: [{ name: 'bar', type: 'u32' }],
type: 'Balance'
}
]
}
});
expect(isFunction((rpc as any).testing.foo)).toBe(true);
expect(rpc.sections.includes('testing')).toBe(true);
expect(rpc.mapping.get('testing_foo')).toEqual({
description: 'User defined',
isDeprecated: false,
isHidden: false,
isOptional: false,
isSigned: false,
description: 'foo',
isSubscription: false,
jsonrpc: 'testing_foo',
method: 'foo',
params: [{
isOptional: false,
name: 'bar',
type: 'u32'
}],
pubsub: ['', '', ''],
section: 'testing',
type: 'Balance'
});
+48 -68
View File
@@ -3,24 +3,19 @@
// of the Apache-2.0 license. See the LICENSE file for details.
import { ProviderInterface, ProviderInterfaceCallback } from '@polkadot/rpc-provider/types';
import { RpcMethod, RpcSection, RpcParam } from '@polkadot/jsonrpc/types';
import { Hash } from '@polkadot/types/interfaces';
import { AnyJson, Codec, Registry } from '@polkadot/types/types';
import { RpcInterface, RpcInterfaceMethod, UserRpc } from './types';
import { AnyJson, Codec, DefinitionRpc, DefinitionRpcExt, DefinitionRpcSub, Registry } from '@polkadot/types/types';
import { RpcInterface, RpcInterfaceMethod } from './types';
import memoizee from 'memoizee';
import { combineLatest, from, Observable, Observer, of, throwError } from 'rxjs';
import { catchError, map, publishReplay, refCount, switchMap } from 'rxjs/operators';
import jsonrpc from '@polkadot/jsonrpc';
import jsonrpcMethod from '@polkadot/jsonrpc/create/method';
import jsonrpcParam from '@polkadot/jsonrpc/create/param';
import jsonrpc from '@polkadot/types/interfaces/jsonrpc';
import { Option, StorageKey, Vec, createClass, createTypeUnsafe } from '@polkadot/types';
import { assert, isFunction, isNull, isNumber, isUndefined, logger, u8aToU8a } from '@polkadot/util';
import { drr } from './rxjs';
type UserRpcConverted = Record<string, Record<string, RpcMethod>>;
const l = logger('rpc-core');
const EMPTY_META = {
@@ -34,7 +29,7 @@ const EMPTY_META = {
// utility method to create a nicely-formatted error
/** @internal */
function createErrorMessage ({ method, params, type }: RpcMethod, error: Error): string {
function createErrorMessage (method: string, { params, type }: DefinitionRpc, error: Error): string {
const inputs = params.map(({ isOptional, name, type }): string =>
`${name}${isOptional ? '?' : ''}: ${type}`
).join(', ');
@@ -68,7 +63,7 @@ function createErrorMessage ({ method, params, type }: RpcMethod, error: Error):
export default class Rpc implements RpcInterface {
readonly #storageCache = new Map<string, string | null>();
public readonly mapping: Map<string, RpcMethod> = new Map();
public readonly mapping: Map<string, DefinitionRpcExt> = new Map();
public readonly provider: ProviderInterface;
@@ -88,6 +83,8 @@ export default class Rpc implements RpcInterface {
public readonly contracts!: RpcInterface['contracts'];
public readonly engine!: RpcInterface['engine'];
public readonly payment!: RpcInterface['payment'];
public readonly rpc!: RpcInterface['rpc'];
@@ -101,14 +98,14 @@ export default class Rpc implements RpcInterface {
* Default constructor for the Api Object
* @param {ProviderInterface} provider An API provider using HTTP or WebSocket
*/
constructor (registry: Registry, provider: ProviderInterface, userRpc: UserRpc = {}) {
constructor (registry: Registry, provider: ProviderInterface, userRpc: Record<string, Record<string, DefinitionRpc | DefinitionRpcSub>> = {}) {
// eslint-disable-next-line @typescript-eslint/unbound-method
assert(provider && isFunction(provider.send), 'Expected Provider to API create');
this.registry = registry;
this.provider = provider;
this.createInterfaces(jsonrpc, userRpc);
this.createInterfaces(userRpc);
}
/**
@@ -118,47 +115,32 @@ export default class Rpc implements RpcInterface {
this.provider.disconnect();
}
private createInterfaces<Section extends keyof RpcInterface> (interfaces: Record<string, RpcSection>, userBare: UserRpc): void {
private createInterfaces<Section extends keyof RpcInterface> (userRpc: Record<string, Record<string, DefinitionRpc | DefinitionRpcSub>>): void {
const sectionNames = Object.keys(jsonrpc);
// these are the base keys (i.e. part of jsonrpc)
this.sections.push(...Object.keys(interfaces));
this.sections.push(...sectionNames);
// add any extra user-defined sections
this.sections.push(...Object.keys(userBare).filter((key): boolean => !this.sections.includes(key)));
// convert the user inputs into the same format as used in jsonrpc
const user = Object.entries(userBare).reduce((user: UserRpcConverted, [sectionName, methods]): UserRpcConverted => {
user[sectionName] = methods.reduce((section: Record<string, RpcMethod>, def): Record<string, RpcMethod> => {
const { description = 'User defined', name, params, type } = def;
section[name] = jsonrpcMethod(sectionName, name, {
description,
params: params.map(({ isOptional, name, type }): RpcParam =>
jsonrpcParam(name, type as any, { isOptional })),
type: type as any
});
return section;
}, {});
return user;
}, {});
this.sections.push(...Object.keys(userRpc).filter((key): boolean => !this.sections.includes(key)));
// decorate the sections with base and user methods
this.sections.forEach((sectionName): void => {
(this as any)[sectionName as Section] = {
...this.createInterface(sectionName, interfaces[sectionName] ? interfaces[sectionName].methods : {}),
...this.createInterface(sectionName, user[sectionName] || {})
...this.createInterface(sectionName, jsonrpc[sectionName as 'babe'] || {}),
...this.createInterface(sectionName, userRpc[sectionName] || {})
};
});
}
private createInterface<Section extends keyof RpcInterface> (section: string, methods: Record<string, RpcMethod>): RpcInterface[Section] {
private createInterface<Section extends keyof RpcInterface> (section: string, methods: Record<string, DefinitionRpc | DefinitionRpcSub>): RpcInterface[Section] {
return Object
.keys(methods)
.reduce((exposed, method): RpcInterface[Section] => {
const def = methods[method];
const isSubscription = !!(def as DefinitionRpcSub).pubsub;
this.mapping.set(`${section}_${method}`, def);
this.mapping.set(`${section}_${method}`, { ...def, isSubscription, jsonrpc: `${section}_${method}`, method, section });
// FIXME Remove any here
// To do so, remove `RpcInterfaceMethod` from './types.ts', and refactor
@@ -166,9 +148,9 @@ export default class Rpc implements RpcInterface {
// `<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)[method] = def.isSubscription
? this.createMethodSubscribe(def)
: this.createMethodSend(def);
(exposed as any)[method] = isSubscription
? this.createMethodSubscribe(section, method, def as DefinitionRpcSub)
: this.createMethodSend(section, method, def);
return exposed;
}, {} as RpcInterface[Section]);
@@ -182,11 +164,9 @@ export default class Rpc implements RpcInterface {
return call as RpcInterfaceMethod;
}
private createMethodSend (method: RpcMethod): RpcInterfaceMethod {
const rpcName = `${method.section}_${method.method}`;
private createMethodSend (section: string, method: string, def: DefinitionRpc): RpcInterfaceMethod {
const rpcName = `${section}_${method}`;
const creator = (isRaw: boolean) => (...values: any[]): Observable<any> => {
// TODO Warn on deprecated methods
// Here, logically, it should be `of(this.formatInputs(method, values))`.
// However, formatInputs can throw, and when it does, the above way
// doesn't throw in the "Observable loop" (which is internally wrapped in
@@ -194,7 +174,7 @@ export default class Rpc implements RpcInterface {
// - first do `of(1)` - won't throw
// - then do `map(()=>this.formatInputs)` - might throw, but inside Observable.
return of(1).pipe(
map((): Codec[] => this.formatInputs(method, values)),
map((): Codec[] => this.formatInputs(def, values)),
switchMap((params): Observable<[Codec[], any]> =>
combineLatest([
of(params),
@@ -204,10 +184,10 @@ export default class Rpc implements RpcInterface {
map(([params, result]): any =>
isRaw
? this.registry.createType('Raw', result)
: this.formatOutput(method, params, result)
: this.formatOutput(def, params, result)
),
catchError((error): any => {
const message = createErrorMessage(method, error);
const message = createErrorMessage(method, def, error);
// don't scare with old nodes, this is handled transparently
rpcName !== 'rpc_methods' && l.error(message);
@@ -238,17 +218,17 @@ export default class Rpc implements RpcInterface {
});
}
private createMethodSubscribe (method: RpcMethod): RpcInterfaceMethod {
const [updateType, subMethod, unsubMethod] = method.pubsub;
const subName = `${method.section}_${subMethod}`;
const unsubName = `${method.section}_${unsubMethod}`;
const subType = `${method.section}_${updateType}`;
private createMethodSubscribe (section: string, method: string, def: DefinitionRpcSub): RpcInterfaceMethod {
const [updateType, subMethod, unsubMethod] = def.pubsub;
const subName = `${section}_${subMethod}`;
const unsubName = `${section}_${unsubMethod}`;
const subType = `${section}_${updateType}`;
const creator = (isRaw: boolean) => (...values: any[]): Observable<any> => {
return new Observable((observer: Observer<any>): VoidCallback => {
// Have at least an empty promise, as used in the unsubscribe
let subscriptionPromise: Promise<number | void> = Promise.resolve();
const errorHandler = (error: Error): void => {
const message = createErrorMessage(method, error);
const message = createErrorMessage(method, def, error);
l.error(message);
@@ -256,11 +236,11 @@ export default class Rpc implements RpcInterface {
};
try {
const params = this.formatInputs(method, values);
const params = this.formatInputs(def, values);
const paramsJson = params.map((param): AnyJson => param.toJSON());
const update = (error?: Error | null, result?: any): void => {
if (error) {
l.error(createErrorMessage(method, error));
l.error(createErrorMessage(method, def, error));
return;
}
@@ -268,7 +248,7 @@ export default class Rpc implements RpcInterface {
observer.next(
isRaw
? this.registry.createType('Raw', result)
: this.formatOutput(method, params, result)
: this.formatOutput(def, params, result)
);
} catch (error) {
observer.error(error);
@@ -301,7 +281,7 @@ export default class Rpc implements RpcInterface {
? this.provider.unsubscribe(subType, unsubName, subscriptionId)
: Promise.resolve(false)
)
.catch((error: Error): void => l.error(createErrorMessage(method, error)));
.catch((error: Error): void => l.error(createErrorMessage(method, def, error)));
};
}).pipe(drr());
};
@@ -319,16 +299,16 @@ export default class Rpc implements RpcInterface {
return memoized;
}
private formatInputs (method: RpcMethod, inputs: any[]): Codec[] {
const reqArgCount = method.params.filter(({ isOptional }): boolean => !isOptional).length;
const optText = reqArgCount === method.params.length
private formatInputs (def: DefinitionRpc, inputs: any[]): Codec[] {
const reqArgCount = def.params.filter(({ isOptional }): boolean => !isOptional).length;
const optText = reqArgCount === def.params.length
? ''
: ` (${method.params.length - reqArgCount} optional)`;
: ` (${def.params.length - reqArgCount} optional)`;
assert(inputs.length >= reqArgCount && inputs.length <= method.params.length, `Expected ${method.params.length} parameters${optText}, ${inputs.length} found instead`);
assert(inputs.length >= reqArgCount && inputs.length <= def.params.length, `Expected ${def.params.length} parameters${optText}, ${inputs.length} found instead`);
return inputs.map((input, index): Codec =>
createTypeUnsafe(this.registry, method.params[index].type, [input])
createTypeUnsafe(this.registry, def.params[index].type, [input])
);
}
@@ -338,8 +318,8 @@ export default class Rpc implements RpcInterface {
return ['0x3a636f6465'].includes(key.toHex());
}
private formatOutput (method: RpcMethod, params: Codec[], result?: any): Codec | Codec[] {
if (method.type === 'StorageData') {
private formatOutput (rpc: DefinitionRpc, params: Codec[], result?: any): Codec | Codec[] {
if (rpc.type === 'StorageData') {
const key = params[0] as StorageKey;
try {
@@ -349,16 +329,16 @@ export default class Rpc implements RpcInterface {
throw error;
}
} else if (method.type === 'StorageChangeSet') {
} else if (rpc.type === 'StorageChangeSet') {
return this.formatStorageSet(params[0] as Vec<StorageKey>, result.changes);
} else if (method.type === 'Vec<StorageChangeSet>') {
} else if (rpc.type === 'Vec<StorageChangeSet>') {
return result.map(({ block, changes }: { block: string; changes: [string, string | null][] }): [Hash, Codec[]] => [
this.registry.createType('Hash', block),
this.formatStorageSet(params[0] as Vec<StorageKey>, changes)
]);
}
return createTypeUnsafe(this.registry, method.type, [result]);
return createTypeUnsafe(this.registry, rpc.type, [result]);
}
private formatStorageData (key: StorageKey, value: string | null): Codec {
+8 -8
View File
@@ -2,6 +2,8 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { DefinitionRpc } from '@polkadot/types/types';
import { TypeRegistry } from '@polkadot/types';
import Rpc from '.';
@@ -9,23 +11,21 @@ import Rpc from '.';
describe('methodSend', (): void => {
const registry = new TypeRegistry();
let rpc: Rpc;
let methods: any;
let methods: Record<string, DefinitionRpc>;
let provider: any;
beforeEach((): void => {
methods = {
blah: {
method: 'blah',
description: 'test',
params: [
{ name: 'foo', type: 'Bytes' }
],
section: 'test',
type: 'Bytes'
},
bleh: {
method: 'bleh',
description: 'test',
params: [],
section: 'test',
type: 'Bytes'
}
};
@@ -41,7 +41,7 @@ describe('methodSend', (): void => {
it('wraps errors with the call signature', (done): void => {
// private access
const method = (rpc as any).createMethodSend(methods.blah);
const method = (rpc as any).createMethodSend('test', 'blah', methods.blah);
method().subscribe(
(): void => undefined,
@@ -54,7 +54,7 @@ describe('methodSend', (): void => {
it('checks for mismatched parameters', (done): void => {
// private method
const method = (rpc as any).createMethodSend(methods.bleh);
const method = (rpc as any).createMethodSend('test', 'bleh', methods.bleh);
method(1).subscribe(
(): void => undefined,
@@ -66,7 +66,7 @@ describe('methodSend', (): void => {
it('calls the provider with the correct parameters', (done): void => {
// private method
const method = (rpc as any).createMethodSend(methods.blah);
const method = (rpc as any).createMethodSend('test', 'blah', methods.blah);
// Args are length-prefixed, because it's a Bytes
method(new Uint8Array([2 << 2, 0x12, 0x34])).subscribe((): void => {
-9
View File
@@ -10,12 +10,3 @@ export interface RpcInterfaceMethod {
(...params: any[]): Observable<any>;
raw (...params: any[]): Observable<any>;
}
export interface UserRpcMethod {
description?: string;
name: string;
params: { isOptional?: boolean; name: string; type: string }[];
type: string;
}
export type UserRpc = Record<string, UserRpcMethod[]>;