Allow user-define RPC interfaces (#1504)
* Allow user-define RPC interfaces * Add user-decorated tests * Pacify CC * Typos * Just push.
This commit is contained in:
@@ -2,7 +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 { RpcMethod } from '@polkadot/jsonrpc/types';
|
||||
import { RpcInterface } from '@polkadot/rpc-core/jsonrpc.types';
|
||||
import { Call, Hash, RuntimeVersion } from '@polkadot/types/interfaces';
|
||||
import { AnyFunction, CallFunction, Codec, CodecArg as Arg, ModulesWithCalls } from '@polkadot/types/types';
|
||||
@@ -20,7 +19,6 @@ import { map, switchMap } from 'rxjs/operators';
|
||||
import decorateDerive, { ExactDerive } from '@polkadot/api-derive';
|
||||
import { Constants } from '@polkadot/api-metadata/consts/types';
|
||||
import { Storage } from '@polkadot/api-metadata/storage/types';
|
||||
import jsonrpc from '@polkadot/jsonrpc';
|
||||
import RpcCore from '@polkadot/rpc-core';
|
||||
import { WsProvider } from '@polkadot/rpc-provider';
|
||||
import { Metadata, Null, u64 } from '@polkadot/types';
|
||||
@@ -69,8 +67,6 @@ export default abstract class Decorate<ApiType> extends Events {
|
||||
|
||||
protected _rpcCore: RpcCore;
|
||||
|
||||
private _rpcMap: Map<string, RpcMethod> = new Map();
|
||||
|
||||
protected _runtimeMetadata?: Metadata;
|
||||
|
||||
protected _runtimeVersion?: RuntimeVersion;
|
||||
@@ -125,7 +121,7 @@ export default abstract class Decorate<ApiType> extends Events {
|
||||
this.decorateMethod = decorateMethod;
|
||||
this._options = options;
|
||||
this._type = type;
|
||||
this._rpcCore = new RpcCore(thisProvider);
|
||||
this._rpcCore = new RpcCore(thisProvider, this._options.rpc);
|
||||
this._isConnected = new BehaviorSubject(this._rpcCore.provider.isConnected());
|
||||
this._rx.hasSubscriptions = this._rpcCore.provider.hasSubscriptions;
|
||||
}
|
||||
@@ -173,7 +169,7 @@ export default abstract class Decorate<ApiType> extends Events {
|
||||
const hasResults = methods.length !== 0;
|
||||
|
||||
// loop through all entries we have (populated in decorate) and filter as required
|
||||
[...this._rpcMap.entries()]
|
||||
[...this._rpcCore.mapping.entries()]
|
||||
.filter(([key, { isOptional }]): boolean =>
|
||||
// only remove when we have results and method missing, or with no results if optional
|
||||
hasResults
|
||||
@@ -188,17 +184,14 @@ export default abstract class Decorate<ApiType> extends Events {
|
||||
}
|
||||
|
||||
protected decorateRpc<ApiType> (rpc: RpcCore, decorateMethod: DecorateMethod<ApiType>): DecoratedRpc<ApiType, RpcInterface> {
|
||||
return Object.keys(jsonrpc).reduce((out, _sectionName): DecoratedRpc<ApiType, RpcInterface> => {
|
||||
return rpc.sections.reduce((out, _sectionName): DecoratedRpc<ApiType, RpcInterface> => {
|
||||
const sectionName = _sectionName as keyof DecoratedRpc<ApiType, RpcInterface>;
|
||||
|
||||
// out and section here are horrors to get right from a typing perspective :()
|
||||
// out and section here are horrors to get right from a typing perspective :(
|
||||
(out as any)[sectionName] = Object.entries(rpc[sectionName]).reduce((section, [methodName, method]): DecoratedRpcSection<ApiType, RpcInterface[typeof sectionName]> => {
|
||||
// skip subscriptions where we have a non-subscribable interface
|
||||
// skip subscriptions where we have a non-subscribe interface
|
||||
if (this.hasSubscriptions || !(methodName.startsWith('subscribe') || methodName.startsWith('unsubscribe'))) {
|
||||
(section as any)[methodName] = decorateMethod(method, { methodName });
|
||||
|
||||
// add this endpoint mapping to our internal map - we use this for filters
|
||||
this._rpcMap.set(`${sectionName}_${methodName}`, jsonrpc[sectionName].methods[methodName]);
|
||||
}
|
||||
|
||||
return section;
|
||||
|
||||
@@ -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 { UserRpc } from '@polkadot/rpc-core/types';
|
||||
import { Hash, RuntimeVersion } from '@polkadot/types/interfaces';
|
||||
import { AnyFunction, Callback, CallFunction, Codec, CodecArg, RegistryTypes, SignatureOptions, SignerPayloadJSON, SignerPayloadRaw } from '@polkadot/types/types';
|
||||
import { SubmittableResultImpl, SubmittableExtrinsic } from './submittable/types';
|
||||
@@ -185,6 +186,10 @@ export interface ApiOptions {
|
||||
* connecting to a WsProvider connecting localhost with the default port, i.e. `ws://127.0.0.1:9944`
|
||||
*/
|
||||
provider?: ProviderInterface;
|
||||
/**
|
||||
* @description User-defined RPC methods
|
||||
*/
|
||||
rpc?: UserRpc;
|
||||
/**
|
||||
* @description An external signer which will be used to sign extrinsic when account passed in is not KeyringPair
|
||||
*/
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import MockProvider from '@polkadot/rpc-provider/mock';
|
||||
import { isFunction } from '@polkadot/util';
|
||||
|
||||
import Rpc from '.';
|
||||
|
||||
@@ -19,8 +20,40 @@ describe('Api', (): void => {
|
||||
expect(
|
||||
Object.keys(rpc).filter((key): boolean => !key.startsWith('_'))
|
||||
).toEqual([
|
||||
'provider',
|
||||
'provider', 'mapping', 'sections',
|
||||
'account', 'author', 'chain', 'contracts', 'rpc', 'state', 'system'
|
||||
]);
|
||||
});
|
||||
|
||||
it('allows for the definition of user RPCs', (): void => {
|
||||
const rpc = new Rpc(new MockProvider(), {
|
||||
testing: [
|
||||
{
|
||||
name: '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,
|
||||
isSubscription: false,
|
||||
method: 'foo',
|
||||
params: [{
|
||||
isOptional: false,
|
||||
name: 'bar',
|
||||
type: 'u32'
|
||||
}],
|
||||
pubsub: ['', '', ''],
|
||||
section: 'testing',
|
||||
type: 'Balance'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,19 +3,23 @@
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import { ProviderInterface } from '@polkadot/rpc-provider/types';
|
||||
import { RpcMethod } from '@polkadot/jsonrpc/types';
|
||||
import { RpcMethod, RpcSection, RpcParam } from '@polkadot/jsonrpc/types';
|
||||
import { AnyJson, Codec } from '@polkadot/types/types';
|
||||
import { RpcInterface } from './jsonrpc.types';
|
||||
import { RpcInterfaceMethod } from './types';
|
||||
import { RpcInterfaceMethod, UserRpc } from './types';
|
||||
|
||||
import memoizee from 'memoizee';
|
||||
import { combineLatest, from, Observable, Observer, of, throwError } from 'rxjs';
|
||||
import { catchError, distinctUntilChanged, map, publishReplay, refCount, switchMap } from 'rxjs/operators';
|
||||
import interfaces from '@polkadot/jsonrpc';
|
||||
import jsonrpc from '@polkadot/jsonrpc';
|
||||
import jsonrpcMethod from '@polkadot/jsonrpc/create/method';
|
||||
import jsonrpcParam from '@polkadot/jsonrpc/create/param';
|
||||
import { Option, StorageKey, Vec, createClass } from '@polkadot/types';
|
||||
import { createTypeUnsafe } from '@polkadot/types/codec';
|
||||
import { assert, isFunction, isNull, isNumber, logger, u8aToU8a } from '@polkadot/util';
|
||||
|
||||
type UserRpcConverted = Record<string, Record<string, RpcMethod>>;
|
||||
|
||||
const l = logger('rpc-core');
|
||||
|
||||
const EMPTY_META = {
|
||||
@@ -55,38 +59,40 @@ export default class Rpc implements RpcInterface {
|
||||
|
||||
public readonly provider: ProviderInterface;
|
||||
|
||||
public readonly account: RpcInterface['account'];
|
||||
public readonly mapping: Map<string, RpcMethod> = new Map();
|
||||
|
||||
public readonly author: RpcInterface['author'];
|
||||
public readonly sections: string[] = [];
|
||||
|
||||
public readonly chain: RpcInterface['chain'];
|
||||
// Ok, this is quite horrible - we really should not be using the ! here, but we are actually assigning
|
||||
// these via the createInterfaces inside the constructor. However... this is not quite visible. The reason
|
||||
// why we don't do for individual assignments is to allow user-defined RPCs to also be defined
|
||||
|
||||
public readonly contracts: RpcInterface['contracts'];
|
||||
public readonly account!: RpcInterface['account'];
|
||||
|
||||
public readonly rpc: RpcInterface['rpc'];
|
||||
public readonly author!: RpcInterface['author'];
|
||||
|
||||
public readonly state: RpcInterface['state'];
|
||||
public readonly chain!: RpcInterface['chain'];
|
||||
|
||||
public readonly system: RpcInterface['system'];
|
||||
public readonly contracts!: RpcInterface['contracts'];
|
||||
|
||||
public readonly rpc!: RpcInterface['rpc'];
|
||||
|
||||
public readonly state!: RpcInterface['state'];
|
||||
|
||||
public readonly system!: RpcInterface['system'];
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
* Default constructor for the Api Object
|
||||
* @param {ProviderInterface} provider An API provider using HTTP or WebSocket
|
||||
*/
|
||||
public constructor (provider: ProviderInterface) {
|
||||
public constructor (provider: ProviderInterface, userRpc: UserRpc = {}) {
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
assert(provider && isFunction(provider.send), 'Expected Provider to API create');
|
||||
|
||||
this.provider = provider;
|
||||
|
||||
this.account = this.createInterface('account');
|
||||
this.author = this.createInterface('author');
|
||||
this.contracts = this.createInterface('contracts');
|
||||
this.chain = this.createInterface('chain');
|
||||
this.rpc = this.createInterface('rpc');
|
||||
this.state = this.createInterface('state');
|
||||
this.system = this.createInterface('system');
|
||||
this.createInterfaces(jsonrpc, userRpc);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -105,9 +111,7 @@ export default class Rpc implements RpcInterface {
|
||||
* ```
|
||||
*/
|
||||
public static signature ({ method, params, type }: RpcMethod): string {
|
||||
const inputs = params.map(({ name, type }): string =>
|
||||
`${name}: ${type}`
|
||||
).join(', ');
|
||||
const inputs = params.map(({ name, type }): string => `${name}: ${type}`).join(', ');
|
||||
|
||||
return `${method} (${inputs}): ${type}`;
|
||||
}
|
||||
@@ -123,13 +127,47 @@ export default class Rpc implements RpcInterface {
|
||||
return `${Rpc.signature(method)}:: ${error.message}`;
|
||||
}
|
||||
|
||||
private createInterface<Section extends keyof RpcInterface> (section: Section): RpcInterface[Section] {
|
||||
const { methods } = interfaces[section];
|
||||
private createInterfaces<Section extends keyof RpcInterface> (interfaces: Record<string, RpcSection>, userBare: UserRpc): void {
|
||||
// these are the base keys (i.e. part of jsonrpc)
|
||||
this.sections.push(...Object.keys(interfaces));
|
||||
|
||||
// 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;
|
||||
}, {});
|
||||
|
||||
// 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] || {})
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private createInterface<Section extends keyof RpcInterface> (section: string, methods: Record<string, RpcMethod>): RpcInterface[Section] {
|
||||
return Object
|
||||
.keys(methods)
|
||||
.reduce((exposed, methodName): RpcInterface[Section] => {
|
||||
const def = methods[methodName];
|
||||
.reduce((exposed, method): RpcInterface[Section] => {
|
||||
const def = methods[method];
|
||||
|
||||
this.mapping.set(`${section}_${method}`, def);
|
||||
|
||||
// FIXME Remove any here
|
||||
// To do so, remove `RpcInterfaceMethod` from './types.ts', and refactor
|
||||
@@ -137,7 +175,7 @@ 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)[methodName] = def.isSubscription
|
||||
(exposed as any)[method] = def.isSubscription
|
||||
? this.createMethodSubscribe(def)
|
||||
: this.createMethodSend(def);
|
||||
|
||||
@@ -170,14 +208,12 @@ export default class Rpc implements RpcInterface {
|
||||
const message = this.createErrorMessage(method, error);
|
||||
|
||||
// don't scare with old nodes, this is handled transparently
|
||||
if (rpcName !== 'rpc_methods') {
|
||||
l.error(message);
|
||||
}
|
||||
rpcName !== 'rpc_methods' && l.error(message);
|
||||
|
||||
return throwError(new Error(message));
|
||||
}),
|
||||
publishReplay(1), // create a Replay(1)
|
||||
refCount() // Unsubcribe WS when there are no more subscribers
|
||||
refCount() // Unsubscribe WS when there are no more subscribers
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -7,3 +7,12 @@ import { Observable } from 'rxjs';
|
||||
export interface RpcInterfaceMethod {
|
||||
(...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[]>;
|
||||
|
||||
Reference in New Issue
Block a user