From 7180f8958e63a969e8adeb545a540e31c46b0779 Mon Sep 17 00:00:00 2001 From: Jaco Greeff Date: Wed, 10 Oct 2018 20:18:48 +0200 Subject: [PATCH] @polkadot/api/rx (#233) * api-rx (new) * Update SubmittableExtrinsic.ts * Update types.d.ts * Update index.ts * Retrieve genesisHash * extrinsics -> tx, state -> st * Update index.ts * Type cleanup * Update index.ts * Update index.ts * Update index.ts * Sign returns SubmittableExtrinsic * Example cleanup * Move to api-rx package * Add connected/disconnected events * Expand storage creation * isReady/isConnected * Add static create * Cleanup docs * Cleanups * ReplaySubjects * Basic tests * Add runtimeVersion * Test for version, meta & hash * Package location * Expand README * 2 ways of init * whenReady -> isReady * api-rx -> api/rx * Namings --- docs/SUMMARY.md | 2 + jest.config.js | 5 +- packages/api-observable/README.md | 4 +- packages/api-observable/src/Base.ts | 9 +- packages/api/LICENSE | 15 + packages/api/README.md | 51 ++++ packages/api/package.json | 40 +++ packages/api/src/index.ts | 5 + packages/api/src/rx/SubmittableExtrinsic.ts | 34 +++ packages/api/src/rx/index.ts | 297 ++++++++++++++++++++ packages/api/src/rx/types.d.ts | 53 ++++ packages/api/test/e2e/rx-queries.spec.js | 36 +++ packages/api/test/e2e/rx-transfer.spec.js | 38 +++ packages/rpc-core/src/index.ts | 21 +- packages/rpc-core/src/types.d.ts | 7 +- packages/rpc-rx/src/index.ts | 36 ++- packages/rpc-rx/src/types.d.ts | 10 +- tsconfig.json | 4 +- 18 files changed, 623 insertions(+), 44 deletions(-) create mode 100644 packages/api/LICENSE create mode 100644 packages/api/README.md create mode 100644 packages/api/package.json create mode 100644 packages/api/src/index.ts create mode 100644 packages/api/src/rx/SubmittableExtrinsic.ts create mode 100644 packages/api/src/rx/index.ts create mode 100644 packages/api/src/rx/types.d.ts create mode 100644 packages/api/test/e2e/rx-queries.spec.js create mode 100644 packages/api/test/e2e/rx-transfer.spec.js diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 94d7ad6073..56281a9f9f 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -1,5 +1,7 @@ ## Packages +- [api](api/README.md) + - [ApiRx](api/classes/_rx_index_.apirx.md) - [rpc-core](rpc-core/README.md) - [Rpc](rpc-core/classes/_index_.rpc.md) - [rpc-provider](rpc-provider/README.md) diff --git a/jest.config.js b/jest.config.js index 78a8588b4a..0bc8a3b14b 100644 --- a/jest.config.js +++ b/jest.config.js @@ -2,12 +2,11 @@ const config = require('@polkadot/dev/config/jest'); module.exports = Object.assign({}, config, { moduleNameMapper: { - '@polkadot/api-observable(.*)$': '/packages/api-observable/src/$1', + '@polkadot/api-(observable)(.*)$': '/packages/api-$1/src/$2', + '@polkadot/api$': '/packages/api/src/$1', '@polkadot/rpc-(core|provider|rx)(.*)$': '/packages/rpc-$1/src/$2', '@polkadot/extrinsics(.*)$': '/packages/type-extrinsics/src/$1', '@polkadot/jsonrpc(.*)$': '/packages/type-jsonrpc/src/$1', - '@polkadot/params(.*)$': '/packages/type-params/src/$1', - '@polkadot/primitives(.*)$': '/packages/type-primitives/src/$1', '@polkadot/storage(.*)$': '/packages/type-storage/src/$1', '@polkadot/types(.*)$': '/packages/types/src/$1' } diff --git a/packages/api-observable/README.md b/packages/api-observable/README.md index 8cbde013d5..4c9456a201 100644 --- a/packages/api-observable/README.md +++ b/packages/api-observable/README.md @@ -8,6 +8,6 @@ [![dependency](https://david-dm.org/polkadot-js/api.svg?style=flat-square&path=packages/api-observable)](https://david-dm.org/polkadot-js/api?path=packages/api-observable) [![devDependency](https://david-dm.org/polkadot-js/api/dev-status.svg?style=flat-square&path=packages/api-observable)](https://david-dm.org/polkadot-js/api?path=packages/api-observable#info=devDependencies) -# @polkadot/api-observable +# @polkadot/api -An RxJs wrapper around the [@polkadot/rpc-core](../rpc-core). +An Observable wrapper around [@polkadot/rpc-core](../rpc-core). diff --git a/packages/api-observable/src/Base.ts b/packages/api-observable/src/Base.ts index 0dc2179ff6..e9a5acdf3b 100644 --- a/packages/api-observable/src/Base.ts +++ b/packages/api-observable/src/Base.ts @@ -12,7 +12,7 @@ import extrinsicsStatic from '@polkadot/extrinsics/static'; import storageFromMeta from '@polkadot/storage/fromMetadata'; import storageStatic from '@polkadot/storage/static'; import { Vector } from '@polkadot/types/codec'; -import { Hash } from '@polkadot/types/index'; +import { Hash, Method } from '@polkadot/types/index'; import { StorageFunction } from '@polkadot/types/StorageKey'; import assert from '@polkadot/util/assert'; import isUndefined from '@polkadot/util/is/undefined'; @@ -40,6 +40,8 @@ export default class ApiBase { static extrinsics = extrinsicsStatic; static storage = storageStatic; + // FIXME This logic is duplicated in api-rx, since that should derive + // from that base, it should be removed when the actual extend is done private init (): Promise { let isReady: boolean = false; @@ -56,9 +58,12 @@ export default class ApiBase { const meta = await this._api.state.getMetadata().toPromise(); this._genesisHash = await this._api.chain.getBlockHash(0).toPromise(); + ApiBase.extrinsics = extrinsicsFromMeta(meta); ApiBase.storage = storageFromMeta(meta); + Method.injectExtrinsics(ApiBase.extrinsics); + if (!isReady) { isReady = true; resolveReady(true); @@ -97,6 +102,7 @@ export default class ApiBase { return fn.apply(null, params); } + // FIXME Remove when extending from api-rx rawStorage = (key: StorageFunction, ...params: Array): Observable => { return this .rawStorageMulti([key, ...params] as [StorageFunction, any]) @@ -109,6 +115,7 @@ export default class ApiBase { ); } + // FIXME Remove when extending from api-rx rawStorageMulti = (...keys: Array<[StorageFunction] | [StorageFunction, any]>): Observable => { let observable; diff --git a/packages/api/LICENSE b/packages/api/LICENSE new file mode 100644 index 0000000000..a47c81152d --- /dev/null +++ b/packages/api/LICENSE @@ -0,0 +1,15 @@ +ISC License (ISC) + +Copyright 2017-2018 @polkadot/api authors & contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. diff --git a/packages/api/README.md b/packages/api/README.md new file mode 100644 index 0000000000..448f9f6680 --- /dev/null +++ b/packages/api/README.md @@ -0,0 +1,51 @@ +[![polkadotjs](https://img.shields.io/badge/polkadot-js-orange.svg?style=flat-square)](https://polkadot.js.org) +![isc](https://img.shields.io/badge/license-ISC-lightgrey.svg?style=flat-square) +[![style](https://img.shields.io/badge/code%20style-semistandard-lightgrey.svg?style=flat-square)](https://github.com/Flet/semistandard) +[![npm](https://img.shields.io/npm/v/@polkadot/api.svg?style=flat-square)](https://www.npmjs.com/package/@polkadot/api) +[![travis](https://img.shields.io/travis/polkadot-js/api.svg?style=flat-square)](https://travis-ci.org/polkadot-js/api) +[![maintainability](https://img.shields.io/codeclimate/maintainability/polkadot-js/api.svg?style=flat-square)](https://codeclimate.com/github/polkadot-js/api/maintainability) +[![coverage](https://img.shields.io/coveralls/polkadot-js/api.svg?style=flat-square)](https://coveralls.io/github/polkadot-js/api?branch=master) +[![dependency](https://david-dm.org/polkadot-js/api.svg?style=flat-square&path=packages/api)](https://david-dm.org/polkadot-js/api?path=packages/api) +[![devDependency](https://david-dm.org/polkadot-js/api/dev-status.svg?style=flat-square&path=packages/api)](https://david-dm.org/polkadot-js/api?path=packages/api#info=devDependencies) + +# @polkadot/api + +An RxJs wrapper around [@polkadot/rpc-core](../rpc-core). + +# Usage + +Installation - + +``` +npm install --save @polkadot/api/rx +``` + +Making rpc calls - + +```javascript +import ApiRx from '@polkadot/api/rx'; +// alternatively +// import { ApiRx } from '@polkadot/api'; + +// initialise via Promise & static create +const api = await ApiRx.create().toPromise(); + +// make a call to retrieve the current network head +api.rpc.chain.newHead().subscribe((header) => { + console.log(`Chain is at #${header.blockNumber}`); +}); +``` + +Subscribing to chain state - + +```javascript +import Api from '@polkadot/api'; + +// initialise via isReady & new +new Api().isReady.subscribe((api) => { + // make a call to retrieve the current network head + api.st.timestamp.now().subscribe((timestamp) => { + console.log(`Current block timestamp ${timestamp}`); + }); +}); +``` diff --git a/packages/api/package.json b/packages/api/package.json new file mode 100644 index 0000000000..70d4a71023 --- /dev/null +++ b/packages/api/package.json @@ -0,0 +1,40 @@ +{ + "name": "@polkadot/api", + "version": "0.31.0", + "description": "An RxJS wrapper around the Polkadot JS RPC", + "main": "index.js", + "keywords": [ + "Polkadot", + "RxJs" + ], + "author": "Jaco Greeff ", + "maintainers": [ + "Jaco Greeff " + ], + "contributors": [], + "license": "ISC", + "engines": { + "node": ">=8.0" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/polkadot-js/api.git" + }, + "bugs": { + "url": "https://github.com/polkadot-js/api/issues" + }, + "homepage": "https://github.com/polkadot-js/api/tree/master/packages/api#readme", + "dependencies": { + "@babel/runtime": "^7.0.0", + "@polkadot/rpc-rx": "^0.31.0", + "@polkadot/rpc-provider": "^0.31.0", + "@polkadot/extrinsics": "^0.31.0", + "@polkadot/storage": "^0.31.0", + "@types/rx": "^4.1.1", + "rxjs": "^6.3.2" + } +} diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts new file mode 100644 index 0000000000..111bbe5825 --- /dev/null +++ b/packages/api/src/index.ts @@ -0,0 +1,5 @@ +// 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. + +export { default as ApiRx } from './rx'; diff --git a/packages/api/src/rx/SubmittableExtrinsic.ts b/packages/api/src/rx/SubmittableExtrinsic.ts new file mode 100644 index 0000000000..870c5dd718 --- /dev/null +++ b/packages/api/src/rx/SubmittableExtrinsic.ts @@ -0,0 +1,34 @@ +// 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 { KeyringPair } from '@polkadot/util-keyring/types'; +import { AnyNumber, AnyU8a } from '@polkadot/types/types'; +import { RxApiInterface } from './types'; + +import { Observable } from 'rxjs'; +import { Extrinsic } from '@polkadot/types/index'; + +export default class SubmittableExtrinsic extends Extrinsic { + private _api: RxApiInterface; + + constructor (api: RxApiInterface, extrinsic: Extrinsic) { + super({ + method: extrinsic.method, + signature: extrinsic.signature + }); + + this._api = api; + } + + // TODO needs to be submitAndWatch + send (): Observable { + return this._api.rpc.author.submitExtrinsic(this); + } + + sign (signerPair: KeyringPair, nonce: AnyNumber, blockHash?: AnyU8a): SubmittableExtrinsic { + super.sign(signerPair, nonce, blockHash || this._api.genesisHash); + + return this; + } +} diff --git a/packages/api/src/rx/index.ts b/packages/api/src/rx/index.ts new file mode 100644 index 0000000000..3ae21a3c61 --- /dev/null +++ b/packages/api/src/rx/index.ts @@ -0,0 +1,297 @@ +// 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 { RxApiInterface, QueryableStorageFunction, QueryableModuleStorage, QueryableStorage, SubmittableExtrinsics, SubmittableModuleExtrinsics, SubmittableExtrinsicFunction } from './types'; + +import E3 from 'eventemitter3'; +import { EMPTY, Observable, from } from 'rxjs'; +import { defaultIfEmpty, map } from 'rxjs/operators'; +import WsProvider from '@polkadot/rpc-provider/ws'; +import RpcRx from '@polkadot/rpc-rx/index'; +import { Extrinsics, ExtrinsicFunction } from '@polkadot/extrinsics/types'; +import extrinsicsFromMeta from '@polkadot/extrinsics/fromMetadata'; +import { Storage } from '@polkadot/storage/types'; +import storageFromMeta from '@polkadot/storage/fromMetadata'; +import { Base } from '@polkadot/types/codec'; +import { Hash, Method, RuntimeVersion } from '@polkadot/types/index'; +import RuntimeMetadata from '@polkadot/types/Metadata'; +import assert from '@polkadot/util/assert'; +import isUndefined from '@polkadot/util/is/undefined'; +import logger from '@polkadot/util/logger'; + +import SubmittableExtrinsic from './SubmittableExtrinsic'; +import { StorageFunction } from '@polkadot/types/StorageKey'; + +const l = logger('api'); + +const INIT_ERROR = `Api needs to be initialised before using, listen on 'whenReady'`; + +export default class ApiRx extends E3.EventEmitter implements RxApiInterface { + private _extrinsics?: SubmittableExtrinsics; + private _genesisHash?: Hash; + private _storage?: QueryableStorage; + private _rpc: RpcRx; + private _runtimeMetadata?: RuntimeMetadata; + private _runtimeVersion?: RuntimeVersion; + + // Observable that returns the first time we are connected and loaded + readonly isReady: Observable; + + /** + * @description Creates an ApiRx instance using the supplied provider. Returns an Observable containing the actual Api instance. + * @param wsProvider Optional WebSocket provider that is passed to the class contructor + * @example + *
+ * + * ```javascript + * import Api from '@polkadot/api/rx'; + * + * Api.create().subscribe((api) => { + * api.st.timestamp.now((timestamp) => { + * console.log(`lastest block timestamp ${timestamp}`); + * }); + * }); + * ``` + */ + static create (wsProvider?: WsProvider): Observable { + return new ApiRx(wsProvider).isReady; + } + + /** + * @param wsProvider An optional WebSocket provider from rpc-provider/ws. If not specified, it will default to connecting to the localhost with the default port + * @example + *
+ * + * ```javascript + * import Api from '@polkadot/api/rx'; + * + * new Api().isReady.subscribe((api) => { + * api.rpc.newHead().subscribe((header) => { + * console.log(`new block #${header.blockNumber.toNumber()}`); + * }); + * }); + * ``` + */ + constructor (wsProvider?: WsProvider) { + super(); + + this._rpc = new RpcRx(wsProvider); + + this.isReady = from(this.init()); + } + + /** + * @description Observable that carries the connected state for the provider. Results in a boolean flag that is true/false based on the connectivity. + */ + get isConnected (): Observable { + return this.rpc.isConnected(); + } + + /** + * @description Contains the genesis Hash of the attached chain. Apart from being useful to determine the actual chain, it can also be used to sign immortal transactions. + */ + get genesisHash (): Hash { + assert(!isUndefined(this._genesisHash), INIT_ERROR); + + return this._genesisHash as Hash; + } + + /** + * @description Contains all the raw rpc sections and their subsequent methods in the API as defined by the jsonrpc interface definitions. + * @example + *
+ * + * ```javascript + * api.rpc.chain + * .newHead() + * .subscribe((header) => { + * console.log('new header', header); + * }); + * ``` + */ + get rpc (): RpcRx { + return this._rpc; + } + + /** + * @description Yields the current attached runtime metadata. Generally this is only used to construct extrinsics & storage, but is useful for current runtime inspection. + */ + get runtimeMetadata (): RuntimeMetadata { + assert(!isUndefined(this._runtimeMetadata), INIT_ERROR); + + return this._runtimeMetadata as RuntimeMetadata; + } + + /** + * @description Contains the version information for the current runtime. + */ + get runtimeVersion (): RuntimeVersion { + assert(!isUndefined(this._runtimeVersion), INIT_ERROR); + + return this._runtimeVersion as RuntimeVersion; + } + + /** + * @description Contains all the chain state modules and their subsequent methods in the API. These are attached dynamically from the runtime metadata. + * @example + *
+ * + * ```javascript + * api.st.balances + * .freeBalance() + * .subscribe((balance) => { + * console.log('new balance', balance); + * }); + * ``` + */ + get st (): QueryableStorage { + assert(!isUndefined(this._storage), INIT_ERROR); + + return this._storage as QueryableStorage; + } + + /** + * @description Contains all the extrinsic modules and their subsequent methods in the API. It allows for the construction of transactions and the submission thereof. These are attached dynamically from the runtime metadata. + * @example + *
+ * + * ```javascript + * api.tx.balances + * .transfer(, ) + * .sign(, , ) + * .send() + * .subscribe((status) => { + * console.log('tx status', status); + * }); + * ``` + */ + get tx (): SubmittableExtrinsics { + assert(!isUndefined(this._extrinsics), INIT_ERROR); + + return this._extrinsics as SubmittableExtrinsics; + } + + private init (): Promise { + let isReady: boolean = false; + + this.initEmitters(); + + return new Promise((resolveReady) => { + this.isConnected.subscribe(async (isConnected) => { + // TODO When re-connected (i.e. disconnected and then connected), we want to do a couple of things + // - refresh metadata as needed, decorating again + // - re-create storage subscriptions for those we already have + // - re-watch extrinsics where we have subscriptions already + // - need to refresh genesisHash, extrinsic resub only when it matches + if (isReady || !isConnected) { + return; + } + + try { + this._runtimeMetadata = await this.rpc.state.getMetadata().toPromise(); + this._runtimeVersion = await this.rpc.chain.getRuntimeVersion().toPromise(); + this._genesisHash = await this.rpc.chain.getBlockHash(0).toPromise(); + + const extrinsics = extrinsicsFromMeta(this.runtimeMetadata); + const storage = storageFromMeta(this.runtimeMetadata); + + this._extrinsics = this.decorateExtrinsics(extrinsics); + this._storage = this.decorateStorage(storage); + + Method.injectExtrinsics(extrinsics); + + if (!isReady) { + isReady = true; + resolveReady(this); + + this.emit('ready', this); + } + } catch (error) { + l.error('init', error); + } + }); + }); + } + + private initEmitters (): void { + this.rpc.on('connected', () => { + this.emit('connected'); + }); + + this.rpc.on('disconnected', () => { + this.emit('disconnected'); + }); + } + + private decorateExtrinsics (extrinsics: Extrinsics): SubmittableExtrinsics { + return Object.keys(extrinsics).reduce((result, sectionName) => { + const section = extrinsics[sectionName]; + + result[sectionName] = Object.keys(section).reduce((result, methodName) => { + result[methodName] = this.decorateExtrinsicEntry(section[methodName]); + + return result; + }, {} as SubmittableModuleExtrinsics); + + return result; + }, {} as SubmittableExtrinsics); + } + + private decorateExtrinsicEntry (method: ExtrinsicFunction): SubmittableExtrinsicFunction { + const decorated: any = (...args: Array): SubmittableExtrinsic => + new SubmittableExtrinsic(this, method(...args)); + + decorated.callIndex = method.callIndex; + decorated.meta = method.meta; + decorated.method = method.method; + decorated.section = method.section; + decorated.toJSON = method.toJSON; + + return decorated as SubmittableExtrinsicFunction; + } + + private decorateStorage (storage: Storage): QueryableStorage { + return Object.keys(storage).reduce((result, sectionName) => { + const section = storage[sectionName]; + + result[sectionName] = Object.keys(section).reduce((result, methodName) => { + result[methodName] = this.decorateStorageEntry(section[methodName]); + + return result; + }, {} as QueryableModuleStorage); + + return result; + }, {} as QueryableStorage); + } + + private decorateStorageEntry (method: StorageFunction): QueryableStorageFunction { + const decorated: any = (arg: any): Observable => { + let observable; + + try { + observable = this.rpc.state.storage([[method, arg]]); + } catch (error) { + // in the case of an exception (upon creation of key), just return an empty + observable = EMPTY; + l.warn(`${method.section}.${method.method}: storage subscription:`, error); + } + + // 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 + return observable.pipe( + defaultIfEmpty([]), + map((result: Array = []): Base | null | undefined => + result[0] + ) + ); + }; + + decorated.meta = method.meta; + decorated.method = method.method; + decorated.section = method.section; + decorated.toJSON = method.toJSON; + + return decorated as QueryableStorageFunction; + } +} diff --git a/packages/api/src/rx/types.d.ts b/packages/api/src/rx/types.d.ts new file mode 100644 index 0000000000..3859fa75a3 --- /dev/null +++ b/packages/api/src/rx/types.d.ts @@ -0,0 +1,53 @@ +// 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 { Observable } from 'rxjs'; +import { ExtrinsicFunction } from '@polkadot/extrinsics/types'; +import RpcRx from '@polkadot/rpc-rx/index'; +import { RxRpcInterface$Events } from '@polkadot/rpc-rx/types'; +import { Base } from '@polkadot/types/codec'; +import { Hash, RuntimeVersion } from '@polkadot/types/index'; +import RuntimeMetadata, { FunctionMetadata } from '@polkadot/types/Metadata'; +import { StorageFunction } from '@polkadot/types/StorageKey'; + +import SubmittableExtrinsic from './SubmittableExtrinsic'; + +export interface QueryableStorageFunction extends StorageFunction { + (arg?: any): Observable; +} + +export interface QueryableModuleStorage { + [index: string]: QueryableStorageFunction; +} + +export interface QueryableStorage { + [index: string]: QueryableModuleStorage; +} + +export interface SubmittableExtrinsicFunction extends ExtrinsicFunction { + (...args: any[]): SubmittableExtrinsic; +} + +export interface SubmittableModuleExtrinsics { + [index: string]: SubmittableExtrinsicFunction; +} + +export interface SubmittableExtrinsics { + [index: string]: SubmittableModuleExtrinsics; +} + +export type RxApiInterface$Events = RxRpcInterface$Events | 'ready'; + +export interface RxApiInterface { + readonly genesisHash: Hash; + readonly isConnected: Observable; + readonly isReady: Observable; + readonly rpc: RpcRx; + readonly runtimeMetadata: RuntimeMetadata; + readonly runtimeVersion: RuntimeVersion; + readonly st: QueryableStorage; + readonly tx: SubmittableExtrinsics + + on: (type: RxApiInterface$Events, handler: (...args: Array) => any) => void; +} diff --git a/packages/api/test/e2e/rx-queries.spec.js b/packages/api/test/e2e/rx-queries.spec.js new file mode 100644 index 0000000000..235803f19f --- /dev/null +++ b/packages/api/test/e2e/rx-queries.spec.js @@ -0,0 +1,36 @@ +// 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 testingPairs from '@polkadot/util-keyring/testingPairs'; + +import Api from '../../src/rx'; + +const keyring = testingPairs(); + +describe.skip('e2e queries', () => { + let api; + + beforeEach(async () => { + api = await Api.create().toPromise(); + }); + + it('makes the runtime, rpc, state & extrinsics available', () => { + expect(api.genesisHash).toBeDefined(); + expect(api.runtimeMetadata).toBeDefined(); + expect(api.runtimeVersion).toBeDefined(); + expect(api.rpc).toBeDefined(); + expect(api.st).toBeDefined(); + expect(api.tx).toBeDefined(); + }); + + it('queries state for a balance', (done) => { + api.st.balances.freeBalance(keyring.alice.address()).subscribe((balance) => { + expect( + balance.isZero() + ).toBe(false); + + done(); + }); + }); +}); diff --git a/packages/api/test/e2e/rx-transfer.spec.js b/packages/api/test/e2e/rx-transfer.spec.js new file mode 100644 index 0000000000..898923c39c --- /dev/null +++ b/packages/api/test/e2e/rx-transfer.spec.js @@ -0,0 +1,38 @@ +// 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 { switchMap } from 'rxjs/operators'; +import testingPairs from '@polkadot/util-keyring/testingPairs'; + +import Api from '../../src/rx'; + +const keyring = testingPairs(); + +describe.skip('e2e transfer', () => { + let api; + + beforeEach(async () => { + api = await Api.create().toPromise(); + }); + + it('makes a transfer', (done) => { + api.st.system + .accountNonce(keyring.alice.address()) + .pipe( + switchMap((nonce) => + api.tx.balances + .transfer(keyring.bob.address(), 12345) + .sign(keyring.alice, nonce) + .send() + ) + ) + .subscribe((hash) => { + expect( + hash.toString() + ).not.toEqual('0x'); + + done(); + }); + }); +}); diff --git a/packages/rpc-core/src/index.ts b/packages/rpc-core/src/index.ts index 250156d881..bfa2fc454b 100644 --- a/packages/rpc-core/src/index.ts +++ b/packages/rpc-core/src/index.ts @@ -7,9 +7,8 @@ import { RpcSection, RpcMethod } from '@polkadot/jsonrpc/types'; import { RpcInterface, RpcInterface$Section, RpcInterface$Section$Method } from './types'; import interfaces from '@polkadot/jsonrpc/index'; -import extrinsicsFromMeta from '@polkadot/extrinsics/fromMetadata'; import { Base, Vector, createType } from '@polkadot/types/codec'; -import { Method, StorageChangeSet, StorageKey } from '@polkadot/types/index'; +import { StorageChangeSet, StorageKey } from '@polkadot/types/index'; import assert from '@polkadot/util/assert'; import ExtError from '@polkadot/util/ext/error'; import isFunction from '@polkadot/util/is/function'; @@ -58,8 +57,6 @@ export default class Rpc implements RpcInterface { this.chain = this.createInterface(interfaces.chain); this.state = this.createInterface(interfaces.state); this.system = this.createInterface(interfaces.system); - - this.init(); } /** @@ -85,22 +82,6 @@ export default class Rpc implements RpcInterface { return `${method} (${inputs}): ${type}`; } - private init (): void { - // load metadata and and setup the Method static so we are able to use - // decoding at any point going forward. This is not great, but the best - // we can come up with for now - this.state - .getMetadata() - .then((metadata) => { - Method.injectExtrinsics( - extrinsicsFromMeta(metadata) - ); - }) - .catch((error) => { - console.error('Unable to retrieve metadata', error); - }); - } - private createInterface ({ methods, section }: RpcSection): RpcInterface$Section { return Object .keys(methods) diff --git a/packages/rpc-core/src/types.d.ts b/packages/rpc-core/src/types.d.ts index 8c626f40bb..b351359c88 100644 --- a/packages/rpc-core/src/types.d.ts +++ b/packages/rpc-core/src/types.d.ts @@ -15,6 +15,9 @@ export type RpcInterface$Section = { [index: string]: RpcInterface$Section$Method }; -export type RpcInterface = { - readonly [key in keyof typeof interfaces]: RpcInterface$Section +export interface RpcInterface { + readonly author: RpcInterface$Section; + readonly chain: RpcInterface$Section; + readonly state: RpcInterface$Section; + readonly system: RpcInterface$Section; } diff --git a/packages/rpc-rx/src/index.ts b/packages/rpc-rx/src/index.ts index 61dd804160..39df1847fa 100644 --- a/packages/rpc-rx/src/index.ts +++ b/packages/rpc-rx/src/index.ts @@ -6,7 +6,8 @@ import { RpcInterface, RpcInterface$Section } from '@polkadot/rpc-core/types'; import { ProviderInterface } from '@polkadot/rpc-provider/types'; import { RxRpcInterface, RxRpcInterface$Section } from './types'; -import { BehaviorSubject, Observable, Subscriber, from } from 'rxjs'; +import E3 from 'eventemitter3'; +import { BehaviorSubject, ReplaySubject, Observable, Subscriber, from } from 'rxjs'; import Rpc from '@polkadot/rpc-core/index'; import Ws from '@polkadot/rpc-provider/ws'; import isFunction from '@polkadot/util/is/function'; @@ -16,7 +17,7 @@ import defaults from './defaults'; type CachedMap = { [index: string]: { - [index: string]: BehaviorSubject + [index: string]: ReplaySubject } }; @@ -36,7 +37,7 @@ type CachedMap = { * const api = new RpcRx(provider); * ``` */ -export default class RpcRx implements RxRpcInterface { +export default class RpcRx extends E3.EventEmitter implements RxRpcInterface { private _api: RpcInterface; private _cacheMap: CachedMap; private _isConnected: BehaviorSubject; @@ -49,12 +50,13 @@ export default class RpcRx implements RxRpcInterface { * @param {ProviderInterface} provider An API provider using HTTP or WebSocket */ constructor (provider: ProviderInterface = new Ws(defaults.WS_URL)) { + super(); + this._api = new Rpc(provider); this._cacheMap = {}; this._isConnected = new BehaviorSubject(provider.isConnected()); - provider.on('connected', () => this._isConnected.next(true)); - provider.on('disconnected', () => this._isConnected.next(false)); + this.initEmitters(provider); this.author = this.createInterface('author', this._api.author); this.chain = this.createInterface('chain', this._api.chain); @@ -66,6 +68,20 @@ export default class RpcRx implements RxRpcInterface { return this._isConnected; } + private initEmitters (provider: ProviderInterface): void { + provider.on('connected', () => { + this._isConnected.next(true); + + this.emit('connected'); + }); + + provider.on('disconnected', () => { + this._isConnected.next(false); + + this.emit('disconnected'); + }); + } + private createInterface (sectionName: string, section: RpcInterface$Section): RxRpcInterface$Section { return Object .keys(section) @@ -77,7 +93,7 @@ export default class RpcRx implements RxRpcInterface { }, ({} as RxRpcInterface$Section)); } - private createObservable (subName: string, name: string, section: RpcInterface$Section): (...params: Array) => Observable | BehaviorSubject { + private createObservable (subName: string, name: string, section: RpcInterface$Section): (...params: Array) => Observable | ReplaySubject { if (isFunction(section[name].unsubscribe)) { return this.createCachedObservable(subName, name, section); } @@ -92,12 +108,12 @@ export default class RpcRx implements RxRpcInterface { ); } - private createCachedObservable (subName: string, name: string, section: RpcInterface$Section): (...params: Array) => BehaviorSubject { + private createCachedObservable (subName: string, name: string, section: RpcInterface$Section): (...params: Array) => ReplaySubject { if (!this._cacheMap[subName]) { this._cacheMap[subName] = {}; } - return (...params: Array): BehaviorSubject => { + return (...params: Array): ReplaySubject => { const paramStr = JSON.stringify(params); if (!this._cacheMap[subName][paramStr]) { @@ -108,8 +124,8 @@ export default class RpcRx implements RxRpcInterface { }; } - private createSubject (name: string, params: Array, section: RpcInterface$Section, unsubCallback?: () => void): BehaviorSubject { - const subject = new BehaviorSubject(undefined); + private createSubject (name: string, params: Array, section: RpcInterface$Section, unsubCallback?: () => void): ReplaySubject { + const subject = new ReplaySubject(1); Observable .create((observer: Subscriber): Function => { diff --git a/packages/rpc-rx/src/types.d.ts b/packages/rpc-rx/src/types.d.ts index 0a79430f15..ea056852b7 100644 --- a/packages/rpc-rx/src/types.d.ts +++ b/packages/rpc-rx/src/types.d.ts @@ -2,21 +2,23 @@ // This software may be modified and distributed under the terms // of the ISC license. See the LICENSE file for details. -import { BehaviorSubject, Observable } from 'rxjs'; - +import { ReplaySubject, Observable } from 'rxjs'; import interfaces from '@polkadot/jsonrpc/types'; -export type RxRpcInterface$Method = (...params: Array) => Observable | BehaviorSubject; +export type RxRpcInterface$Method = (...params: Array) => Observable | ReplaySubject; export type RxRpcInterface$Section = { [index: string]: RxRpcInterface$Method }; +export type RxRpcInterface$Events = 'connected' | 'disconnected'; + export type RxRpcInterface = { readonly author: RxRpcInterface$Section; readonly chain: RxRpcInterface$Section; readonly state: RxRpcInterface$Section; readonly system: RxRpcInterface$Section; - isConnected: () => BehaviorSubject + isConnected: () => Observable + on: (type: RxRpcInterface$Events, handler: (...args: Array) => any) => void; } diff --git a/tsconfig.json b/tsconfig.json index b189402d53..e734bbf50c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,13 +6,13 @@ "paths": { "@polkadot/api-observable/*": ["packages/api-observable/src/*"], + "@polkadot/api/*": ["packages/api/src/*"], + "@polkadot/api-rx/*": ["packages/api-rx/src/*"], "@polkadot/rpc-core/*": ["packages/rpc-core/src/*"], "@polkadot/rpc-provider/*": ["packages/rpc-provider/src/*"], "@polkadot/rpc-rx/*": ["packages/rpc-rx/src/*"], "@polkadot/extrinsics/*": ["packages/type-extrinsics/src/*"], "@polkadot/jsonrpc/*": ["packages/type-jsonrpc/src/*"], - "@polkadot/params/*": ["packages/type-params/src/*"], - "@polkadot/primitives/*": ["packages/type-primitives/src/*"], "@polkadot/storage/*": ["packages/type-storage/src/*"], "@polkadot/types/*": ["packages/types/src/*"] },