diff --git a/CHANGELOG.md b/CHANGELOG.md index 55405f2309..d238a67678 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # 1.4.0-beta.x +- Add support for `api.rpc.chain.subscribeAllHeads` (not just best) (Thanks to https://github.com/jak-pan) +- Add support for `api.rpc.engine.*` for manual seal support - Expand `.toHuman` on ExtrinsicEra, SubmittableResult & Vote - Remove `GenericDigestItem`, the `DigestItem` is now a type interface via `@polkadot/types/interfaces` - Move `H160`, `H256` & `H512` to interfaces in `@polkadot/types/interfaces` @@ -7,6 +9,7 @@ - Expose static with on `UInt`, `Int` & `U8aFixed` classes with optional type name override - Support for arbitrary UInt types via `UInt` definitions - Add `api.injectMetadata(metadata)` to initialize with a specific version +- The api now checks for runtime upgrades, decorating with new metadata # 1.3.1 Feb 18, 2020 @@ -24,7 +27,7 @@ - Add `LookupSource` and `LookupTarget` types instead of mapping these directly for aliassing - Add `BitVec` type primitive - Add support for `system.account` for balance & nonce queries in derives as well as Submittables (Substrate composites) -- Add `rpc.author.hasKey` and `rpc.author.hasSessinKeys` RPCs +- Add `rpc.author.hasKey` and `rpc.author.hasSessionKeys` RPCs - Add derives for the parachains interfaces - Applied the latest types for Polkadot parachains - `registry.findMetaError` now can take in a `DispatchErrorModule` object for error lookups (from `asModule` in `DispatchError`) diff --git a/packages/api/src/base/Decorate.ts b/packages/api/src/base/Decorate.ts index 6e66d7bd8f..b2eb54c42e 100644 --- a/packages/api/src/base/Decorate.ts +++ b/packages/api/src/base/Decorate.ts @@ -18,7 +18,7 @@ import DecoratedMeta from '@polkadot/metadata/Decorated'; import getHasher from '@polkadot/metadata/Decorated/storage/fromMetadata/getHasher'; import RpcCore from '@polkadot/rpc-core'; import { WsProvider } from '@polkadot/rpc-provider'; -import { Metadata, Null, Option, TypeRegistry, u64, Vec } from '@polkadot/types'; +import { Metadata, Null, Option, Text, TypeRegistry, u64, Vec } 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 StorageKey, { StorageEntry } from '@polkadot/types/primitive/StorageKey'; @@ -69,6 +69,8 @@ export default abstract class Decorate extends Events protected _rpcCore: RpcCore; + protected _runtimeChain?: Text; + protected _runtimeMetadata?: Metadata; protected _runtimeVersion?: RuntimeVersion; @@ -146,18 +148,18 @@ export default abstract class Decorate extends Events return this._rpcCore.provider.hasSubscriptions; } - public injectMetadata (metadata: Metadata): void { + public injectMetadata (metadata: Metadata, fromEmpty?: boolean): void { const decoratedMeta = new DecoratedMeta(this.registry, metadata); // this API - augmentObject('tx', this.decorateExtrinsics(decoratedMeta.tx, this.decorateMethod), this._extrinsics); - augmentObject('query', this.decorateStorage(decoratedMeta.query, this.decorateMethod), this._query); - augmentObject('consts', decoratedMeta.consts, this._consts); + augmentObject('tx', this.decorateExtrinsics(decoratedMeta.tx, this.decorateMethod), this._extrinsics, fromEmpty); + augmentObject('query', this.decorateStorage(decoratedMeta.query, this.decorateMethod), this._query, fromEmpty); + augmentObject('consts', decoratedMeta.consts, this._consts, fromEmpty); // rx - augmentObject('', this.decorateExtrinsics(decoratedMeta.tx, this.rxDecorateMethod), this._rx.tx); - augmentObject('', this.decorateStorage(decoratedMeta.query, this.rxDecorateMethod), this._rx.query); - augmentObject('', decoratedMeta.consts, this._rx.consts); + augmentObject('', this.decorateExtrinsics(decoratedMeta.tx, this.rxDecorateMethod), this._rx.tx, fromEmpty); + augmentObject('', this.decorateStorage(decoratedMeta.query, this.rxDecorateMethod), this._rx.query, fromEmpty); + augmentObject('', decoratedMeta.consts, this._rx.consts, fromEmpty); } private decorateFunctionMeta (input: MetaDecoration, output: MetaDecoration): MetaDecoration { diff --git a/packages/api/src/base/Init.ts b/packages/api/src/base/Init.ts index b1b5d1e699..651fba0fbc 100644 --- a/packages/api/src/base/Init.ts +++ b/packages/api/src/base/Init.ts @@ -2,11 +2,13 @@ // This software may be modified and distributed under the terms // of the Apache-2.0 license. See the LICENSE file for details. -import { SignedBlock } from '@polkadot/types/interfaces'; +import { SignedBlock, RuntimeVersion } from '@polkadot/types/interfaces'; import { ApiBase, ApiOptions, ApiTypes, DecorateMethod } from '../types'; -import { Metadata } from '@polkadot/types'; -import { getChainTypes, getMetadataTypes } from '@polkadot/types/known'; +import { Subscription, combineLatest, of } from 'rxjs'; +import { map, switchMap } from 'rxjs/operators'; +import { Metadata, Text } from '@polkadot/types'; +import { getMetadataTypes, getSpecTypes, getUserTypes } from '@polkadot/types/known'; import { LATEST_EXTRINSIC_VERSION } from '@polkadot/types/primitive/Extrinsic/Extrinsic'; import { logger } from '@polkadot/util'; import { cryptoWaitReady } from '@polkadot/util-crypto'; @@ -15,16 +17,18 @@ import Decorate from './Decorate'; const KEEPALIVE_INTERVAL = 15000; -const l = logger('api/decorator'); +const l = logger('api/init'); export default abstract class Init extends Decorate { private _healthTimer: NodeJS.Timeout | null = null; + private _updateSub?: Subscription; + constructor (options: ApiOptions, type: ApiTypes, decorateMethod: DecorateMethod) { super(options, type, decorateMethod); if (!this.hasSubscriptions) { - console.warn('Api will be available in a limited mode since the provider does not support subscriptions'); + l.warn('Api will be available in a limited mode since the provider does not support subscriptions'); } // We only register the types (global) if this is not a cloned instance. @@ -53,15 +57,30 @@ export default abstract class Init extends Decorate { + const genesisHash = await this._rpcCore.chain.getBlockHash(0).toPromise(); + + // on re-connection to the same chain, we don't want to re-do everything from chain again + if (this._isReady && !this._options.source && genesisHash.eq(this._genesisHash)) { + return true; + } + + if (this._genesisHash) { + l.warn('Connection to new genesis detected, re-initializing'); + } + + this._genesisHash = genesisHash; + + if (this._updateSub) { + this._updateSub.unsubscribe(); + } + 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._runtimeMetadata = await this.metaFromSource(this._options.source); - } + this._runtimeMetadata = this._options.source?._isReady + ? await this.metaFromSource(this._options.source) + : await this.metaFromChain(metadata); return this.initFromMeta(this._runtimeMetadata); } @@ -88,32 +107,64 @@ export default abstract class Init extends Decorate + combineLatest( + of(version), + this._rpcCore.state.getMetadata() + ) + ), + map(([version, metadata]: [RuntimeVersion, Metadata]): boolean => { + if (this._runtimeVersion?.specVersion.eq(version.specVersion)) { + return false; + } + + l.log(`Runtime version updated to ${version.specVersion}`); + + this._runtimeMetadata = metadata; + this._runtimeVersion = version; + + this.registerTypes(getSpecTypes(this._runtimeChain as Text, version)); + this.injectMetadata(metadata, false); + + return true; + }) + ).subscribe(); + } + private async metaFromChain (optMetadata: Record): Promise { const { typesChain, typesSpec } = this._options; - const [genesisHash, runtimeVersion, chain, chainProps] = await Promise.all([ - this._rpcCore.chain.getBlockHash(0).toPromise(), + const [runtimeVersion, chain, chainProps] = await Promise.all([ this._rpcCore.state.getRuntimeVersion().toPromise(), this._rpcCore.system.chain().toPromise(), this._rpcCore.system.properties().toPromise() ]); - // based on the node spec & chain, inject specific type overrides - this.registerTypes(getChainTypes(chain, runtimeVersion, typesChain, typesSpec)); + // set our chain version & genesisHash as returned + this._runtimeChain = chain; + this._runtimeVersion = runtimeVersion; + + // do the setup for the specific chain this.registry.setChainProperties(chainProps); + this.registerTypes(getSpecTypes(chain, runtimeVersion)); + this.registerTypes(getUserTypes(chain, runtimeVersion, typesChain, typesSpec)); + this.subscribeUpdates(); // filter the RPC methods (this does an rpc-methods call) await this.filterRpc(); // retrieve metadata, either from chain or as pass-in via options - const metadataKey = `${genesisHash}-${runtimeVersion.specVersion}`; + const metadataKey = `${this._genesisHash}-${runtimeVersion.specVersion}`; const metadata = metadataKey in optMetadata ? new Metadata(this.registry, optMetadata[metadataKey]) : await this._rpcCore.state.getMetadata().toPromise(); - // set our chain version & genesisHash as returned - this._genesisHash = genesisHash; - this._runtimeVersion = runtimeVersion; - // get unique types & validate metadata.getUniqTypes(false); @@ -141,7 +192,7 @@ export default abstract class Init extends Decorate { +describe('augmentObject', (): void => { let spy: any; beforeEach((): void => { @@ -15,20 +15,25 @@ describe('findRemoved', (): void => { spy.mockClear(); }); - it('logs removed sections', (): void => { - findRemoved('test', { foo: {}, bar: {} }, { cde: {}, foo: {}, bar: {}, baz: {} }); + it('logs added/removed sections and methods', (): void => { + augmentObject( + 'test', + { foo: { f: 1, d: 1 }, bar: { b: 1 }, new: { z: 1 } }, + { foo: { f: 1, c: 1 }, bar: { a: 1, c: 1 }, baz: { a: 1 } } + ); - expect(spy).toHaveBeenCalledWith('api.test: Found 2 removed modules: baz, cde'); + expect(spy).toHaveBeenCalledWith( + expect.anything(), + expect.stringContaining('API/AUGMENT'), + 'api.test: Found 1 added and 1 removed modules:\n\t added: new\n\tremoved: baz' + ); + expect(spy).toHaveBeenCalledWith( + expect.anything(), + expect.stringContaining('API/AUGMENT'), + 'api.test: Found 2 added and 3 removed calls:\n\t added: bar.b, foo.d\n\tremoved: bar.a, bar.c, foo.c' + ); }); - it('logs removed calls', (): void => { - findRemoved('test', { foo: { a: 1 }, bar: {} }, { foo: { a: 1 }, bar: { a: 1 } }); - - expect(spy).toHaveBeenCalledWith('api.test: Found 1 removed calls: bar.a'); - }); -}); - -describe('augmentObject', (): void => { it('copies sections to the dest', (): void => { const src = { foo: { a: 1 }, bar: { b: 1 } }; @@ -39,6 +44,10 @@ describe('augmentObject', (): void => { const src = { foo: { a: 1 }, bar: { b: 1 } }; const dst = { foo: { b: 2 }, baz: { c: 1 } }; - expect(augmentObject('test', src, dst)).toEqual({ foo: { b: 2, a: 1 }, bar: { b: 1 }, baz: { c: 1 } }); + expect(augmentObject('test', src, dst)).toEqual({ + foo: { b: 2, a: 1 }, + bar: { b: 1 }, + baz: { c: 1 } + }); }); }); diff --git a/packages/api/src/util/augmentObject.ts b/packages/api/src/util/augmentObject.ts index e5686c208b..0957ba9999 100644 --- a/packages/api/src/util/augmentObject.ts +++ b/packages/api/src/util/augmentObject.ts @@ -2,24 +2,53 @@ // This software may be modified and distributed under the terms // of the Apache-2.0 license. See the LICENSE file for details. +import { logger } from '@polkadot/util'; + +type StringsStrings = [string[], string[]]; + +const l = logger('api/augment'); + +function logLength (type: 'added' | 'removed', values: string[], and: string[] = []): string { + return values.length + ? ` ${values.length} ${type}${and.length ? ' and' : ''}` + : ''; +} + +function logValues (type: 'added' | 'removed', values: string[]): string { + return values.length + ? `\n\t${type.padStart(7)}: ${values.sort().join(', ')}` + : ''; +} + // log details to console -function warn (prefix: string, type: 'calls' | 'modules', rmed: string[]): void { - if (rmed.length) { - console.warn(`api.${prefix}: Found ${rmed.length} removed ${type}: ${rmed.join(', ')}`); +function warn (prefix: string, type: 'calls' | 'modules', [added, removed]: StringsStrings): void { + if (added.length || removed.length) { + l.warn(`api.${prefix}: Found${logLength('added', added, removed)}${logLength('removed', removed)} ${type}:${logValues('added', added)}${logValues('removed', removed)}`); } } -// log all the stuff that has been removed/ -export function findRemoved (prefix: string, src: Record>, dst: Record>): void { +function extractKeys (src: Record>, dst: Record>): StringsStrings { + return [Object.keys(src), Object.keys(dst)]; +} + +function findSectionExcludes (a: string[], b: string[]): string[] { + return a.filter((section): boolean => !b.includes(section)); +} + +function extractSections (src: Record>, dst: Record>): StringsStrings { + const [srcSections, dstSections] = extractKeys(src, dst); + + return [ + findSectionExcludes(srcSections, dstSections), + findSectionExcludes(dstSections, srcSections) + ]; +} + +function findMethodExcludes (src: Record>, dst: Record>): string[] { const srcSections = Object.keys(src); const dstSections = Object.keys(dst); - warn(prefix, 'modules', dstSections - .filter((section): boolean => !srcSections.includes(section)) - .sort() - ); - - warn(prefix, 'calls', dstSections + return dstSections .filter((section): boolean => srcSections.includes(section)) .reduce((rmMethods: string[], section): string[] => { const srcMethods = Object.keys(src[section]); @@ -30,9 +59,14 @@ export function findRemoved (prefix: string, src: Record !srcMethods.includes(method)) .map((method): string => `${section}.${method}`) ); - }, []) - .sort() - ); + }, []); +} + +function extractMethods (src: Record>, dst: Record>): StringsStrings { + return [ + findMethodExcludes(dst, src), + findMethodExcludes(src, dst) + ]; } /** @@ -40,9 +74,16 @@ export function findRemoved (prefix: string, src: Record>, dst: Record>): Record> { +export default function augmentObject (prefix: string, src: Record>, dst: Record>, fromEmpty = false): Record> { + if (fromEmpty) { + Object.keys(dst).forEach((key): void => { + delete dst[key]; + }); + } + if (prefix && Object.keys(dst).length) { - findRemoved(prefix, src, dst); + warn(prefix, 'modules', extractSections(src, dst)); + warn(prefix, 'calls', extractMethods(src, dst)); } return Object diff --git a/packages/types/src/codec/index.ts b/packages/types/src/codec/index.ts index d95536f2b9..d3fe62ecf5 100644 --- a/packages/types/src/codec/index.ts +++ b/packages/types/src/codec/index.ts @@ -19,12 +19,12 @@ export { default as Set } from './Set'; export { default as Struct } from './Struct'; // export { default as StructAny } from './StructAny'; export { default as Tuple } from './Tuple'; -export { default as U8aFixed } from './U8aFixed'; export { default as Vec } from './Vec'; // Convenience base classes, used as "anything of this type" bases export { default as Raw } from './Raw'; export { default as UInt } from './UInt'; +export { default as U8aFixed } from './U8aFixed'; // Type management helper functions export * from './utils/encodeTypes'; diff --git a/packages/types/src/known/index.ts b/packages/types/src/known/index.ts index 34909e627a..f2ff59c32b 100644 --- a/packages/types/src/known/index.ts +++ b/packages/types/src/known/index.ts @@ -33,14 +33,24 @@ export function getMetadataTypes (version: number): RegistryTypes { // based on the chain and runtimeVersion, get the applicable types (ready for registration) /** @internal */ -export function getChainTypes (chainName: Text, { specName, specVersion }: RuntimeVersion, typesChain: Record = {}, typesSpec: Record = {}): RegistryTypes { +export function getSpecTypes (chainName: Text | string, { specName, specVersion }: RuntimeVersion): RegistryTypes { const _chainName = chainName.toString(); const _specName = specName.toString(); const _specVersion = specVersion.toNumber(); return { ...filterVersions(TYPES_SPEC[_specName], _specVersion), - ...filterVersions(TYPES_CHAIN[_chainName], _specVersion), + ...filterVersions(TYPES_CHAIN[_chainName], _specVersion) + }; +} + +// based on the spec and chain, get the specific user-defind types +/** @internal */ +export function getUserTypes (chainName: Text | string, { specName }: RuntimeVersion, typesChain: Record = {}, typesSpec: Record = {}): RegistryTypes { + const _chainName = chainName.toString(); + const _specName = specName.toString(); + + return { ...(typesSpec[_specName] || {}), ...(typesChain[_chainName] || {}) };