This commit is contained in:
ian.he
2019-04-11 13:42:06 +12:00
commit 4e30fab9d6
688 changed files with 51707 additions and 0 deletions
+80
View File
@@ -0,0 +1,80 @@
// Copyright 2017-2019 @polkadot/rpc-core authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import BN from 'bn.js';
import storage from '@plugnet/storage/static';
import Api from '.';
const ADDR_ONE = '5DkQbYAExs3M2sZgT1Ec3mKfZnAQCL4Dt9beTCknkCUn5jzo';
const ADDR_TWO = '5C62W7ELLAAfix9LYrcx5smtcffbhvThkM5x7xfMeYXCtGwF';
const ENC_ONE = '0x4af2c53fce3ec33c6ccccf22e926f1a7';
const ENC_TWO = '0x3e62f7ed6e788e1337bce2a97b68a12a';
describe('formatting', () => {
let api: Api;
let provider: any;
beforeEach(() => {
provider = {
send: jest.fn((method, params) =>
Promise.resolve('0x0102')
),
subscribe: jest.fn((type, method, params, subscription) =>
subscription(null, {
block: '0x1234',
changes: [
[ENC_ONE, '0x0102'],
[ENC_TWO, '0x0201']
]
})
)
};
api = new Api(provider);
});
it('encodes key (with params), decoding response', () => {
return api.state
.getStorage(
[storage.balances.freeBalance, ADDR_ONE]
)
.then((value) => {
expect(
provider.send
).toHaveBeenCalledWith(
'state_getStorage',
[ENC_ONE]
);
expect(value.toNumber()).toEqual(513);
});
});
it('encodes multiple keys, decoding multiple results', (done) => {
return api.state.subscribeStorage(
[
[storage.balances.freeBalance, ADDR_ONE],
[storage.balances.freeBalance, ADDR_TWO]
],
(value: any) => {
console.error(value);
expect(
provider.subscribe
).toHaveBeenCalledWith(
'state_storage',
'state_subscribeStorage',
[[ENC_ONE, ENC_TWO]],
expect.anything()
);
expect(
value.map((balance: BN) =>
balance.toNumber()
)
).toEqual([513, 258]);
done();
});
});
});
+39
View File
@@ -0,0 +1,39 @@
// Copyright 2017-2019 @polkadot/rpc-core authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import Api from '.';
describe('Api', () => {
let api: Api;
let provider: any;
let sendSpy: any;
beforeEach(() => {
provider = {
send: (method: any, params: Array<any>) => {
return Promise.resolve(params[0]);
}
};
sendSpy = jest.spyOn(provider, 'send');
api = new Api(provider);
});
afterEach(() => {
sendSpy.mockRestore();
});
it('requires a provider with a send method', () => {
expect(
() => new Api({} as any)
).toThrow(/Expected Provider/);
});
it('sets up the chain interface', () => {
expect(api.chain).toBeDefined();
});
it('sets up the state interface', () => {
expect(api.state).toBeDefined();
});
});
+250
View File
@@ -0,0 +1,250 @@
// Copyright 2017-2019 @polkadot/rpc-core authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { ProviderInterface, ProviderInterface$Callback } from '@plugnet/rpc-provider/types';
import { RpcSection, RpcMethod } from '@plugnet/jsonrpc/types';
import { RpcInterface, RpcInterface$Method, RpcInterface$Section } from './types';
import interfaces from '@plugnet/jsonrpc';
import { WsProvider } from '@plugnet/rpc-provider';
import { Codec } from '@plugnet/types/types';
import { Option, StorageChangeSet, StorageKey, Vector, createClass, createType } from '@plugnet/types';
import { ExtError, assert, isFunction, isNull, logger } from '@plugnet/util';
const l = logger('rpc-core');
/**
* @name Rpc
* @summary The API may use a HTTP or WebSockets provider.
* @description It allows for querying a Polkadot Client Node.
* WebSockets provider is recommended since HTTP provider only supports basic querying.
*
* ```mermaid
* graph LR;
* A[Api] --> |WebSockets| B[WsProvider];
* B --> |endpoint| C[ws://127.0.0.1:9944]
* ```
*
* @example
* <BR>
*
* ```javascript
* import Api from '@plugnet/rpc-core';
* import WsProvider from '@plugnet/rpc-provider/ws';
*
* const provider = new WsProvider('ws://127.0.0.1:9944');
* const api = new Api(provider);
* ```
*/
export default class Rpc implements RpcInterface {
readonly _provider: ProviderInterface;
readonly author: RpcInterface$Section;
readonly chain: RpcInterface$Section;
readonly state: RpcInterface$Section;
readonly system: RpcInterface$Section;
/**
* @constructor
* Default constructor for the Api Object
* @param {ProviderInterface} provider An API provider using HTTP or WebSocket
*/
constructor (provider: ProviderInterface = new WsProvider()) {
assert(provider && isFunction(provider.send), 'Expected Provider to API create');
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);
}
/**
* @name signature
* @summary Returns a string representation of the method with inputs and outputs.
* @description
* Formats the name, inputs and outputs into a human-readable string. This contains the input parameter names input types and output type.
*
* @example
* <BR>
*
* ```javascript
* import Api from '@plugnet/rpc-core';
*
* Api.signature({ name: 'test_method', params: [ { name: 'dest', type: 'Address' } ], type: 'Address' }); // => test_method (dest: Address): Address
* ```
*/
static signature ({ method, params, type }: RpcMethod): string {
const inputs = params.map(({ name, type }) =>
`${name}: ${type}`
).join(', ');
return `${method} (${inputs}): ${type}`;
}
/**
* @description Manually disconnect from the attached provider
*/
disconnect (): void {
this._provider.disconnect();
}
private createInterface ({ methods }: RpcSection): RpcInterface$Section {
return Object
.keys(methods)
.reduce((exposed, method) => {
const def = methods[method];
exposed[method] = def.isSubscription
? this.createMethodSubscribe(def)
: this.createMethodSend(def);
return exposed;
}, {} as RpcInterface$Section);
}
private createMethodSend (method: RpcMethod): RpcInterface$Method {
const rpcName = `${method.section}_${method.method}`;
const call = async (...values: Array<any>): Promise<any> => {
// TODO Warn on deprecated methods
try {
const params = this.formatInputs(method, values);
const paramsJson = params.map((param) => param.toJSON());
const result = await this._provider.send(rpcName, paramsJson);
return this.formatOutput(method, params, result);
} catch (error) {
const message = `${Rpc.signature(method)}:: ${error.message}`;
l.error(message);
throw new ExtError(message, (error as ExtError).code, undefined);
}
};
return call as RpcInterface$Method;
}
private createMethodSubscribe (method: RpcMethod): RpcInterface$Method {
const [updateType, subMethod, unsubMethod] = method.pubsub;
const subName = `${method.section}_${subMethod}`;
const unsubName = `${method.section}_${unsubMethod}`;
const subType = `${method.section}_${updateType}`;
const unsubscribe = (subscriptionId: any): Promise<any> =>
this._provider.unsubscribe(subType, unsubName, subscriptionId);
const _call = async (...values: Array<any>): Promise<any> => {
try {
const cb: ProviderInterface$Callback = values.pop();
assert(isFunction(cb), `Expected callback in last position of params`);
const params = this.formatInputs(method, values);
const paramsJson = params.map((param) => param.toJSON());
const update = (error: Error | null, result?: any) => {
if (error) {
l.error(`${Rpc.signature(method)}:: ${error.message}`);
return;
}
cb(this.formatOutput(method, params, result));
};
return this._provider.subscribe(subType, subName, paramsJson, update);
} catch (error) {
const message = `${Rpc.signature(method)}:: ${error.message}`;
l.error(message);
throw new ExtError(message, (error as ExtError).code, undefined);
}
};
const call = _call as RpcInterface$Method;
call.unsubscribe = unsubscribe;
return call;
}
private formatInputs (method: RpcMethod, inputs: Array<any>): Array<Codec> {
const reqArgCount = method.params.filter(({ isOptional }) => !isOptional).length;
const optText = reqArgCount === method.params.length
? ''
: ` (${method.params.length - reqArgCount} optional)`;
assert(inputs.length >= reqArgCount && inputs.length <= method.params.length, `Expected ${method.params.length} parameters${optText}, ${inputs.length} found instead`);
return inputs.map((input, index) =>
createType(method.params[index].type, input)
);
}
private formatOutput (method: RpcMethod, params: Array<Codec>, result?: any): Codec | Array<Codec | null | undefined> {
const base = createType(method.type as string, result);
if (method.type === 'StorageData') {
// single return value (via state.getStorage), decode the value based on the
// outputType that we have specified. Fallback to Data on nothing
const key = params[0] as StorageKey;
const type = key.outputType || 'Data';
const Clazz = createClass(type);
const meta = key.meta || { default: undefined, modifier: { isOptional: true } };
if (key.meta && key.meta.type.isMap && key.meta.type.asMap.isLinked) {
// linked map
return new Clazz(base);
} else {
return meta.modifier.isOptional
? new Option(Clazz, isNull(result) ? null : new Clazz(base))
: new Clazz(base);
}
} else if (method.type === 'StorageChangeSet') {
// multiple return values (via state.storage subscription), decode the values
// one at a time, all based on the query types. Three values can be returned -
// - Base - There is a valid value, non-empty
// - null - The storage key is empty (but in the resultset)
// - undefined - The storage value is not in the resultset
return (params[0] as Vector<StorageKey>).reduce((result, key: StorageKey) => {
// Fallback to Data (i.e. just the encoding) if we don't have a specific type
const type = key.outputType || 'Data';
const Clazz = createClass(type);
// see if we have a result value for this specific key
const hexKey = key.toHex();
const { value } = (base as StorageChangeSet).changes.find((item) =>
item.key.toHex() === hexKey
) || { value: null };
const meta = key.meta || { default: undefined, modifier: { isOptional: true } };
if (!value) {
// if we don't have a value, do not fill in the entry, it will be up to the
// caller to sort this out, either ignoring or having a cache for older values
result.push(undefined);
} else {
if (key.meta && key.meta.type.isMap && key.meta.type.asMap.isLinked) {
// linked map
result.push(new Clazz(value.unwrapOr(null)));
} else {
if (meta.modifier.isOptional) {
// create option either with the existing value, or empty when
// there is no value returned
result.push(new Option(Clazz, value.isNone ? null : new Clazz(value.unwrap())));
} else {
// for `null` we fallback to the default value, or create an empty type,
// otherwise we return the actual value as retrieved
result.push(new Clazz(value.unwrapOr(meta.default)));
}
}
}
return result;
}, [] as Array<Codec | null | undefined>);
}
return base;
}
}
+66
View File
@@ -0,0 +1,66 @@
// Copyright 2017-2019 @polkadot/rpc-core authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import Api from '.';
describe('methodSend', () => {
let api: Api;
let methods: any;
let provider: any;
beforeEach(() => {
methods = {
blah: {
method: 'blah',
params: [
{ name: 'foo', type: 'Bytes' }
],
section: 'test',
type: 'Bytes'
},
bleh: {
method: 'bleh',
params: [],
section: 'test',
type: 'Bytes'
}
};
provider = {
send: jest.fn((method, params) => {
return Promise.resolve(params[0]);
})
};
api = new Api(provider);
});
it('wraps errors with the call signature', () => {
// @ts-ignore private method
const method = api.createMethodSend(methods.blah);
return method().catch((error) => {
expect(error.message).toMatch(/blah \(foo: Bytes\): Bytes/);
});
});
it('checks for mismatched parameters', () => {
// @ts-ignore private method
const method = api.createMethodSend(methods.bleh);
return method(1).catch((error) => {
expect(error.message).toMatch(/parameters, 1 found instead/);
});
});
it('calls the provider with the correct parameters', () => {
// @ts-ignore private method
const method = api.createMethodSend(methods.blah);
// Args are length-prefixed, because it's a Bytes
return method(new Uint8Array([2 << 2, 0x12, 0x34])).then(() => {
expect(provider.send).toHaveBeenCalledWith('test_blah', ['0x1234']);
});
});
});
+21
View File
@@ -0,0 +1,21 @@
// Copyright 2017-2019 @polkadot/rpc-core authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
export interface RpcInterface$Method {
(...params: Array<any>): Promise<any>;
subscription: string;
unsubscribe: (id: number) => Promise<any>;
}
export type RpcInterface$Section = {
[index: string]: RpcInterface$Method
};
export interface RpcInterface {
readonly author: RpcInterface$Section;
readonly chain: RpcInterface$Section;
readonly state: RpcInterface$Section;
readonly system: RpcInterface$Section;
}