Remove create/* folder for API, include priv methods (#156)
* Remove create/* folder for API, include priv methods * Move create/* into Api-rx as private * Modularize subsequent value caching
This commit is contained in:
@@ -2,12 +2,23 @@
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the ISC license. See the LICENSE file for details.
|
||||
|
||||
import cachedSubscription from './cached';
|
||||
jest.mock('@polkadot/api-provider/ws', () => class {
|
||||
isConnected = () => true;
|
||||
on = () => true;
|
||||
send = () => true;
|
||||
});
|
||||
|
||||
describe('cached', () => {
|
||||
const Api = require('./index').default;
|
||||
|
||||
describe('createCachedObservable', () => {
|
||||
let api;
|
||||
let creator;
|
||||
let section;
|
||||
|
||||
beforeEach(() => {
|
||||
api = new Api();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
const subMethod = jest.fn((name, ...params) => {
|
||||
return Promise.resolve(12345);
|
||||
@@ -26,7 +37,7 @@ describe('cached', () => {
|
||||
subMethod
|
||||
};
|
||||
|
||||
creator = cachedSubscription('test', 'subMethod', section);
|
||||
creator = api.createCachedObservable('test', 'subMethod', section);
|
||||
});
|
||||
|
||||
it('creates a single observable', () => {
|
||||
@@ -9,7 +9,6 @@ jest.mock('@polkadot/api-provider/ws', () => class {
|
||||
on = () => true;
|
||||
send = () => true;
|
||||
});
|
||||
jest.mock('./interface', () => (api, sectionName) => sectionName);
|
||||
|
||||
const Api = require('./index').default;
|
||||
|
||||
|
||||
@@ -2,16 +2,24 @@
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the ISC license. See the LICENSE file for details.
|
||||
|
||||
import { ApiInterface } from '@polkadot/api/types';
|
||||
import { ApiInterface, ApiInterface$Section } from '@polkadot/api/types';
|
||||
import { ProviderInterface } from '@polkadot/api-provider/types';
|
||||
import { Interface$Sections } from '@polkadot/jsonrpc/types';
|
||||
import { RxApiInterface, RxApiInterface$Section } from './types';
|
||||
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { BehaviorSubject, Observable, Subscriber, from } from 'rxjs';
|
||||
import Api from '@polkadot/api/index';
|
||||
import Ws from '@polkadot/api-provider/ws';
|
||||
import isFunction from '@polkadot/util/is/function';
|
||||
import isUndefined from '@polkadot/util/is/undefined';
|
||||
|
||||
import defaults from './defaults';
|
||||
import createInterface from './interface';
|
||||
|
||||
type CachedMap = {
|
||||
[index: string]: {
|
||||
[index: string]: BehaviorSubject<any>
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* An RxJs wrapper around the [[api]].
|
||||
@@ -20,12 +28,14 @@ import createInterface from './interface';
|
||||
*
|
||||
* import RxApi from '@polkadot/api';
|
||||
* import WsProvider from '@polkadot/api-provider/ws';
|
||||
*
|
||||
* const provider = new WsProvider('http://127.0.0.1:9944');
|
||||
* const rxapi = new RxApi(provider);
|
||||
* ```
|
||||
*/
|
||||
export default class RxApi implements RxApiInterface {
|
||||
private _api: ApiInterface;
|
||||
private _cacheMap: CachedMap;
|
||||
private _isConnected: BehaviorSubject<boolean>;
|
||||
readonly author: RxApiInterface$Section;
|
||||
readonly chain: RxApiInterface$Section;
|
||||
@@ -37,18 +47,123 @@ export default class RxApi implements RxApiInterface {
|
||||
*/
|
||||
constructor (provider: ProviderInterface = new Ws(defaults.WS_URL)) {
|
||||
this._api = new Api(provider);
|
||||
this._cacheMap = {};
|
||||
this._isConnected = new BehaviorSubject(provider.isConnected());
|
||||
|
||||
provider.on('connected', () => this._isConnected.next(true));
|
||||
provider.on('disconnected', () => this._isConnected.next(false));
|
||||
|
||||
this.author = createInterface(this._api, 'author');
|
||||
this.chain = createInterface(this._api, 'chain');
|
||||
this.state = createInterface(this._api, 'state');
|
||||
this.system = createInterface(this._api, 'system');
|
||||
this.author = this.createInterface('author');
|
||||
this.chain = this.createInterface('chain');
|
||||
this.state = this.createInterface('state');
|
||||
this.system = this.createInterface('system');
|
||||
}
|
||||
|
||||
isConnected (): BehaviorSubject<boolean> {
|
||||
return this._isConnected;
|
||||
}
|
||||
|
||||
private createInterface (sectionName: Interface$Sections): RxApiInterface$Section {
|
||||
const section: ApiInterface$Section = this._api[sectionName];
|
||||
|
||||
return Object
|
||||
.keys(section)
|
||||
.filter((name) => !['subscribe', 'unsubscribe'].includes(name))
|
||||
.reduce((observables, name) => {
|
||||
observables[name] = this.createObservable(`${sectionName}_${name}`, name, section);
|
||||
|
||||
return observables;
|
||||
}, ({} as RxApiInterface$Section));
|
||||
}
|
||||
|
||||
private createObservable (subName: string, name: string, section: ApiInterface$Section): (...params: Array<any>) => Observable<any> | BehaviorSubject<any> {
|
||||
if (isFunction(section[name].unsubscribe)) {
|
||||
return this.createCachedObservable(subName, name, section);
|
||||
}
|
||||
|
||||
return (...params: Array<any>): Observable<any> =>
|
||||
from(
|
||||
section[name]
|
||||
.apply(null, params)
|
||||
.catch((error: Error) => {
|
||||
console.error(error);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private createCachedObservable (subName: string, name: string, section: ApiInterface$Section): (...params: Array<any>) => BehaviorSubject<any> {
|
||||
if (!this._cacheMap[subName]) {
|
||||
this._cacheMap[subName] = {};
|
||||
}
|
||||
|
||||
return (...params: Array<any>): BehaviorSubject<any> => {
|
||||
const paramStr = JSON.stringify(params);
|
||||
|
||||
if (!this._cacheMap[subName][paramStr]) {
|
||||
this._cacheMap[subName][paramStr] = this.createSubject(name, params, section);
|
||||
}
|
||||
|
||||
return this._cacheMap[subName][paramStr];
|
||||
};
|
||||
}
|
||||
|
||||
private createSubject (name: string, params: Array<any>, section: ApiInterface$Section, unsubCallback?: () => void): BehaviorSubject<any> {
|
||||
const subject = new BehaviorSubject(undefined);
|
||||
|
||||
Observable
|
||||
.create((observer: Subscriber<any>): Function => {
|
||||
try {
|
||||
const fn = section[name];
|
||||
const subscribe = fn(...params, this.createSubjectCallback(observer));
|
||||
|
||||
return (): void => {
|
||||
subscribe
|
||||
.then((subscriptionId: number) =>
|
||||
fn.unsubscribe(subscriptionId)
|
||||
)
|
||||
.then(() =>
|
||||
isFunction(unsubCallback) && unsubCallback()
|
||||
)
|
||||
.catch((error) => {
|
||||
console.error('Unsubscribe failed', error);
|
||||
});
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
return (): void => {
|
||||
console.error('Unsubscribe called on previously failed subscription', error);
|
||||
};
|
||||
}
|
||||
})
|
||||
.subscribe(subject);
|
||||
|
||||
return subject;
|
||||
}
|
||||
|
||||
private createSubjectCallback (observer: Subscriber<any>) {
|
||||
let cachedResult: any;
|
||||
|
||||
return (error: Error | null, result: any) => {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
observer.next();
|
||||
return;
|
||||
}
|
||||
|
||||
if (isUndefined(cachedResult) || !Array.isArray(cachedResult)) {
|
||||
cachedResult = result;
|
||||
} else {
|
||||
const resultArray = (result as Array<any>) || [];
|
||||
|
||||
cachedResult = cachedResult.map((cachedValue, index) =>
|
||||
isUndefined(resultArray[index])
|
||||
? cachedValue
|
||||
: resultArray[index]
|
||||
);
|
||||
}
|
||||
|
||||
observer.next(cachedResult);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
// Copyright 2017-2018 @polkadot/api-rx authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the ISC license. See the LICENSE file for details.
|
||||
|
||||
jest.mock('./observable', () => (subName, name, section) => `${subName}_${name}_${Object.keys(section).join(':')}`);
|
||||
|
||||
const createInterface = require('./interface').default;
|
||||
|
||||
describe('createInterface', () => {
|
||||
let api;
|
||||
let int;
|
||||
|
||||
beforeEach(() => {
|
||||
api = {
|
||||
chain: {
|
||||
foo: 'test',
|
||||
bar: 'test',
|
||||
subscribe: 'noInclude',
|
||||
unsubscribe: 'noInclude'
|
||||
},
|
||||
state: {
|
||||
baz: 'test'
|
||||
}
|
||||
};
|
||||
|
||||
int = createInterface(api, 'chain');
|
||||
});
|
||||
|
||||
it('creates observables for all relevant methods', () => {
|
||||
expect(
|
||||
int
|
||||
).toEqual({
|
||||
foo: 'chain_foo_foo_foo:bar:subscribe:unsubscribe',
|
||||
bar: 'chain_bar_bar_foo:bar:subscribe:unsubscribe'
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,22 +0,0 @@
|
||||
// Copyright 2017-2018 @polkadot/api-rx authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the ISC license. See the LICENSE file for details.
|
||||
|
||||
import { ApiInterface, ApiInterface$Section } from '@polkadot/api/types';
|
||||
import { Interface$Sections } from '@polkadot/jsonrpc/types';
|
||||
import { RxApiInterface$Section } from './types';
|
||||
|
||||
import observable from './observable';
|
||||
|
||||
export default function createInterface (api: ApiInterface, sectionName: Interface$Sections): RxApiInterface$Section {
|
||||
const section: ApiInterface$Section = api[sectionName];
|
||||
|
||||
return Object
|
||||
.keys(section)
|
||||
.filter((name) => !['subscribe', 'unsubscribe'].includes(name))
|
||||
.reduce((observables, name) => {
|
||||
observables[name] = observable(`${sectionName}_${name}`, name, section);
|
||||
|
||||
return observables;
|
||||
}, ({} as RxApiInterface$Section));
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
// Copyright 2017-2018 @polkadot/api-rx authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the ISC license. See the LICENSE file for details.
|
||||
|
||||
import { ApiInterface$Section } from '@polkadot/api/types';
|
||||
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
type CachedMap = {
|
||||
[index: string]: {
|
||||
[index: string]: BehaviorSubject<any>
|
||||
}
|
||||
};
|
||||
|
||||
import subject from './subject';
|
||||
|
||||
const cacheMap: CachedMap = {};
|
||||
|
||||
export default function cached (subName: string, name: string, section: ApiInterface$Section): (...params: Array<any>) => BehaviorSubject<any> {
|
||||
if (!cacheMap[subName]) {
|
||||
cacheMap[subName] = {};
|
||||
}
|
||||
|
||||
return (...params: Array<any>): BehaviorSubject<any> => {
|
||||
const paramStr = JSON.stringify(params);
|
||||
|
||||
if (!cacheMap[subName][paramStr]) {
|
||||
cacheMap[subName][paramStr] = subject(name, params, section);
|
||||
}
|
||||
|
||||
return cacheMap[subName][paramStr];
|
||||
};
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
// Copyright 2017-2018 @polkadot/api-rx authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the ISC license. See the LICENSE file for details.
|
||||
|
||||
import createObservable from './index';
|
||||
|
||||
describe('observable', () => {
|
||||
let section;
|
||||
|
||||
beforeEach(() => {
|
||||
const withSub = () => {};
|
||||
|
||||
withSub.unsubscribe = jest.fn();
|
||||
|
||||
section = {
|
||||
'test_noSub': jest.fn(() => Promise.resolve(true)),
|
||||
'test_withSub': withSub,
|
||||
subscribe: jest.fn((name, params, _callback) => {
|
||||
return Promise.resolve(12345);
|
||||
}),
|
||||
unsubscribe: jest.fn(() => {
|
||||
return Promise.resolve(true);
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
it('creates a single-shot observable', () => {
|
||||
expect(
|
||||
createObservable('test_noSub', 'test_noSub', section)()
|
||||
).toBeDefined();
|
||||
});
|
||||
|
||||
it('creates a subscription observable', () => {
|
||||
expect(
|
||||
createObservable('test_withSub', 'test_withSub', section)()
|
||||
).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -1,27 +0,0 @@
|
||||
// Copyright 2017-2018 @polkadot/api-rx authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the ISC license. See the LICENSE file for details.
|
||||
|
||||
import { ApiInterface$Section } from '@polkadot/api/types';
|
||||
|
||||
import { BehaviorSubject, Observable, from } from 'rxjs';
|
||||
import isFunction from '@polkadot/util/is/function';
|
||||
// import isNull from '@polkadot/util/is/null';
|
||||
// import isUndefined from '@polkadot/util/is/undefined';
|
||||
|
||||
import cached from './cached';
|
||||
|
||||
export default function observable (subName: string, name: string, section: ApiInterface$Section): (...params: Array<any>) => Observable<any> | BehaviorSubject<any> {
|
||||
if (isFunction(section[name].unsubscribe)) {
|
||||
return cached(subName, name, section);
|
||||
}
|
||||
|
||||
return (...params: Array<any>): Observable<any> =>
|
||||
from(
|
||||
section[name]
|
||||
.apply(null, params)
|
||||
.catch((error: Error) => {
|
||||
console.error(error);
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
// Copyright 2017-2018 @polkadot/api-rx authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the ISC license. See the LICENSE file for details.
|
||||
|
||||
import { ApiInterface$Section } from '@polkadot/api/types';
|
||||
|
||||
import { BehaviorSubject, Observable, Subscriber } from 'rxjs';
|
||||
import isUndefined from '@polkadot/util/is/undefined';
|
||||
|
||||
export default function subscription (name: string, params: Array<any>, section: ApiInterface$Section, unsubCallback?: () => void): BehaviorSubject<any> {
|
||||
const subject = new BehaviorSubject(undefined);
|
||||
|
||||
Observable
|
||||
.create((observer: Subscriber<any>): Function => {
|
||||
let cachedResult: any;
|
||||
|
||||
const callback = (error: Error | null, result: any) => {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
observer.next();
|
||||
return;
|
||||
}
|
||||
|
||||
if (isUndefined(cachedResult) || !Array.isArray(cachedResult)) {
|
||||
cachedResult = result;
|
||||
} else {
|
||||
const resultArray = (result as Array<any>) || [];
|
||||
|
||||
cachedResult = cachedResult.map((cachedValue, index) =>
|
||||
isUndefined(resultArray[index])
|
||||
? cachedValue
|
||||
: resultArray[index]
|
||||
);
|
||||
}
|
||||
|
||||
observer.next(cachedResult);
|
||||
};
|
||||
|
||||
try {
|
||||
const fn = section[name];
|
||||
const subscribe = fn(...params, callback);
|
||||
|
||||
return (): void => {
|
||||
subscribe
|
||||
.then((subscriptionId: number) =>
|
||||
fn.unsubscribe(subscriptionId)
|
||||
)
|
||||
.then(() => {
|
||||
if (unsubCallback) {
|
||||
unsubCallback();
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Unsubscribe failed', error);
|
||||
});
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
return (): void => {
|
||||
console.error('Unsubscribe called on previously failed subscription', error);
|
||||
};
|
||||
}
|
||||
})
|
||||
.subscribe(subject);
|
||||
|
||||
return subject;
|
||||
}
|
||||
+13
-2
@@ -2,15 +2,26 @@
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the ISC license. See the LICENSE file for details.
|
||||
|
||||
import createObservable from './subject';
|
||||
jest.mock('@polkadot/api-provider/ws', () => class {
|
||||
isConnected = () => true;
|
||||
on = () => true;
|
||||
send = () => true;
|
||||
});
|
||||
|
||||
const Api = require('./index').default;
|
||||
|
||||
describe('subject', () => {
|
||||
const params = [123, false];
|
||||
let api;
|
||||
let callback;
|
||||
let section;
|
||||
let subscription;
|
||||
let observable;
|
||||
|
||||
beforeEach(() => {
|
||||
api = new Api();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
const subMethod = jest.fn((name, ...params) => {
|
||||
callback = params.pop();
|
||||
@@ -26,7 +37,7 @@ describe('subject', () => {
|
||||
subMethod
|
||||
};
|
||||
|
||||
observable = createObservable('subMethod', params, section);
|
||||
observable = api.createSubject('subMethod', params, section);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -1,52 +0,0 @@
|
||||
// Copyright 2017-2018 @polkadot/api authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the ISC license. See the LICENSE file for details.
|
||||
|
||||
import params from './params';
|
||||
|
||||
describe('params', () => {
|
||||
let methods;
|
||||
|
||||
beforeEach(() => {
|
||||
methods = {
|
||||
array: {
|
||||
params: [
|
||||
{ name: 'test', type: ['Bytes'] }
|
||||
],
|
||||
type: 'Bytes'
|
||||
},
|
||||
blah: {
|
||||
params: [
|
||||
{ name: 'foo', type: 'Bytes' }
|
||||
],
|
||||
type: 'Bytes'
|
||||
},
|
||||
bleh: {
|
||||
params: [
|
||||
{ name: 'foo', type: 'Bytes', isOptional: true }
|
||||
],
|
||||
type: 'Bytes'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
it('check against params', () => {
|
||||
expect(
|
||||
() => params(methods.blah.params, [])
|
||||
).toThrow(/params expected/);
|
||||
});
|
||||
|
||||
it('check against params (optional)', () => {
|
||||
expect(
|
||||
params(methods.bleh.params, [])
|
||||
).toBeDefined();
|
||||
});
|
||||
|
||||
it('formats array inputs correctly', () => {
|
||||
expect(
|
||||
params(methods.array.params, [[new Uint8Array([1]), new Uint8Array([2])]])
|
||||
).toEqual([[
|
||||
'0x01', '0x02'
|
||||
]]);
|
||||
});
|
||||
});
|
||||
@@ -1,36 +0,0 @@
|
||||
// Copyright 2017-2018 @polkadot/api authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the ISC license. See the LICENSE file for details.
|
||||
|
||||
import { Interfaces } from '@polkadot/jsonrpc/types';
|
||||
import { SectionItem } from '@polkadot/params/types';
|
||||
import { Storages } from '@polkadot/storage/types';
|
||||
|
||||
import formatOutput from '@polkadot/api-format/output';
|
||||
import decodeParams from '@polkadot/params/decode';
|
||||
|
||||
function formatStorageOutput (key: SectionItem<Storages>, result?: any): any {
|
||||
return decodeParams(key.type, result, 'latest', true).value;
|
||||
}
|
||||
|
||||
export default function formatResult (method: SectionItem<Interfaces>, params: Array<any>, inputs: Array<any>, result?: any): any {
|
||||
if (method.type === 'StorageResult') {
|
||||
return formatStorageOutput(inputs[0][0], result);
|
||||
}
|
||||
|
||||
if (method.type === 'StorageResultSet') {
|
||||
return params[0].map((key: string, index: number) => {
|
||||
const input = inputs[0][index][0];
|
||||
const { changes = [] }: { block: string, changes: Array<[string, string]> } = result || {};
|
||||
const value = changes.find(([_key]) => key === _key);
|
||||
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return formatStorageOutput(input, value[1]);
|
||||
});
|
||||
}
|
||||
|
||||
return formatOutput(method.type, result);
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
// Copyright 2017-2018 @polkadot/api authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the ISC license. See the LICENSE file for details.
|
||||
|
||||
import isFunction from '@polkadot/util/is/function';
|
||||
|
||||
jest.mock('@polkadot/jsonrpc', () => ({
|
||||
test: {
|
||||
public: {
|
||||
blah: {
|
||||
params: [
|
||||
{ name: 'foo', type: 'Address' }
|
||||
],
|
||||
type: 'Address'
|
||||
},
|
||||
bleh: {
|
||||
params: [],
|
||||
type: 'Address'
|
||||
},
|
||||
pubsub: {
|
||||
isSubscription: true,
|
||||
params: [],
|
||||
type: 'Address'
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
const createInterface = require('./interface').default;
|
||||
|
||||
describe('createInterface', () => {
|
||||
let container;
|
||||
let provider;
|
||||
|
||||
beforeEach(() => {
|
||||
provider = {
|
||||
send: jest.fn((method, params) => {
|
||||
return Promise.resolve(params[0]);
|
||||
})
|
||||
};
|
||||
container = createInterface(provider, 'test');
|
||||
});
|
||||
|
||||
it('adds the specified methods to the interface', () => {
|
||||
expect(Object.keys(container)).toEqual(
|
||||
['blah', 'bleh', 'pubsub']
|
||||
);
|
||||
});
|
||||
|
||||
it('had function calls for the attached methods', () => {
|
||||
expect(isFunction(container.blah)).toEqual(true);
|
||||
expect(isFunction(container.bleh)).toEqual(true);
|
||||
});
|
||||
});
|
||||
@@ -1,36 +0,0 @@
|
||||
// Copyright 2017-2018 @polkadot/api authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the ISC license. See the LICENSE file for details.
|
||||
|
||||
import { Interface$Sections } from '@polkadot/jsonrpc/types';
|
||||
import { ProviderInterface } from '@polkadot/api-provider/types';
|
||||
import { ApiInterface$Section } from '../types';
|
||||
|
||||
import interfaces from '@polkadot/jsonrpc/index';
|
||||
import assert from '@polkadot/util/assert';
|
||||
import isUndefined from '@polkadot/util/is/undefined';
|
||||
|
||||
import methodSend from './methodSend';
|
||||
import methodSubscribe from './methodSubscribe';
|
||||
|
||||
export default function createInterface (provider: ProviderInterface, section: Interface$Sections): ApiInterface$Section {
|
||||
const definition = interfaces[section];
|
||||
|
||||
assert(!isUndefined(definition), `Unable to find section '${section}`);
|
||||
|
||||
// @ts-ignore undefined check done in assert
|
||||
const methods = definition.public;
|
||||
|
||||
return Object
|
||||
.keys(methods)
|
||||
.reduce((exposed, name) => {
|
||||
const rpcName = `${section}_${name}`;
|
||||
const def = methods[name];
|
||||
|
||||
exposed[name] = def.isSubscription
|
||||
? methodSubscribe(provider, rpcName, def)
|
||||
: methodSend(provider, rpcName, def);
|
||||
|
||||
return exposed;
|
||||
}, {} as ApiInterface$Section);
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
// Copyright 2017-2018 @polkadot/api authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the ISC license. See the LICENSE file for details.
|
||||
|
||||
import { Interfaces } from '@polkadot/jsonrpc/types';
|
||||
import { SectionItem } from '@polkadot/params/types';
|
||||
import { ProviderInterface } from '@polkadot/api-provider/types';
|
||||
import { ApiInterface$Section$Method } from '../types';
|
||||
|
||||
import formatInputs from '@polkadot/api-format/input';
|
||||
import ExtError from '@polkadot/util/ext/error';
|
||||
import signature from '@polkadot/params/signature';
|
||||
|
||||
import formatResult from './formatResult';
|
||||
|
||||
export default function createMethodSend (provider: ProviderInterface, rpcName: string, method: SectionItem<Interfaces>): ApiInterface$Section$Method {
|
||||
const call = async (...values: Array<any>): Promise<any> => {
|
||||
// TODO Warn on deprecated methods
|
||||
try {
|
||||
const params = formatInputs(method.params, values);
|
||||
const result = await provider.send(rpcName, params);
|
||||
|
||||
return formatResult(method, params, values, result);
|
||||
} catch (error) {
|
||||
throw new ExtError(`${signature(method)}:: ${error.message}`, (error as ExtError).code, undefined);
|
||||
}
|
||||
};
|
||||
|
||||
return call as ApiInterface$Section$Method;
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
// Copyright 2017-2018 @polkadot/api authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the ISC license. See the LICENSE file for details.
|
||||
|
||||
import { Interfaces } from '@polkadot/jsonrpc/types';
|
||||
import { SectionItem } from '@polkadot/params/types';
|
||||
import { ProviderInterface, ProviderInterface$Callback } from '@polkadot/api-provider/types';
|
||||
import { ApiInterface$Section$Method } from '../types';
|
||||
|
||||
import formatInputs from '@polkadot/api-format/input';
|
||||
import signature from '@polkadot/params/signature';
|
||||
import assert from '@polkadot/util/assert';
|
||||
import ExtError from '@polkadot/util/ext/error';
|
||||
import isFunction from '@polkadot/util/is/function';
|
||||
|
||||
import formatResult from './formatResult';
|
||||
|
||||
export default function methodSubscribe (provider: ProviderInterface, rpcName: string, method: SectionItem<Interfaces>): ApiInterface$Section$Method {
|
||||
const unsubscribe = (subscriptionId: any): Promise<any> =>
|
||||
provider.unsubscribe(rpcName, method.subscribe[1], 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 = formatInputs(method.params, values);
|
||||
const update = (error: Error | null, result?: any) => {
|
||||
cb(error, formatResult(method, params, values, result));
|
||||
};
|
||||
|
||||
return provider.subscribe(rpcName, method.subscribe[0], params, update);
|
||||
} catch (error) {
|
||||
throw new ExtError(`${signature(method)}:: ${error.message}`, (error as ExtError).code, undefined);
|
||||
}
|
||||
};
|
||||
|
||||
const call = _call as ApiInterface$Section$Method;
|
||||
|
||||
call.unsubscribe = unsubscribe;
|
||||
|
||||
return call;
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
// Copyright 2017-2018 @polkadot/api authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the ISC license. See the LICENSE file for details.
|
||||
|
||||
import { Params } from '@polkadot/params/types';
|
||||
|
||||
import formatInputs from '@polkadot/api-format/input';
|
||||
import assert from '@polkadot/util/assert';
|
||||
|
||||
export default function createParams (params: Params, values: Array<any>): Array<any> {
|
||||
const required = params.filter(({ isOptional }) => !isOptional);
|
||||
const optionalText = params.length
|
||||
? ` (${(params.length - required.length) || 'none'} optional)`
|
||||
: '';
|
||||
|
||||
assert(values.length >= required.length && values.length <= params.length, `${params.length || 'no'} params expected${optionalText}, found ${values.length} instead`);
|
||||
|
||||
return formatInputs(params, values);
|
||||
}
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
|
||||
import storage from '@polkadot/storage';
|
||||
|
||||
import Api from '../index';
|
||||
import Api from './index';
|
||||
|
||||
const ADDR_ONE = '5EhmTa7fL6SdjgKXo9g6hetR6nHnRAmrtisoGFWEESjzECtY';
|
||||
const ADDR_TWO = '5FjbQAHSptRU4Q7Py6zDsRnE7UkpZikJVg5nm1tmu3WuZXKd';
|
||||
+109
-7
@@ -2,11 +2,21 @@
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the ISC license. See the LICENSE file for details.
|
||||
|
||||
import { ProviderInterface } from '@polkadot/api-provider/types';
|
||||
import { ApiInterface, ApiInterface$Section } from './types';
|
||||
import { ProviderInterface, ProviderInterface$Callback } from '@polkadot/api-provider/types';
|
||||
import { Interfaces, Interface$Sections } from '@polkadot/jsonrpc/types';
|
||||
import { SectionItem } from '@polkadot/params/types';
|
||||
import { Storages } from '@polkadot/storage/types';
|
||||
import { ApiInterface, ApiInterface$Section, ApiInterface$Section$Method } from './types';
|
||||
|
||||
import formatInputs from '@polkadot/api-format/input';
|
||||
import formatOutput from '@polkadot/api-format/output';
|
||||
import interfaces from '@polkadot/jsonrpc/index';
|
||||
import decodeParams from '@polkadot/params/decode';
|
||||
import signature from '@polkadot/params/signature';
|
||||
import assert from '@polkadot/util/assert';
|
||||
import ExtError from '@polkadot/util/ext/error';
|
||||
import isFunction from '@polkadot/util/is/function';
|
||||
import createInterface from './create/interface';
|
||||
import isUndefined from '@polkadot/util/is/undefined';
|
||||
|
||||
/**
|
||||
* @example
|
||||
@@ -14,6 +24,7 @@ import createInterface from './create/interface';
|
||||
*
|
||||
* import Api from '@polkadot/api';
|
||||
* import WsProvider from '@polkadot/api-provider/ws';
|
||||
*
|
||||
* const provider = new WsProvider('http://127.0.0.1:9944');
|
||||
* const api = new Api(provider);
|
||||
* ```
|
||||
@@ -35,9 +46,100 @@ export default class Api implements ApiInterface {
|
||||
|
||||
this._provider = provider;
|
||||
|
||||
this.author = createInterface(this._provider, 'author');
|
||||
this.chain = createInterface(this._provider, 'chain');
|
||||
this.state = createInterface(this._provider, 'state');
|
||||
this.system = createInterface(this._provider, 'system');
|
||||
this.author = this.createInterface('author');
|
||||
this.chain = this.createInterface('chain');
|
||||
this.state = this.createInterface('state');
|
||||
this.system = this.createInterface('system');
|
||||
}
|
||||
|
||||
private createInterface (section: Interface$Sections): ApiInterface$Section {
|
||||
const definition = interfaces[section];
|
||||
|
||||
assert(!isUndefined(definition), `Unable to find section '${section}`);
|
||||
|
||||
// @ts-ignore undefined check done in assert
|
||||
const methods = definition.public;
|
||||
|
||||
return Object
|
||||
.keys(methods)
|
||||
.reduce((exposed, name) => {
|
||||
const rpcName = `${section}_${name}`;
|
||||
const def = methods[name];
|
||||
|
||||
exposed[name] = def.isSubscription
|
||||
? this.createMethodSubscribe(rpcName, def)
|
||||
: this.createMethodSend(rpcName, def);
|
||||
|
||||
return exposed;
|
||||
}, {} as ApiInterface$Section);
|
||||
}
|
||||
|
||||
private createMethodSend (rpcName: string, method: SectionItem<Interfaces>): ApiInterface$Section$Method {
|
||||
const call = async (...values: Array<any>): Promise<any> => {
|
||||
// TODO Warn on deprecated methods
|
||||
try {
|
||||
const params = formatInputs(method.params, values);
|
||||
const result = await this._provider.send(rpcName, params);
|
||||
|
||||
return this.formatResult(method, params, values, result);
|
||||
} catch (error) {
|
||||
throw new ExtError(`${signature(method)}:: ${error.message}`, (error as ExtError).code, undefined);
|
||||
}
|
||||
};
|
||||
|
||||
return call as ApiInterface$Section$Method;
|
||||
}
|
||||
|
||||
private createMethodSubscribe (rpcName: string, method: SectionItem<Interfaces>): ApiInterface$Section$Method {
|
||||
const unsubscribe = (subscriptionId: any): Promise<any> =>
|
||||
this._provider.unsubscribe(rpcName, method.subscribe[1], 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 = formatInputs(method.params, values);
|
||||
const update = (error: Error | null, result?: any) => {
|
||||
cb(error, this.formatResult(method, params, values, result));
|
||||
};
|
||||
|
||||
return this._provider.subscribe(rpcName, method.subscribe[0], params, update);
|
||||
} catch (error) {
|
||||
throw new ExtError(`${signature(method)}:: ${error.message}`, (error as ExtError).code, undefined);
|
||||
}
|
||||
};
|
||||
|
||||
const call = _call as ApiInterface$Section$Method;
|
||||
|
||||
call.unsubscribe = unsubscribe;
|
||||
|
||||
return call;
|
||||
}
|
||||
|
||||
private formatResult (method: SectionItem<Interfaces>, params: Array<any>, inputs: Array<any>, result?: any): any {
|
||||
if (method.type === 'StorageResult') {
|
||||
return this.formatStorageOutput(inputs[0][0], result);
|
||||
}
|
||||
|
||||
if (method.type === 'StorageResultSet') {
|
||||
return params[0].map((key: string, index: number) => {
|
||||
const input = inputs[0][index][0];
|
||||
const { changes = [] }: { block: string, changes: Array<[string, string]> } = result || {};
|
||||
const value = changes.find(([_key]) => key === _key);
|
||||
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return this.formatStorageOutput(input, value[1]);
|
||||
});
|
||||
}
|
||||
|
||||
return formatOutput(method.type, result);
|
||||
}
|
||||
|
||||
private formatStorageOutput (key: SectionItem<Storages>, result?: any): any {
|
||||
return decodeParams(key.type, result, 'latest', true).value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the ISC license. See the LICENSE file for details.
|
||||
|
||||
import createMethod from './methodSend';
|
||||
import Api from './index';
|
||||
|
||||
describe('methodCall', () => {
|
||||
describe('methodSend', () => {
|
||||
let api;
|
||||
let methods;
|
||||
let provider;
|
||||
|
||||
@@ -29,10 +30,12 @@ describe('methodCall', () => {
|
||||
return Promise.resolve(params[0]);
|
||||
})
|
||||
};
|
||||
|
||||
api = new Api(provider);
|
||||
});
|
||||
|
||||
it('wraps errors with the call signature', () => {
|
||||
const method = createMethod(provider, 'test_blah', methods.blah);
|
||||
const method = api.createMethodSend('test_blah', methods.blah);
|
||||
|
||||
return method().catch((error) => {
|
||||
expect(error.message).toMatch(/blah \(foo: Bytes\): Bytes/);
|
||||
@@ -40,7 +43,7 @@ describe('methodCall', () => {
|
||||
});
|
||||
|
||||
it('checks for mismatched parameters', () => {
|
||||
const method = createMethod(provider, 'test_bleh', methods.bleh);
|
||||
const method = api.createMethodSend('test_bleh', methods.bleh);
|
||||
|
||||
return method(1).catch((error) => {
|
||||
expect(error.message).toMatch(/no params expected, found 1 instead/);
|
||||
@@ -48,7 +51,7 @@ describe('methodCall', () => {
|
||||
});
|
||||
|
||||
it('calls the provider with the correct parameters', () => {
|
||||
const method = createMethod(provider, 'test_blah', methods.blah);
|
||||
const method = api.createMethodSend('test_blah', methods.blah);
|
||||
|
||||
return method(new Uint8Array([0x12, 0x34])).then(() => {
|
||||
expect(provider.send).toHaveBeenCalledWith('test_blah', ['0x1234']);
|
||||
Reference in New Issue
Block a user