From fbac330e109ba6ec412963d8f5422b5c358cf23c Mon Sep 17 00:00:00 2001 From: Jaco Greeff Date: Fri, 16 Aug 2019 00:42:54 +0200 Subject: [PATCH] Minor readability enhancements (#1313) * Minor readability enhancements * Split unwieldly ApiBase * Fix import case * Some movements, init in root * Simplify overloaded functions * Some more reworks * Hoist non-class functions * Flatten * Split decorate and init * Split meta from chain * getNext cleanup --- packages/api-derive/src/chain/bestNumber.ts | 14 +- .../src/chain/bestNumberFinalized.ts | 14 +- .../api-derive/src/chain/subscribeNewHead.ts | 38 +- packages/api-derive/src/staking/info.ts | 24 +- packages/api/src/Base.ts | 850 ------------------ packages/api/src/SubmittableExtrinsic.ts | 2 +- packages/api/src/base/Decorate.ts | 316 +++++++ packages/api/src/base/Events.ts | 96 ++ packages/api/src/base/Init.ts | 190 ++++ packages/api/src/base/index.ts | 269 ++++++ packages/api/src/promise/Api.ts | 2 +- packages/api/src/rx/Api.ts | 2 +- packages/api/src/types.ts | 24 +- packages/rpc-core/src/index.ts | 2 +- packages/types/src/types.ts | 22 +- 15 files changed, 940 insertions(+), 925 deletions(-) delete mode 100644 packages/api/src/Base.ts create mode 100644 packages/api/src/base/Decorate.ts create mode 100644 packages/api/src/base/Events.ts create mode 100644 packages/api/src/base/Init.ts create mode 100644 packages/api/src/base/index.ts diff --git a/packages/api-derive/src/chain/bestNumber.ts b/packages/api-derive/src/chain/bestNumber.ts index 28cd65ebb4..4fce9e1851 100644 --- a/packages/api-derive/src/chain/bestNumber.ts +++ b/packages/api-derive/src/chain/bestNumber.ts @@ -2,10 +2,10 @@ // This software may be modified and distributed under the terms // of the Apache-2.0 license. See the LICENSE file for details. -import { BlockNumber, Header } from '@polkadot/types/interfaces'; +import { BlockNumber } from '@polkadot/types/interfaces'; import { Observable } from 'rxjs'; -import { filter, map } from 'rxjs/operators'; +import { map } from 'rxjs/operators'; import { ApiInterfaceRx } from '@polkadot/api/types'; import { drr } from '../util/drr'; @@ -24,10 +24,8 @@ import { drr } from '../util/drr'; */ export function bestNumber (api: ApiInterfaceRx): () => Observable { return (): Observable => - api.rpc.chain.subscribeNewHead() - .pipe( - filter((header: Header): boolean => !!header && !!header.number), - map((header: Header): BlockNumber => header.number.unwrap()), - drr() - ); + api.rpc.chain.subscribeNewHead().pipe( + map((header): BlockNumber => header.number.unwrap()), + drr() + ); } diff --git a/packages/api-derive/src/chain/bestNumberFinalized.ts b/packages/api-derive/src/chain/bestNumberFinalized.ts index 395672edd7..6b373197ef 100644 --- a/packages/api-derive/src/chain/bestNumberFinalized.ts +++ b/packages/api-derive/src/chain/bestNumberFinalized.ts @@ -2,10 +2,10 @@ // This software may be modified and distributed under the terms // of the Apache-2.0 license. See the LICENSE file for details. -import { BlockNumber, Header } from '@polkadot/types/interfaces'; +import { BlockNumber } from '@polkadot/types/interfaces'; import { Observable } from 'rxjs'; -import { filter, map } from 'rxjs/operators'; +import { map } from 'rxjs/operators'; import { ApiInterfaceRx } from '@polkadot/api/types'; import { drr } from '../util/drr'; @@ -25,10 +25,8 @@ import { drr } from '../util/drr'; */ export function bestNumberFinalized (api: ApiInterfaceRx): () => Observable { return (): Observable => - api.rpc.chain.subscribeFinalizedHeads() - .pipe( - filter((header: Header): boolean => !!header && !!header.number), - map((header: Header): BlockNumber => header.number.unwrap()), - drr() - ); + api.rpc.chain.subscribeFinalizedHeads().pipe( + map((header): BlockNumber => header.number.unwrap()), + drr() + ); } diff --git a/packages/api-derive/src/chain/subscribeNewHead.ts b/packages/api-derive/src/chain/subscribeNewHead.ts index 44fc21ee9e..b7b273b614 100644 --- a/packages/api-derive/src/chain/subscribeNewHead.ts +++ b/packages/api-derive/src/chain/subscribeNewHead.ts @@ -6,7 +6,7 @@ import { ApiInterfaceRx } from '@polkadot/api/types'; import { AccountId, Header } from '@polkadot/types/interfaces'; import { Observable, combineLatest, of } from 'rxjs'; -import { filter, map, switchMap } from 'rxjs/operators'; +import { map, switchMap } from 'rxjs/operators'; import { HeaderExtended } from '../type'; import { drr } from '../util/drr'; @@ -28,23 +28,21 @@ export type HeaderAndValidators = [Header, AccountId[]]; */ export function subscribeNewHead (api: ApiInterfaceRx): () => Observable { return (): Observable => - (api.rpc.chain.subscribeNewHead() as Observable
) - .pipe( - filter((header: Header): boolean => !!header && !!header.number), - switchMap((header: Header): Observable => - (combineLatest([ - of(header), - // theoretically we could combine at the first call with session.validators(), however - // we make 100% sure we actually get the validators at a specific block so when these - // change at an era boundary, we have the previous values to ensure our indexes are correct - api.query.session - ? api.query.session.validators.at(header.hash) - : of([]) - ]) as Observable) - ), - map(([header, validators]): HeaderExtended => - new HeaderExtended(header, validators) - ), - drr() - ); + api.rpc.chain.subscribeNewHead().pipe( + switchMap((header: Header): Observable => + (combineLatest([ + of(header), + // theoretically we could combine at the first call with session.validators(), however + // we make 100% sure we actually get the validators at a specific block so when these + // change at an era boundary, we have the previous values to ensure our indexes are correct + api.query.session + ? api.query.session.validators.at(header.hash) + : of([]) + ]) as Observable) + ), + map(([header, validators]): HeaderExtended => + new HeaderExtended(header, validators) + ), + drr() + ); } diff --git a/packages/api-derive/src/staking/info.ts b/packages/api-derive/src/staking/info.ts index a7c093e209..eadcc11bab 100644 --- a/packages/api-derive/src/staking/info.ts +++ b/packages/api-derive/src/staking/info.ts @@ -26,6 +26,7 @@ type MultiResult = [Option, [ Option | Vec<[AccountId, Keys] & Codec>, Option, [AccountId[]], RewardDestination, Exposure, [ValidatorPrefs] ]]; +// groups the supplied chunks by era, i.e. { [era]: BN(total of values) } function groupByEra (list: UnlockChunk[]): Record { return list.reduce((map: Record, { era, value }): Record => { const key = era.toString(); @@ -38,6 +39,7 @@ function groupByEra (list: UnlockChunk[]): Record { }, {}); } +// calculate the remining blocks in a specific unlock era function remainingBlocks (era: BN, eraLength: BN, bestNumber: BlockNumber): BN { const remaining = eraLength.mul(era).sub(bestNumber); @@ -46,15 +48,19 @@ function remainingBlocks (era: BN, eraLength: BN, bestNumber: BlockNumber): BN { : remaining; } +// select the Unlockchunks that can't be redeemed yet +function calcChunks (stakingLedger: StakingLedger, eraLength: BN, bestNumber: BlockNumber): UnlockChunk[] { + return stakingLedger.unlocking.filter((chunk): boolean => + remainingBlocks(chunk.era.unwrap(), eraLength, bestNumber).gtn(0) + ); +} + function calculateUnlocking (stakingLedger: StakingLedger | undefined, eraLength: BN, bestNumber: BlockNumber): DerivedUnlocking | undefined { if (isUndefined(stakingLedger)) { return undefined; } - // select the Unlockchunks that can't be redeemed yet. - const unlockingChunks = stakingLedger.unlocking.filter((chunk): boolean => - remainingBlocks(chunk.era.unwrap(), eraLength, bestNumber).gtn(0) - ); + const unlockingChunks = calcChunks(stakingLedger, eraLength, bestNumber); if (!unlockingChunks.length) { return undefined; @@ -75,13 +81,9 @@ function redeemableSum (stakingLedger: StakingLedger | undefined, eraLength: BN, return new BN(0); } - return stakingLedger.unlocking - .filter((chunk): boolean => - remainingBlocks(chunk.era.unwrap(), eraLength, bestNumber).eqn(0) - ) - .reduce((curr, prev): BN => - curr.add(prev.value.unwrap()), new BN(0) - ); + return calcChunks(stakingLedger, eraLength, bestNumber).reduce((curr, prev): BN => + curr.add(prev.value.unwrap()), new BN(0) + ); } function unwrapSessionIds (stashId: AccountId, queuedKeys: Option | Vec<[AccountId, Keys] & Codec>, nextKeys: Option): { nextSessionIds: AccountId[]; nextSessionId?: AccountId; sessionIds: AccountId[]; sessionId?: AccountId } { diff --git a/packages/api/src/Base.ts b/packages/api/src/Base.ts deleted file mode 100644 index 824c62aeea..0000000000 --- a/packages/api/src/Base.ts +++ /dev/null @@ -1,850 +0,0 @@ -// Copyright 2017-2019 @polkadot/api 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 { RpcInterface } from '@polkadot/rpc-core/jsonrpc.types'; -import { Hash, RuntimeVersion, SignedBlock } from '@polkadot/types/interfaces'; -import { AnyFunction, CallFunction, Codec, CodecArg, ModulesWithCalls, RegistryTypes } from '@polkadot/types/types'; -import { ApiInterfaceRx, ApiInterfaceEvents, ApiOptions, ApiTypes, DecorateMethodOptions, DecoratedRpc, DecoratedRpcSection, QueryableModuleStorage, QueryableStorage, QueryableStorageEntry, QueryableStorageMulti, QueryableStorageMultiArg, QueryableStorageMultiArgs, SignerPayloadRawBase, SubmittableExtrinsicFunction, SubmittableExtrinsics, SubmittableModuleExtrinsics, Signer } from './types'; - -import EventEmitter from 'eventemitter3'; -import { BehaviorSubject, Observable } from 'rxjs'; -import { map, switchMap } from 'rxjs/operators'; -import decorateDerive from '@polkadot/api-derive'; -import constantsFromMeta from '@polkadot/api-metadata/consts/fromMetadata'; -import { Constants } from '@polkadot/api-metadata/consts/types'; -import extrinsicsFromMeta from '@polkadot/api-metadata/extrinsics/fromMetadata'; -import { Storage } from '@polkadot/api-metadata/storage/types'; -import storageFromMeta from '@polkadot/api-metadata/storage/fromMetadata'; -import RpcCore from '@polkadot/rpc-core'; -import { WsProvider } from '@polkadot/rpc-provider'; -import { getTypeRegistry, GenericCall, GenericEvent, Metadata, Null, u64 } from '@polkadot/types'; -import Linkage, { LinkageResult } from '@polkadot/types/codec/Linkage'; -import { DEFAULT_VERSION as EXTRINSIC_DEFAULT_VERSION, LATEST_VERSION as EXTRINSIC_LATEST_VERSION } from '@polkadot/types/primitive/Extrinsic/constants'; -import { StorageEntry } from '@polkadot/types/primitive/StorageKey'; -import { assert, compactStripLength, isString, isUndefined, logger, u8aToHex, u8aToU8a } from '@polkadot/util'; -import { cryptoWaitReady } from '@polkadot/util-crypto'; - -import createSubmittable, { SubmittableExtrinsic } from './SubmittableExtrinsic'; -import { decorateSections } from './util/decorate'; - -interface MetaDecoration { - callIndex?: Uint8Array; - meta: any; - method: string; - section: string; - toJSON: () => any; -} - -interface KeyringSigner { - sign (message: Uint8Array): Uint8Array; -} - -const INIT_ERROR = `Api needs to be initialised before using, listen on 'ready'`; -const KEEPALIVE_INTERVAL = 15000; - -const l = logger('api/decorator'); - -let pkgJson: { name: string; version: string }; - -try { - pkgJson = require('./package.json'); -} catch (error) { - // development environment - pkgJson = { name: '@polkadot/api', version: '-' }; -} - -/** - * Put the `this.onCall` function of ApiRx here, because it is needed by - * `api._rx`. - */ -function rxDecorateMethod (method: Method): Method { - return method; -} - -export default abstract class ApiBase { - private _consts?: Constants; - - private _derive?: ReturnType['decorateDerive']>; - - private _eventemitter: EventEmitter; - - private _extrinsics?: SubmittableExtrinsics; - - private _extrinsicType: number = EXTRINSIC_DEFAULT_VERSION; - - private _genesisHash?: Hash; - - protected _isConnected: BehaviorSubject; - - private _isReady: boolean = false; - - protected readonly _options: ApiOptions; - - private _query?: QueryableStorage; - - private _queryMulti: QueryableStorageMulti; - - private _rpc: DecoratedRpc; - - protected _rpcCore: RpcCore; - - private _runtimeMetadata?: Metadata; - - private _runtimeVersion?: RuntimeVersion; - - private _rx: Partial = {}; - - private _type: ApiTypes; - - /** - * @description Create an instance of the class - * - * @param options Options object to create API instance or a Provider instance - * - * @example - *
- * - * ```javascript - * import Api from '@polkadot/api/promise'; - * - * const api = new Api().isReady(); - * - * api.rpc.subscribeNewHead((header) => { - * console.log(`new block #${header.number.toNumber()}`); - * }); - * ``` - */ - public constructor (options: ApiOptions = {}, type: ApiTypes) { - const thisProvider = options.source - ? options.source._rpcCore.provider.clone() - : (options.provider || new WsProvider()); - - // We only register the types (global) if this is not a cloned instance. - // Do right up-front, so we get in the user types before we are actually - // doing anything on-chain, this ensures we have the overrides in-place - if (!options.source && options.types) { - this.registerTypes(options.types); - } - - this._options = options; - this._type = type; - this._eventemitter = new EventEmitter(); - this._rpcCore = new RpcCore(thisProvider); - this._isConnected = new BehaviorSubject(this._rpcCore.provider.isConnected()); - - assert(this.hasSubscriptions, 'Api can only be used with a provider supporting subscriptions'); - - this._rpc = this.decorateRpc(this._rpcCore, this.decorateMethod); - this._rx.rpc = this.decorateRpc(this._rpcCore, rxDecorateMethod); - this._queryMulti = this.decorateMulti(this.decorateMethod); - this._rx.queryMulti = this.decorateMulti(rxDecorateMethod); - this._rx.signer = options.signer; - - this.init(); - } - - /** - * @description Returns th version of extrinsics in-use on this chain - */ - public get extrinsicVersion (): number { - return this._extrinsicType; - } - - /** - * @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. - */ - public get genesisHash (): Hash { - assert(!isUndefined(this._genesisHash), INIT_ERROR); - - return this._genesisHash as Hash; - } - - /** - * @description `true` when subscriptions are supported - */ - public get hasSubscriptions (): boolean { - return this._rpcCore.provider.hasSubscriptions; - } - - /** - * @description The library information name & version (from package.json) - */ - public get libraryInfo (): string { - return `${pkgJson.name} v${pkgJson.version}`; - } - - /** - * @description Yields the current attached runtime metadata. Generally this is only used to construct extrinsics & storage, but is useful for current runtime inspection. - */ - public get runtimeMetadata (): Metadata { - assert(!isUndefined(this._runtimeMetadata), INIT_ERROR); - - return this._runtimeMetadata as Metadata; - } - - /** - * @description Contains the version information for the current runtime. - */ - public get runtimeVersion (): RuntimeVersion { - assert(!isUndefined(this._runtimeVersion), INIT_ERROR); - - return this._runtimeVersion as RuntimeVersion; - } - - /** - * @description The type of this API instance, either 'rxjs' or 'promise' - */ - public get type (): ApiTypes { - return this._type; - } - - /** - * @description Finds the definition for a specific [[Call]] based on the index supplied - */ - public findCall (callIndex: Uint8Array | string): CallFunction { - return GenericCall.findFunction(u8aToU8a(callIndex)); - } - - /** - * @description Set an external signer which will be used to sign extrinsic when account passed in is not KeyringPair - */ - public setSigner (signer: Signer): void { - this._rx.signer = signer; - } - - /** - * @description Signs a raw signer payload, string or Uint8Array - */ - public async sign (signer: KeyringSigner | string, data: SignerPayloadRawBase): Promise { - // NOTE Do we really want to do this? Or turn it into an observable for rxjs? - if (isString(signer)) { - if (!this._rx.signer || !this._rx.signer.signRaw) { - throw new Error('No signer exists with a signRaw interface'); - } - - return ( - await this._rx.signer.signRaw({ - type: 'bytes', - ...data, - address: signer - }) - ).signature; - } - - return u8aToHex(signer.sign(u8aToU8a(data.data))); - } - - /** - * @description Derived results that are injected into the API, allowing for combinations of various query results. - * - * @example - *
- * - * ```javascript - * api.derive.chain.bestNumber((number) => { - * console.log('best number', number); - * }); - * ``` - */ - public get derive (): ReturnType['decorateDerive']> { - assert(!isUndefined(this._derive), INIT_ERROR); - - return this._derive as ReturnType['decorateDerive']>; - } - - /** - * @description Contains the parameter types (constants) of all modules. - * - * The values are instances of the appropriate type and are accessible using `section`.`constantName`, - * - * @example - *
- * - * ```javascript - * console.log(api.consts.democracy.enactmentPeriod.toString()) - * ``` - */ - public get consts (): Constants { - assert(!isUndefined(this._consts), INIT_ERROR); - - return this._consts as Constants; - } - - /** - * @description Contains all the chain state modules and their subsequent methods in the API. These are attached dynamically from the runtime metadata. - * - * All calls inside the namespace, is denoted by `section`.`method` and may take an optional query parameter. As an example, `api.query.timestamp.now()` (current block timestamp) does not take parameters, while `api.query.system.accountNonce()` (retrieving the associated nonce for an account), takes the `AccountId` as a parameter. - * - * @example - *
- * - * ```javascript - * api.query.balances.freeBalance(, (balance) => { - * console.log('new balance', balance); - * }); - * ``` - */ - public get query (): QueryableStorage { - assert(!isUndefined(this._query), INIT_ERROR); - - return this._query as QueryableStorage; - } - - /** - * @description Allows for the querying of multiple storage entries and the combination thereof into a single result. This is a very optimal way to make multiple queries since it only makes a single connection to the node and retrieves the data over one subscription. - * - * @example - *
- * - * ```javascript - * const unsub = await api.queryMulti( - * [ - * // you can include the storage without any parameters - * api.query.balances.totalIssuance, - * // or you can pass parameters to the storage query - * [api.query.balances.freeBalance, '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY'] - * ], - * ([existential, balance]) => { - * console.log(`You have ${balance.sub(existential)} more than the existential deposit`); - * - * unsub(); - * } - * ); - * ``` - */ - public get queryMulti (): QueryableStorageMulti { - return this._queryMulti; - } - - /** - * @description Contains all the raw rpc sections and their subsequent methods in the API as defined by the jsonrpc interface definitions. Unlike the dynamic `api.query` and `api.tx` sections, these methods are fixed (although extensible with node upgrades) and not determined by the runtime. - * - * RPC endpoints available here allow for the query of chain, node and system information, in addition to providing interfaces for the raw queries of state (usine known keys) and the submission of transactions. - * - * @example - *
- * - * ```javascript - * api.rpc.chain.subscribeNewHead((header) => { - * console.log('new header', header); - * }); - * ``` - */ - public get rpc (): DecoratedRpc { - return this._rpc; - } - - /** - * @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(, ) - * .signAndSend(, ({status}) => { - * console.log('tx status', status.asFinalized.toHex()); - * }); - * ``` - */ - public get tx (): SubmittableExtrinsics { - assert(!isUndefined(this._extrinsics), INIT_ERROR); - - return this._extrinsics as SubmittableExtrinsics; - } - - /** - * @description Disconnect from the underlying provider, halting all comms - */ - public disconnect (): void { - this._rpcCore.disconnect(); - } - - /** - * @description Attach an eventemitter handler to listen to a specific event - * - * @param type The type of event to listen to. Available events are `connected`, `disconnected`, `ready` and `error` - * @param handler The callback to be called when the event fires. Depending on the event type, it could fire with additional arguments. - * - * @example - *
- * - * ```javascript - * api.on('connected', (): void => { - * console.log('API has been connected to the endpoint'); - * }); - * - * api.on('disconnected', (): void => { - * console.log('API has been disconnected from the endpoint'); - * }); - * ``` - */ - public on (type: ApiInterfaceEvents, handler: (...args: any[]) => any): this { - this._eventemitter.on(type, handler); - - return this; - } - - /** - * @description Remove the given eventemitter handler - * - * @param type The type of event the callback was attached to. Available events are `connected`, `disconnected`, `ready` and `error` - * @param handler The callback to unregister. - * - * @example - *
- * - * ```javascript - * const handler = (): void => { - * console.log('Connected !); - * }; - * - * // Start listening - * api.on('connected', handler); - * - * // Stop listening - * api.off('connected', handler); - * ``` - */ - public off (type: ApiInterfaceEvents, handler: (...args: any[]) => any): this { - this._eventemitter.removeListener(type, handler); - - return this; - } - - /** - * @description Attach an one-time eventemitter handler to listen to a specific event - * - * @param type The type of event to listen to. Available events are `connected`, `disconnected`, `ready` and `error` - * @param handler The callback to be called when the event fires. Depending on the event type, it could fire with additional arguments. - * - * @example - *
- * - * ```javascript - * api.once('connected', (): void => { - * console.log('API has been connected to the endpoint'); - * }); - * - * api.once('disconnected', (): void => { - * console.log('API has been disconnected from the endpoint'); - * }); - * ``` - */ - public once (type: ApiInterfaceEvents, handler: (...args: any[]) => any): this { - this._eventemitter.once(type, handler); - - return this; - } - - /** - * @description Register additional user-defined of chain-specific types in the type registry - */ - public registerTypes (types?: RegistryTypes): void { - if (types) { - getTypeRegistry().register(types); - } - } - - /** - * This is the one and only method concrete children classes need to implement. - * It's a higher-order function, which takes one argument - * `method: Method extends (...args: any[]) => Observable` - * (and one optional `options`), and should return the user facing method. - * For example: - * - For ApiRx, `decorateMethod` should just be identity, because the input - * function is already an Observable - * - For ApiPromise, `decorateMethod` should return a function that takes all - * the parameters from `method`, adds an optional `callback` argument, and - * returns a Promise. - * - * We could easily imagine other user-facing interfaces, which are simply - * implemented by transforming the Observable to Stream/Iterator/Kefir/Bacon - * via `deocrateMethod`. - */ - protected abstract decorateMethod(method: (...args: any[]) => Observable, options?: DecorateMethodOptions): any; - - private emit (type: ApiInterfaceEvents, ...args: any[]): void { - this._eventemitter.emit(type, ...args); - } - - private init (): void { - let healthTimer: NodeJS.Timeout | null = null; - - this._rpcCore.provider.on('disconnected', (): void => { - this.emit('disconnected'); - this._isConnected.next(false); - - if (healthTimer) { - clearInterval(healthTimer); - healthTimer = null; - } - }); - - this._rpcCore.provider.on('error', (error): void => { - this.emit('error', error); - }); - - this._rpcCore.provider.on('connected', async (): Promise => { - this.emit('connected'); - this._isConnected.next(true); - - try { - const [hasMeta, cryptoReady] = await Promise.all([ - this.loadMeta(), - cryptoWaitReady() - ]); - - if (hasMeta && !this._isReady && cryptoReady) { - this._isReady = true; - - this.emit('ready', this); - } - - healthTimer = setInterval((): void => { - this._rpcCore.system.health().toPromise().catch((): void => { - // ignore - }); - }, KEEPALIVE_INTERVAL); - } catch (_error) { - const error = new Error(`FATAL: Unable to initialize the API: ${_error.message}`); - - l.error(error); - - this.emit('error', error); - } - }); - } - - private async loadMeta (): Promise { - const { metadata = {} } = this._options; - - // only load from on-chain if we are not a clone (default path), alternatively - // just use the values from the source instance provided - if (!this._options.source || !this._options.source._isReady) { - [this._genesisHash, this._runtimeVersion] = await Promise.all([ - this._rpcCore.chain.getBlockHash(0).toPromise(), - this._rpcCore.chain.getRuntimeVersion().toPromise() - ]); - - // NOTE The SessionKeys definition for Polkadot and Substrate (OpaqueKeys - // implementation) are different. Detect Polkadot and inject the `Keys` - // definition as applicable. (3 keys in substrate vs 4 in Polkadot). - if (this._runtimeVersion.specName.eq('polkadot')) { - getTypeRegistry().register({ Keys: 'SessionKeysPolkadot' }); - } - - const metadataKey = `${this._genesisHash}-${(this._runtimeVersion as RuntimeVersion).specVersion}`; - - if (metadataKey in metadata) { - this._runtimeMetadata = new Metadata(metadata[metadataKey]); - } else { - this._runtimeMetadata = await this._rpcCore.state.getMetadata().toPromise(); - } - - // get unique types & validate - this.runtimeMetadata.getUniqTypes(false); - } else { - this._extrinsicType = this._options.source.extrinsicVersion; - this._runtimeMetadata = this._options.source.runtimeMetadata; - this._runtimeVersion = this._options.source.runtimeVersion; - this._genesisHash = this._options.source.genesisHash; - } - - const extrinsics = extrinsicsFromMeta(this.runtimeMetadata); - const storage = storageFromMeta(this.runtimeMetadata); - const constants = constantsFromMeta(this.runtimeMetadata); - - // only inject if we are not a clone (global init) - if (!this._options.source) { - GenericEvent.injectMetadata(this.runtimeMetadata); - GenericCall.injectMethods(extrinsics); - - // detect the extrinsic version in-use based on the last block - const lastBlock: SignedBlock = await this._rpcCore.chain.getBlock().toPromise(); - const blockExtrinsic = lastBlock.block.extrinsics[0]; - - // If we haven't sync-ed to 1 yes, this won't have any values - this._extrinsicType = blockExtrinsic - ? blockExtrinsic.type - : EXTRINSIC_LATEST_VERSION; - - // HACK Assume that old versions, substrate 1.x is u64 BlockNumber/Nonce - // and has the old EventRecord format. Remove this ASAP with support for - // Alex dropped - if (this._extrinsicType === 1) { - getTypeRegistry().register({ - BlockNumber: 'u64', - Index: 'u64', - EventRecord: 'EventRecord0to76' - }); - } - } - - this._extrinsics = this.decorateExtrinsics(extrinsics, this.decorateMethod); - this._query = this.decorateStorage(storage, this.decorateMethod); - this._consts = constants; - - this._rx.extrinsicType = this._extrinsicType; - this._rx.genesisHash = this._genesisHash; - this._rx.runtimeVersion = this._runtimeVersion; - this._rx.tx = this.decorateExtrinsics(extrinsics, rxDecorateMethod); - this._rx.query = this.decorateStorage(storage, rxDecorateMethod); - this._rx.consts = constants; - - this._derive = this.decorateDerive(this._rx as ApiInterfaceRx, this.decorateMethod); - - return true; - } - - private decorateFunctionMeta (input: MetaDecoration, output: MetaDecoration): MetaDecoration { - output.meta = input.meta; - output.method = input.method; - output.section = input.section; - output.toJSON = input.toJSON; - - if (input.callIndex) { - output.callIndex = input.callIndex; - } - - return output; - } - - private decorateRpc (rpc: RpcCore, decorateMethod: ApiBase['decorateMethod']): DecoratedRpc { - return ['author', 'chain', 'state', 'system'].reduce((result, _sectionName): DecoratedRpc => { - const sectionName = _sectionName as keyof DecoratedRpc; - - // @ts-ignore Hard to type these correctly, I don't understand the TS errors - result[sectionName] = Object.keys(rpc[sectionName]).reduce((section, methodName): DecoratedRpcSection => { - // @ts-ignore - const method = rpc[sectionName][methodName]; - // @ts-ignore - section[methodName] = decorateMethod(method, { methodName }); - - return section; - }, {} as unknown as DecoratedRpcSection); - - return result; - }, {} as unknown as DecoratedRpc); - } - - private decorateMulti (decorateMethod: ApiBase['decorateMethod']): QueryableStorageMulti { - return decorateMethod( - (calls: QueryableStorageMultiArgs): Observable => { - const mapped = calls.map((arg: QueryableStorageMultiArg): [QueryableStorageEntry, ...CodecArg[]] => - // the input is a QueryableStorageEntry, convert to StorageEntry - Array.isArray(arg) - ? [arg[0].creator, ...arg.slice(1)] - : [arg.creator] as any - ); - - return this._rpcCore.state.subscribeStorage(mapped); - }); - } - - private decorateExtrinsics (extrinsics: ModulesWithCalls, decorateMethod: ApiBase['decorateMethod']): SubmittableExtrinsics { - const creator = (value: Uint8Array | string): SubmittableExtrinsic => - createSubmittable(this.type, this._rx as ApiInterfaceRx, decorateMethod, value); - - return Object.keys(extrinsics).reduce((result, sectionName): SubmittableExtrinsics => { - const section = extrinsics[sectionName]; - - result[sectionName] = Object.keys(section).reduce((result, methodName): SubmittableModuleExtrinsics => { - result[methodName] = this.decorateExtrinsicEntry(section[methodName], decorateMethod); - - return result; - }, {} as unknown as SubmittableModuleExtrinsics); - - return result; - }, creator as unknown as SubmittableExtrinsics); - } - - private decorateExtrinsicEntry (method: CallFunction, decorateMethod: ApiBase['decorateMethod']): SubmittableExtrinsicFunction { - const decorated = - (...params: CodecArg[]): SubmittableExtrinsic => - createSubmittable(this.type, this._rx as ApiInterfaceRx, decorateMethod, method(...params)); - - return this.decorateFunctionMeta(method, decorated as any) as SubmittableExtrinsicFunction; - } - - private decorateStorage (storage: Storage, decorateMethod: ApiBase['decorateMethod']): QueryableStorage { - return Object.keys(storage).reduce((result, sectionName): QueryableStorage => { - const section = storage[sectionName]; - - result[sectionName] = Object.keys(section).reduce((result, methodName): QueryableModuleStorage => { - result[methodName] = this.decorateStorageEntry(section[methodName], decorateMethod); - - return result; - }, {} as unknown as QueryableModuleStorage); - - return result; - }, {} as unknown as QueryableStorage); - } - - private decorateStorageEntry (creator: StorageEntry, decorateMethod: ApiBase['decorateMethod']): QueryableStorageEntry { - const decorated = creator.headKey - ? this.decorateStorageEntryLinked(creator, decorateMethod) - : decorateMethod( - (...args: any[]): Observable => { - return this._rpcCore.state - // Unfortunately for one-shot calls we also use .subscribeStorage here - .subscribeStorage<[Codec]>([ - creator.meta.type.isDoubleMap - ? [creator, args] - : [creator, ...args] - ]) - .pipe( - // 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 - map(([data]): Codec => data) - ); - }, { - methodName: creator.method - }); - - decorated.creator = creator; - - decorated.at = decorateMethod( - (hash: Hash, arg1?: CodecArg, arg2?: CodecArg): Observable => - this._rpcCore.state.getStorage( - creator.meta.type.isDoubleMap - ? [creator, [arg1, arg2]] - : [creator, arg1], - hash - ) - ); - - decorated.hash = decorateMethod( - (arg1?: CodecArg, arg2?: CodecArg): Observable => - this._rpcCore.state.getStorageHash( - creator.meta.type.isDoubleMap - ? [creator, [arg1, arg2]] - : [creator, arg1] - ) - ); - - decorated.key = (arg1?: CodecArg, arg2?: CodecArg): string => - u8aToHex(compactStripLength(creator(creator.meta.type.isDoubleMap ? [arg1, arg2] : arg1))[1]); - - // When using double map storage function, user need to path double map key as an array - decorated.multi = decorateMethod( - (args: (CodecArg | CodecArg[])[]): Observable => - this._rpcCore.state - .subscribeStorage( - args.map((arg: CodecArg[] | CodecArg): [StorageEntry, CodecArg | CodecArg[]] => - [creator, arg] - ) - ) - ); - - decorated.size = decorateMethod( - (arg1?: CodecArg, arg2?: CodecArg): Observable => - this._rpcCore.state.getStorageSize( - creator.meta.type.isDoubleMap - ? [creator, [arg1, arg2]] - : [creator, arg1]) - ); - - return this.decorateFunctionMeta(creator, decorated) as unknown as QueryableStorageEntry; - } - - private decorateStorageEntryLinked (creator: StorageEntry, decorateMethod: ApiBase['decorateMethod']): ReturnType['decorateMethod']> { - const result: Map]> = new Map(); - let subject: BehaviorSubject; - let head: Codec | null = null; - - // retrieve a value based on the key, iterating if it has a next entry. Since - // entries can be re-linked in the middle of a list, we subscribe here to make - // sure we catch any updates, no matter the list position - const getNext = (key: Codec): Observable => { - return this._rpcCore.state.subscribeStorage<[[Codec, Linkage]]>([[creator, key]]) - .pipe( - switchMap(([data]: [[Codec, Linkage]]): Observable => { - const linkage = data[1]; - - result.set(key, data); - - // iterate from this key to the children, constructing - // entries for all those found and available - if (linkage.next.isSome) { - return getNext(linkage.next.unwrap()); - } - - const keys = []; - const values = []; - let nextKey = head; - - // loop through the results collected, starting at the head an re-creating - // the list. Our map may have old entries, based on the linking these will - // not be returned in the final result - while (nextKey) { - const entry = result.get(nextKey); - - if (!entry) { - break; - } - - const [item, linkage] = entry; - - keys.push(nextKey); - values.push(item); - - nextKey = linkage.next && linkage.next.unwrapOr(null); - } - - const nextResult = values.length - ? new LinkageResult( - [keys[0].constructor as any, keys], - [values[0].constructor as any, values] - ) - : new LinkageResult( - [Null, []], - [Null, []] - ); - - // we set our result into a subject so we have a single observable for - // which the value changes over time. Initially create, follow-up next - if (subject) { - subject.next(nextResult); - } else { - subject = new BehaviorSubject(nextResult); - } - - return subject; - }) - ); - }; - - // this handles the case where the head changes effectively, i.e. a new entry - // appears at the top of the list, the new getNext gets kicked off - return decorateMethod( - (...args: any[]): Observable]> => - args.length - ? this._rpcCore.state - .subscribeStorage<[[Codec, Linkage]]>([[creator, ...args]]) - .pipe( - map(([data]): [Codec, Linkage] => data) - ) - : this._rpcCore.state - .subscribeStorage<[LinkageResult]>([creator.headKey]) - .pipe( - switchMap(([key]): Observable => { - head = key; - - return getNext(key); - }) - ) - ); - } - - // FIXME I have no idea how to get this done - // eslint-disable-next-line @typescript-eslint/explicit-function-return-type - private decorateDerive (apiRx: ApiInterfaceRx, decorateMethod: ApiBase['decorateMethod']) { - // Pull in derive from api-derive - const derive = decorateDerive(apiRx, this._options.derives); - - return decorateSections(derive, decorateMethod); - } -} diff --git a/packages/api/src/SubmittableExtrinsic.ts b/packages/api/src/SubmittableExtrinsic.ts index 00ab61618b..b80b274808 100644 --- a/packages/api/src/SubmittableExtrinsic.ts +++ b/packages/api/src/SubmittableExtrinsic.ts @@ -13,7 +13,7 @@ import { createType, Vec } from '@polkadot/types'; import { isBn, isFunction, isNumber, isUndefined } from '@polkadot/util'; import filterEvents from './util/filterEvents'; -import ApiBase from './Base'; +import ApiBase from './base'; import SignerPayload from './SignerPayload'; // eslint-disable-next-line @typescript-eslint/interface-name-prefix diff --git a/packages/api/src/base/Decorate.ts b/packages/api/src/base/Decorate.ts new file mode 100644 index 0000000000..38ba559e11 --- /dev/null +++ b/packages/api/src/base/Decorate.ts @@ -0,0 +1,316 @@ +// Copyright 2017-2019 @polkadot/api 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 { RpcInterface } from '@polkadot/rpc-core/jsonrpc.types'; +import { Hash, RuntimeVersion } from '@polkadot/types/interfaces'; +import { AnyFunction, CallFunction, Codec, CodecArg as Arg, ModulesWithCalls } from '@polkadot/types/types'; +import { ApiInterfaceRx, ApiOptions, ApiTypes, DecorateMethodOptions, DecoratedRpc, DecoratedRpcSection, QueryableModuleStorage, QueryableStorage, QueryableStorageEntry, QueryableStorageMulti, QueryableStorageMultiArg, QueryableStorageMultiArgs, SubmittableExtrinsicFunction, SubmittableExtrinsics, SubmittableModuleExtrinsics } from '../types'; + +import BN from 'bn.js'; +import { BehaviorSubject, Observable } from 'rxjs'; +import { map, switchMap } from 'rxjs/operators'; +import decorateDerive from '@polkadot/api-derive'; +import { Constants } from '@polkadot/api-metadata/consts/types'; +import { Storage } from '@polkadot/api-metadata/storage/types'; +import RpcCore from '@polkadot/rpc-core'; +import { WsProvider } from '@polkadot/rpc-provider'; +import { Metadata, Null, u64 } from '@polkadot/types'; +import Linkage, { LinkageResult } from '@polkadot/types/codec/Linkage'; +import { DEFAULT_VERSION as EXTRINSIC_DEFAULT_VERSION } from '@polkadot/types/primitive/Extrinsic/constants'; +import { StorageEntry } from '@polkadot/types/primitive/StorageKey'; +import { compactStripLength, u8aToHex } from '@polkadot/util'; + +import createSubmittable, { SubmittableExtrinsic } from '../SubmittableExtrinsic'; +import { decorateSections } from '../util/decorate'; +import Events from './Events'; + +interface MetaDecoration { + callIndex?: Uint8Array; + meta: any; + method: string; + section: string; + toJSON: () => any; +} + +export default abstract class Decorate extends Events { + // HACK Use BN import so decorateDerive works... yes, wtf. + protected __phantom = new BN(0); + + protected _consts?: Constants; + + protected _derive?: ReturnType['decorateDerive']>; + + protected _extrinsics?: SubmittableExtrinsics; + + protected _extrinsicType: number = EXTRINSIC_DEFAULT_VERSION; + + protected _genesisHash?: Hash; + + protected _isConnected: BehaviorSubject; + + protected _isReady: boolean = false; + + protected readonly _options: ApiOptions; + + protected _query?: QueryableStorage; + + protected _queryMulti?: QueryableStorageMulti; + + protected _rpc?: DecoratedRpc; + + protected _rpcCore: RpcCore; + + protected _runtimeMetadata?: Metadata; + + protected _runtimeVersion?: RuntimeVersion; + + protected _rx: Partial = {}; + + protected _type: ApiTypes; + + /** + * @description Create an instance of the class + * + * @param options Options object to create API instance or a Provider instance + * + * @example + *
+ * + * ```javascript + * import Api from '@polkadot/api/promise'; + * + * const api = new Api().isReady(); + * + * api.rpc.subscribeNewHead((header) => { + * console.log(`new block #${header.number.toNumber()}`); + * }); + * ``` + */ + public constructor (options: ApiOptions, type: ApiTypes) { + super(); + + const thisProvider = options.source + ? options.source._rpcCore.provider.clone() + : (options.provider || new WsProvider()); + + this._options = options; + this._type = type; + this._rpcCore = new RpcCore(thisProvider); + this._isConnected = new BehaviorSubject(this._rpcCore.provider.isConnected()); + } + + /** + * This is the one and only method concrete children classes need to implement. + * It's a higher-order function, which takes one argument + * `method: Method extends (...args: any[]) => Observable` + * (and one optional `options`), and should return the user facing method. + * For example: + * - For ApiRx, `decorateMethod` should just be identity, because the input + * function is already an Observable + * - For ApiPromise, `decorateMethod` should return a function that takes all + * the parameters from `method`, adds an optional `callback` argument, and + * returns a Promise. + * + * We could easily imagine other user-facing interfaces, which are simply + * implemented by transforming the Observable to Stream/Iterator/Kefir/Bacon + * via `deocrateMethod`. + */ + protected abstract decorateMethod(method: (...args: any[]) => Observable, options?: DecorateMethodOptions): any; + + private decorateFunctionMeta (input: MetaDecoration, output: MetaDecoration): MetaDecoration { + output.meta = input.meta; + output.method = input.method; + output.section = input.section; + output.toJSON = input.toJSON; + + if (input.callIndex) { + output.callIndex = input.callIndex; + } + + return output; + } + + protected decorateRpc (rpc: RpcCore, decorateMethod: Decorate['decorateMethod']): DecoratedRpc { + return ['author', 'chain', 'state', 'system'].reduce((out, _sectionName): DecoratedRpc => { + const sectionName = _sectionName as keyof DecoratedRpc; + + // @ts-ignore Hard to type these correctly, I don't understand the TS errors + out[sectionName] = Object.entries(rpc[sectionName]).reduce((section, [methodName, method]): DecoratedRpcSection => { + // @ts-ignore + section[methodName] = decorateMethod(method, { methodName }); + + return section; + }, {} as unknown as DecoratedRpcSection); + + return out; + }, {} as unknown as DecoratedRpc); + } + + protected decorateMulti (decorateMethod: Decorate['decorateMethod']): QueryableStorageMulti { + return decorateMethod((calls: QueryableStorageMultiArgs): Observable => + this._rpcCore.state.subscribeStorage( + calls.map((arg: QueryableStorageMultiArg): [QueryableStorageEntry, ...Arg[]] => + // the input is a QueryableStorageEntry, convert to StorageEntry + Array.isArray(arg) + ? [arg[0].creator, ...arg.slice(1)] + : [arg.creator] as any))); + } + + protected decorateExtrinsics (extrinsics: ModulesWithCalls, decorateMethod: Decorate['decorateMethod']): SubmittableExtrinsics { + const creator = (value: Uint8Array | string): SubmittableExtrinsic => + createSubmittable(this._type, this._rx as ApiInterfaceRx, decorateMethod, value); + + return Object.entries(extrinsics).reduce((out, [name, section]): SubmittableExtrinsics => { + out[name] = Object.entries(section).reduce((out, [name, method]): SubmittableModuleExtrinsics => { + out[name] = this.decorateExtrinsicEntry(method, decorateMethod); + + return out; + }, {} as unknown as SubmittableModuleExtrinsics); + + return out; + }, creator as unknown as SubmittableExtrinsics); + } + + private decorateExtrinsicEntry (method: CallFunction, decorateMethod: Decorate['decorateMethod']): SubmittableExtrinsicFunction { + const decorated = (...params: Arg[]): SubmittableExtrinsic => + createSubmittable(this._type, this._rx as ApiInterfaceRx, decorateMethod, method(...params)); + + return this.decorateFunctionMeta(method, decorated as any) as SubmittableExtrinsicFunction; + } + + protected decorateStorage (storage: Storage, decorateMethod: Decorate['decorateMethod']): QueryableStorage { + return Object.entries(storage).reduce((out, [name, section]): QueryableStorage => { + out[name] = Object.entries(section).reduce((out, [name, method]): QueryableModuleStorage => { + out[name] = this.decorateStorageEntry(method, decorateMethod); + + return out; + }, {} as unknown as QueryableModuleStorage); + + return out; + }, {} as unknown as QueryableStorage); + } + + private decorateStorageEntry (creator: StorageEntry, decorateMethod: Decorate['decorateMethod']): QueryableStorageEntry { + // get the storage arguments, with DoubleMap as an array entry, otherwise spread + const getArgs = (...args: any[]): any[] => creator.meta.type.isDoubleMap + ? [creator, args] + : [creator, ...args]; + + const decorated = creator.headKey + ? this.decorateStorageEntryLinked(creator, decorateMethod) + : decorateMethod((...args: any[]): Observable => + this._rpcCore.state + // Unfortunately for one-shot calls we also use .subscribeStorage here + .subscribeStorage<[Codec]>([getArgs(...args)]) + // 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 + .pipe(map(([data]): Codec => data)), { methodName: creator.method }); + + decorated.creator = creator; + + decorated.at = decorateMethod((hash: Hash, arg1?: Arg, arg2?: Arg): Observable => + this._rpcCore.state.getStorage(getArgs(arg1, arg2), hash)); + + decorated.hash = decorateMethod((arg1?: Arg, arg2?: Arg): Observable => + this._rpcCore.state.getStorageHash(getArgs(arg1, arg2))); + + decorated.key = (arg1?: Arg, arg2?: Arg): string => + u8aToHex(compactStripLength(creator(creator.meta.type.isDoubleMap ? [arg1, arg2] : arg1))[1]); + + // When using double map storage function, user need to path double map key as an array + decorated.multi = decorateMethod((args: (Arg | Arg[])[]): Observable => + this._rpcCore.state.subscribeStorage( + args.map((arg: Arg[] | Arg): [StorageEntry, Arg | Arg[]] => [creator, arg]))); + + decorated.size = decorateMethod((arg1?: Arg, arg2?: Arg): Observable => + this._rpcCore.state.getStorageSize(getArgs(arg1, arg2))); + + return this.decorateFunctionMeta(creator, decorated) as unknown as QueryableStorageEntry; + } + + private decorateStorageEntryLinked (creator: StorageEntry, decorateMethod: Decorate['decorateMethod']): ReturnType['decorateMethod']> { + const result: Map]> = new Map(); + let subject: BehaviorSubject; + let head: Codec | null = null; + + // retrieve a value based on the key, iterating if it has a next entry. Since + // entries can be re-linked in the middle of a list, we subscribe here to make + // sure we catch any updates, no matter the list position + const getNext = (key: Codec): Observable => + this._rpcCore.state.subscribeStorage<[[Codec, Linkage]]>([[creator, key]]).pipe( + switchMap(([data]: [[Codec, Linkage]]): Observable => { + result.set(key, data); + + // iterate from this key to the linkages, constructing entries for all + // those found and available + if (data[1].next.isSome) { + return getNext(data[1].next.unwrap()); + } + + const [keys, vals]: [Codec[], Codec[]] = [[], []]; + let nextKey = head; + + // loop through the results collected, starting at the head an re-creating + // the list. Our map may have old entries, based on the linking these will + // not be returned in the final result + while (nextKey) { + const entry = result.get(nextKey); + + if (!entry) { + break; + } + + const [item, linkage] = entry; + + keys.push(nextKey); + vals.push(item); + + nextKey = linkage.next && linkage.next.unwrapOr(null); + } + + const nextResult = vals.length + ? new LinkageResult([keys[0].constructor as any, keys], [vals[0].constructor as any, vals]) + : new LinkageResult([Null, []], [Null, []]); + + // we set our result into a subject so we have a single observable for + // which the value changes over time. Initially create, follow-up next + if (subject) { + subject.next(nextResult); + } else { + subject = new BehaviorSubject(nextResult); + } + + return subject; + })); + + // this handles the case where the head changes effectively, i.e. a new entry + // appears at the top of the list, the new getNext gets kicked off + return decorateMethod((...args: any[]): Observable]> => + args.length + ? this._rpcCore.state + .subscribeStorage<[[Codec, Linkage]]>([[creator, ...args]]) + .pipe(map(([data]): [Codec, Linkage] => data)) + : this._rpcCore.state + .subscribeStorage<[LinkageResult]>([creator.headKey]) + .pipe(switchMap(([key]): Observable => getNext(head = key))) + ); + } + + // FIXME Any better ideas? `any` doesn't work, needs a return but TS infers + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + protected decorateDerive (apiRx: ApiInterfaceRx, decorateMethod: Decorate['decorateMethod']) { + // Pull in derive from api-derive + const derive = decorateDerive(apiRx, this._options.derives); + + return decorateSections(derive, decorateMethod); + } + + /** + * Put the `this.onCall` function of ApiRx here, because it is needed by + * `api._rx`. + */ + protected rxDecorateMethod (method: Method): Method { + return method; + } +} diff --git a/packages/api/src/base/Events.ts b/packages/api/src/base/Events.ts new file mode 100644 index 0000000000..81dbd56721 --- /dev/null +++ b/packages/api/src/base/Events.ts @@ -0,0 +1,96 @@ +// Copyright 2017-2019 @polkadot/api 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 { ApiInterfaceEvents } from '../types'; + +import EventEmitter from 'eventemitter3'; + +export default class Events { + private _eventemitter: EventEmitter; + + protected constructor () { + this._eventemitter = new EventEmitter(); + } + + protected emit (type: ApiInterfaceEvents, ...args: any[]): void { + this._eventemitter.emit(type, ...args); + } + + /** + * @description Attach an eventemitter handler to listen to a specific event + * + * @param type The type of event to listen to. Available events are `connected`, `disconnected`, `ready` and `error` + * @param handler The callback to be called when the event fires. Depending on the event type, it could fire with additional arguments. + * + * @example + *
+ * + * ```javascript + * api.on('connected', (): void => { + * console.log('API has been connected to the endpoint'); + * }); + * + * api.on('disconnected', (): void => { + * console.log('API has been disconnected from the endpoint'); + * }); + * ``` + */ + public on (type: ApiInterfaceEvents, handler: (...args: any[]) => any): this { + this._eventemitter.on(type, handler); + + return this; + } + + /** + * @description Remove the given eventemitter handler + * + * @param type The type of event the callback was attached to. Available events are `connected`, `disconnected`, `ready` and `error` + * @param handler The callback to unregister. + * + * @example + *
+ * + * ```javascript + * const handler = (): void => { + * console.log('Connected !); + * }; + * + * // Start listening + * api.on('connected', handler); + * + * // Stop listening + * api.off('connected', handler); + * ``` + */ + public off (type: ApiInterfaceEvents, handler: (...args: any[]) => any): this { + this._eventemitter.removeListener(type, handler); + + return this; + } + + /** + * @description Attach an one-time eventemitter handler to listen to a specific event + * + * @param type The type of event to listen to. Available events are `connected`, `disconnected`, `ready` and `error` + * @param handler The callback to be called when the event fires. Depending on the event type, it could fire with additional arguments. + * + * @example + *
+ * + * ```javascript + * api.once('connected', (): void => { + * console.log('API has been connected to the endpoint'); + * }); + * + * api.once('disconnected', (): void => { + * console.log('API has been disconnected from the endpoint'); + * }); + * ``` + */ + public once (type: ApiInterfaceEvents, handler: (...args: any[]) => any): this { + this._eventemitter.once(type, handler); + + return this; + } +} diff --git a/packages/api/src/base/Init.ts b/packages/api/src/base/Init.ts new file mode 100644 index 0000000000..c70f12d15d --- /dev/null +++ b/packages/api/src/base/Init.ts @@ -0,0 +1,190 @@ +// Copyright 2017-2019 @polkadot/api 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 { RuntimeVersion, SignedBlock } from '@polkadot/types/interfaces'; +import { RegistryTypes } from '@polkadot/types/types'; +import { ApiInterfaceRx, ApiOptions, ApiTypes } from '../types'; + +import constantsFromMeta from '@polkadot/api-metadata/consts/fromMetadata'; +import extrinsicsFromMeta from '@polkadot/api-metadata/extrinsics/fromMetadata'; +import storageFromMeta from '@polkadot/api-metadata/storage/fromMetadata'; +import { GenericCall, GenericEvent, Metadata } from '@polkadot/types'; +import { LATEST_VERSION as EXTRINSIC_LATEST_VERSION } from '@polkadot/types/primitive/Extrinsic/constants'; +import { assert, logger } from '@polkadot/util'; +import { cryptoWaitReady } from '@polkadot/util-crypto'; + +import Decorate from './Decorate'; + +const KEEPALIVE_INTERVAL = 15000; + +// these are override types for polkadot chains +// NOTE The SessionKeys definition for Polkadot and Substrate (OpaqueKeys +// implementation) are different. Detect Polkadot and inject the `Keys` +// definition as applicable. (3 keys in substrate vs 4 in Polkadot). +const TYPES_FOR_POLKADOT = { + Keys: 'SessionKeysPolkadot' +}; + +// these are the types that we are using when we detect a extrinsic V1 chain +// NOTE for older chains (e.g. Alex) the BlockNumber/Index is u64, for current +// these are now set to u32 +const TYPES_FOR_V1 = { + BlockNumber: 'u64', + Index: 'u64', + EventRecord: 'EventRecord0to76' +}; + +const l = logger('api/decorator'); + +export default abstract class Init extends Decorate { + private _healthTimer: NodeJS.Timeout | null = null; + + public constructor (options: ApiOptions, type: ApiTypes) { + super(options, type); + + assert(this._rpcCore.provider.hasSubscriptions, 'Api can only be used with a provider supporting subscriptions'); + + // We only register the types (global) if this is not a cloned instance. + // Do right up-front, so we get in the user types before we are actually + // doing anything on-chain, this ensures we have the overrides in-place + if (!options.source && options.types) { + this.registerTypes(options.types); + } + + this._rpc = this.decorateRpc(this._rpcCore, this.decorateMethod); + this._rx.rpc = this.decorateRpc(this._rpcCore, this.rxDecorateMethod); + this._queryMulti = this.decorateMulti(this.decorateMethod); + this._rx.queryMulti = this.decorateMulti(this.rxDecorateMethod); + this._rx.signer = options.signer; + + this._rpcCore.provider.on('disconnected', this._onProviderDisconnect); + this._rpcCore.provider.on('error', this._onProviderError); + this._rpcCore.provider.on('connected', this._onProviderConnect); + } + + public abstract registerTypes (types?: RegistryTypes): void; + + protected async loadMeta (): Promise { + const { metadata = {} } = this._options; + + // only load from on-chain if we are not a clone (default path), alternatively + // just use the values from the source instance provided + if (!this._options.source || !this._options.source._isReady) { + this._runtimeMetadata = await this.metaFromChain(metadata); + } else { + this._extrinsicType = this._options.source.extrinsicVersion; + this._runtimeMetadata = this._options.source.runtimeMetadata; + this._runtimeVersion = this._options.source.runtimeVersion; + this._genesisHash = this._options.source.genesisHash; + } + + return this.initFromMeta(this._runtimeMetadata); + } + + private async metaFromChain (optMetadata: Record): Promise { + [this._genesisHash, this._runtimeVersion] = await Promise.all([ + this._rpcCore.chain.getBlockHash(0).toPromise(), + this._rpcCore.chain.getRuntimeVersion().toPromise() + ]); + + if (this._runtimeVersion.specName.eq('polkadot')) { + this.registerTypes(TYPES_FOR_POLKADOT); + } + + const metadataKey = `${this._genesisHash}-${(this._runtimeVersion as RuntimeVersion).specVersion}`; + const metadata = metadataKey in optMetadata + ? new Metadata(optMetadata[metadataKey]) + : await this._rpcCore.state.getMetadata().toPromise(); + + // get unique types & validate + metadata.getUniqTypes(false); + + return metadata; + } + + private async initFromMeta (metadata: Metadata): Promise { + const extrinsics = extrinsicsFromMeta(metadata); + const storage = storageFromMeta(metadata); + const constants = constantsFromMeta(metadata); + + // only inject if we are not a clone (global init) + if (!this._options.source) { + GenericEvent.injectMetadata(metadata); + GenericCall.injectMethods(extrinsics); + + // detect the extrinsic version in-use based on the last block + const { block: { extrinsics: [firstTx] } }: SignedBlock = await this._rpcCore.chain.getBlock().toPromise(); + + // If we haven't sync-ed to 1 yes, this won't have any values + this._extrinsicType = firstTx ? firstTx.type : EXTRINSIC_LATEST_VERSION; + + // HACK Assume that old versions, substrate 1.x is u64 BlockNumber/Nonce + // and has the old EventRecord format. Remove this ASAP with support for + // Alex dropped + if (this._extrinsicType === 1) { + this.registerTypes(TYPES_FOR_V1); + } + } + + this._extrinsics = this.decorateExtrinsics(extrinsics, this.decorateMethod); + this._query = this.decorateStorage(storage, this.decorateMethod); + this._consts = constants; + + this._rx.extrinsicType = this._extrinsicType; + this._rx.genesisHash = this._genesisHash; + this._rx.runtimeVersion = this._runtimeVersion; + this._rx.tx = this.decorateExtrinsics(extrinsics, this.rxDecorateMethod); + this._rx.query = this.decorateStorage(storage, this.rxDecorateMethod); + this._rx.consts = constants; + + // derive is last, since it uses the decorated rx + this._derive = this.decorateDerive(this._rx as ApiInterfaceRx, this.decorateMethod); + + return true; + } + + private _onProviderConnect = async (): Promise => { + this.emit('connected'); + this._isConnected.next(true); + + try { + const [hasMeta, cryptoReady] = await Promise.all([ + this.loadMeta(), + cryptoWaitReady() + ]); + + if (hasMeta && !this._isReady && cryptoReady) { + this._isReady = true; + + this.emit('ready', this); + } + + this._healthTimer = setInterval((): void => { + this._rpcCore.system.health().toPromise().catch((): void => { + // ignore + }); + }, KEEPALIVE_INTERVAL); + } catch (_error) { + const error = new Error(`FATAL: Unable to initialize the API: ${_error.message}`); + + l.error(error); + + this.emit('error', error); + } + } + + private _onProviderDisconnect = (): void => { + this.emit('disconnected'); + this._isConnected.next(false); + + if (this._healthTimer) { + clearInterval(this._healthTimer); + this._healthTimer = null; + } + }; + + private _onProviderError = (error: Error): void => { + this.emit('error', error); + }; +} diff --git a/packages/api/src/base/index.ts b/packages/api/src/base/index.ts new file mode 100644 index 0000000000..a1a2071aa0 --- /dev/null +++ b/packages/api/src/base/index.ts @@ -0,0 +1,269 @@ +// Copyright 2017-2019 @polkadot/api 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 { RpcInterface } from '@polkadot/rpc-core/jsonrpc.types'; +import { Hash, RuntimeVersion } from '@polkadot/types/interfaces'; +import { CallFunction, RegistryTypes } from '@polkadot/types/types'; + +import { ApiOptions, ApiTypes, DecoratedRpc, QueryableStorage, QueryableStorageMulti, SignerPayloadRawBase, SubmittableExtrinsics, Signer } from '../types'; + +import { Constants } from '@polkadot/api-metadata/consts/types'; +import { GenericCall, Metadata, getTypeRegistry } from '@polkadot/types'; +import { assert, isString, isUndefined, u8aToHex, u8aToU8a } from '@polkadot/util'; + +import Init from './Init'; + +interface KeyringSigner { + sign (message: Uint8Array): Uint8Array; +} + +let pkgJson: { name: string; version: string }; + +try { + pkgJson = require('../package.json'); +} catch (error) { + // development environment + pkgJson = { name: '@polkadot/api', version: '-' }; +} + +function assertResult (value: T | undefined): T { + assert(!isUndefined(value), `Api needs to be initialised before using, listen on 'ready'`); + + return value as T; +} + +export default abstract class ApiBase extends Init { + /** + * @description Create an instance of the class + * + * @param options Options object to create API instance or a Provider instance + * + * @example + *
+ * + * ```javascript + * import Api from '@polkadot/api/promise'; + * + * const api = new Api().isReady(); + * + * api.rpc.subscribeNewHead((header) => { + * console.log(`new block #${header.number.toNumber()}`); + * }); + * ``` + */ + public constructor (options: ApiOptions = {}, type: ApiTypes) { + super(options, type); + } + + /** + * @description Contains the parameter types (constants) of all modules. + * + * The values are instances of the appropriate type and are accessible using `section`.`constantName`, + * + * @example + *
+ * + * ```javascript + * console.log(api.consts.democracy.enactmentPeriod.toString()) + * ``` + */ + public get consts (): Constants { + return assertResult(this._consts); + } + + /** + * @description Derived results that are injected into the API, allowing for combinations of various query results. + * + * @example + *
+ * + * ```javascript + * api.derive.chain.bestNumber((number) => { + * console.log('best number', number); + * }); + * ``` + */ + public get derive (): ReturnType['decorateDerive']> { + return assertResult(this._derive); + } + + /** + * @description Returns th version of extrinsics in-use on this chain + */ + public get extrinsicVersion (): number { + return this._extrinsicType; + } + + /** + * @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. + */ + public get genesisHash (): Hash { + return assertResult(this._genesisHash); + } + + /** + * @description `true` when subscriptions are supported + */ + public get hasSubscriptions (): boolean { + return this._rpcCore.provider.hasSubscriptions; + } + + /** + * @description The library information name & version (from package.json) + */ + public get libraryInfo (): string { + return `${pkgJson.name} v${pkgJson.version}`; + } + + /** + * @description Contains all the chain state modules and their subsequent methods in the API. These are attached dynamically from the runtime metadata. + * + * All calls inside the namespace, is denoted by `section`.`method` and may take an optional query parameter. As an example, `api.query.timestamp.now()` (current block timestamp) does not take parameters, while `api.query.system.accountNonce()` (retrieving the associated nonce for an account), takes the `AccountId` as a parameter. + * + * @example + *
+ * + * ```javascript + * api.query.balances.freeBalance(, (balance) => { + * console.log('new balance', balance); + * }); + * ``` + */ + public get query (): QueryableStorage { + return assertResult(this._query); + } + + /** + * @description Allows for the querying of multiple storage entries and the combination thereof into a single result. This is a very optimal way to make multiple queries since it only makes a single connection to the node and retrieves the data over one subscription. + * + * @example + *
+ * + * ```javascript + * const unsub = await api.queryMulti( + * [ + * // you can include the storage without any parameters + * api.query.balances.totalIssuance, + * // or you can pass parameters to the storage query + * [api.query.balances.freeBalance, '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY'] + * ], + * ([existential, balance]) => { + * console.log(`You have ${balance.sub(existential)} more than the existential deposit`); + * + * unsub(); + * } + * ); + * ``` + */ + public get queryMulti (): QueryableStorageMulti { + return assertResult(this._queryMulti); + } + + /** + * @description Contains all the raw rpc sections and their subsequent methods in the API as defined by the jsonrpc interface definitions. Unlike the dynamic `api.query` and `api.tx` sections, these methods are fixed (although extensible with node upgrades) and not determined by the runtime. + * + * RPC endpoints available here allow for the query of chain, node and system information, in addition to providing interfaces for the raw queries of state (usine known keys) and the submission of transactions. + * + * @example + *
+ * + * ```javascript + * api.rpc.chain.subscribeNewHead((header) => { + * console.log('new header', header); + * }); + * ``` + */ + public get rpc (): DecoratedRpc { + return assertResult(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. + */ + public get runtimeMetadata (): Metadata { + return assertResult(this._runtimeMetadata); + } + + /** + * @description Contains the version information for the current runtime. + */ + public get runtimeVersion (): RuntimeVersion { + return assertResult(this._runtimeVersion); + } + + /** + * @description The type of this API instance, either 'rxjs' or 'promise' + */ + public get type (): ApiTypes { + return this._type; + } + + /** + * @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(, ) + * .signAndSend(, ({status}) => { + * console.log('tx status', status.asFinalized.toHex()); + * }); + * ``` + */ + public get tx (): SubmittableExtrinsics { + return assertResult(this._extrinsics); + } + + /** + * @description Disconnect from the underlying provider, halting all comms + */ + public disconnect (): void { + this._rpcCore.disconnect(); + } + + /** + * @description Finds the definition for a specific [[Call]] based on the index supplied + */ + public findCall (callIndex: Uint8Array | string): CallFunction { + return GenericCall.findFunction(u8aToU8a(callIndex)); + } + + /** + * @description Register additional user-defined of chain-specific types in the type registry + */ + public registerTypes (types?: RegistryTypes): void { + types && getTypeRegistry().register(types); + } + + /** + * @description Set an external signer which will be used to sign extrinsic when account passed in is not KeyringPair + */ + public setSigner (signer: Signer): void { + this._rx.signer = signer; + } + + /** + * @description Signs a raw signer payload, string or Uint8Array + */ + public async sign (signer: KeyringSigner | string, data: SignerPayloadRawBase): Promise { + // NOTE Do we really want to do this? Or turn it into an observable for rxjs? + if (isString(signer)) { + if (!this._rx.signer || !this._rx.signer.signRaw) { + throw new Error('No signer exists with a signRaw interface'); + } + + return ( + await this._rx.signer.signRaw({ + type: 'bytes', + ...data, + address: signer + }) + ).signature; + } + + return u8aToHex(signer.sign(u8aToU8a(data.data))); + } +} diff --git a/packages/api/src/promise/Api.ts b/packages/api/src/promise/Api.ts index d1518ee757..4b16e389b6 100644 --- a/packages/api/src/promise/Api.ts +++ b/packages/api/src/promise/Api.ts @@ -9,7 +9,7 @@ import { Observable, EMPTY } from 'rxjs'; import { catchError, first, tap } from 'rxjs/operators'; import { isFunction, assert } from '@polkadot/util'; -import ApiBase from '../Base'; +import ApiBase from '../base'; import Combinator, { CombinatorCallback, CombinatorFunction } from './Combinator'; /** diff --git a/packages/api/src/rx/Api.ts b/packages/api/src/rx/Api.ts index c9ee5c39be..253ae6c537 100644 --- a/packages/api/src/rx/Api.ts +++ b/packages/api/src/rx/Api.ts @@ -7,7 +7,7 @@ import { ApiOptions } from '../types'; import { from, Observable } from 'rxjs'; -import ApiBase from '../Base'; +import ApiBase from '../base'; /** * # @polkadot/api/rx diff --git a/packages/api/src/types.ts b/packages/api/src/types.ts index 1ba417671a..24ad36fcda 100644 --- a/packages/api/src/types.ts +++ b/packages/api/src/types.ts @@ -14,7 +14,7 @@ import { ProviderInterface, ProviderInterfaceEmitted } from '@polkadot/rpc-provi import { Metadata, u64 } from '@polkadot/types'; import { StorageEntry } from '@polkadot/types/primitive/StorageKey'; -import ApiBase from './Base'; +import ApiBase from './base'; import { ISubmittableResult, SubmittableExtrinsic } from './SubmittableExtrinsic'; // Prepend an element V onto the beginning of a tuple T. @@ -80,15 +80,18 @@ export type DecoratedRpc = { [Section in keyof AllSections]: DecoratedRpcSection } -export interface StorageEntryObservable { +interface StorageEntryBase { + at: (hash: Hash | Uint8Array | string, arg1?: CodecArg, arg2?: CodecArg) => C; + creator: StorageEntry; + hash: (arg1?: CodecArg, arg2?: CodecArg) => H; + key: (arg1?: CodecArg, arg2?: CodecArg) => string; + size: (arg1?: CodecArg, arg2?: CodecArg) => U; +} + +export interface StorageEntryObservable extends StorageEntryBase, Observable, Observable> { (arg1?: CodecArg, arg2?: CodecArg): Observable; (arg1?: CodecArg, arg2?: CodecArg): Observable; - at: (hash: Hash | Uint8Array | string, arg1?: CodecArg, arg2?: CodecArg) => Observable; - creator: StorageEntry; - hash: (arg1?: CodecArg, arg2?: CodecArg) => Observable; - key: (arg1?: CodecArg, arg2?: CodecArg) => string; multi: (args: (CodecArg[] | CodecArg)[]) => Observable; - size: (arg1?: CodecArg, arg2?: CodecArg) => Observable; } export interface StorageEntryPromiseOverloads { @@ -104,13 +107,8 @@ export interface StorageEntryPromiseMulti { (args: (CodecArg[] | CodecArg)[], callback: Callback): UnsubscribePromise; } -export interface StorageEntryPromise extends StorageEntryPromiseOverloads { - at: (hash: Hash | Uint8Array | string, arg1?: CodecArg, arg2?: CodecArg) => Promise; - creator: StorageEntry; - hash: (arg1?: CodecArg, arg2?: CodecArg) => Promise; - key: (arg1?: CodecArg, arg2?: CodecArg) => string; +export interface StorageEntryPromise extends StorageEntryBase, Promise, Promise>, StorageEntryPromiseOverloads { multi: StorageEntryPromiseMulti; - size: (arg1?: CodecArg, arg2?: CodecArg) => Promise; } export type QueryableStorageEntry = diff --git a/packages/rpc-core/src/index.ts b/packages/rpc-core/src/index.ts index 594d7444f0..20fbcdba76 100644 --- a/packages/rpc-core/src/index.ts +++ b/packages/rpc-core/src/index.ts @@ -269,7 +269,7 @@ export default class Rpc implements RpcInterface { } private formatOutput (method: RpcMethod, params: Codec[], result?: any): Codec | (Codec | null | undefined)[] { - const base = createTypeUnsafe(method.type as string, [result]); + const base = createTypeUnsafe(method.type, [result]); if (method.type === 'StorageData') { const key = params[0] as StorageKey; diff --git a/packages/types/src/types.ts b/packages/types/src/types.ts index 3ebae9a4d3..d47e9521f3 100644 --- a/packages/types/src/types.ts +++ b/packages/types/src/types.ts @@ -196,22 +196,22 @@ export interface IExtrinsicEra extends Codec { } // eslint-disable-next-line @typescript-eslint/interface-name-prefix -export interface IExtrinsicImpl extends Codec { - readonly method: Call; - readonly signature: IExtrinsicSignature; - readonly version: number; - - addSignature (signer: Address | Uint8Array | string, signature: Uint8Array | string, payload: ExtrinsicPayloadValue | Uint8Array | string): IExtrinsicImpl; - sign (account: IKeyringPair, options: SignatureOptions): IExtrinsicImpl; +interface IExtrinsicSignable { + addSignature (signer: Address | Uint8Array | string, signature: Uint8Array | string, payload: ExtrinsicPayloadValue | Uint8Array | string): T; + sign (account: IKeyringPair, options: SignatureOptions): T; } // eslint-disable-next-line @typescript-eslint/interface-name-prefix -export interface IExtrinsic extends ExtrinsicSignatureBase, IMethod { +export interface IExtrinsicImpl extends IExtrinsicSignable, Codec { + readonly method: Call; + readonly signature: IExtrinsicSignature; + readonly version: number; +} + +// eslint-disable-next-line @typescript-eslint/interface-name-prefix +export interface IExtrinsic extends IExtrinsicSignable, ExtrinsicSignatureBase, IMethod { readonly length: number; readonly method: Call; readonly type: number; readonly version: number; - - addSignature (signer: Address | Uint8Array | string, signature: Uint8Array | string, payload: ExtrinsicPayloadValue | Uint8Array | string): IExtrinsic; - sign (account: IKeyringPair, options: SignatureOptions): IExtrinsic; }