Files
api/packages/api/src/base/Init.ts
T

258 lines
8.8 KiB
TypeScript

// Copyright 2017-2020 @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 { SignedBlock, RuntimeVersion } from '@polkadot/types/interfaces';
import { ApiBase, ApiOptions, ApiTypes, DecorateMethod } from '../types';
import { Subscription, combineLatest, of } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';
import { Metadata, Text } from '@polkadot/types';
import { LATEST_EXTRINSIC_VERSION } from '@polkadot/types/extrinsic/Extrinsic';
import { getMetadataTypes, getSpecTypes } from '@polkadot/types-known';
import { logger } from '@polkadot/util';
import { cryptoWaitReady } from '@polkadot/util-crypto';
import Decorate from './Decorate';
const KEEPALIVE_INTERVAL = 15000;
const l = logger('api/init');
export default abstract class Init<ApiType extends ApiTypes> extends Decorate<ApiType> {
#healthTimer: NodeJS.Timeout | null = null;
#updateSub?: Subscription;
constructor (options: ApiOptions, type: ApiTypes, decorateMethod: DecorateMethod<ApiType>) {
super(options, type, decorateMethod);
if (!this.hasSubscriptions) {
l.warn('Api will be available in a limited mode since the provider does not support subscriptions');
}
// all injected types added to the registry for overrides
this.registry.setKnownTypes({
types: options.types,
typesAlias: options.typesAlias,
typesChain: options.typesChain,
typesSpec: options.typesSpec
});
// 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) {
this.registerTypes(options.types);
} else {
this.registry.setKnownTypes(options.source.registry.knownTypes);
}
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);
// If the provider was instantiated earlier, and has already emitted a
// 'connected' event, then the `on('connected')` won't fire anymore. To
// cater for this case, we call manually `this._onProviderConnect`.
if (this._rpcCore.provider.isConnected()) {
this.#onProviderConnect();
}
}
protected async _loadMeta (): Promise<boolean> {
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
this._runtimeMetadata = this._options.source?._isReady
? await this._metaFromSource(this._options.source)
: await this._metaFromChain(metadata);
return this._initFromMeta(this._runtimeMetadata);
}
// eslint-disable-next-line @typescript-eslint/require-await
private async _metaFromSource (source: ApiBase<any>): Promise<Metadata> {
this._extrinsicType = source.extrinsicVersion;
this._runtimeVersion = source.runtimeVersion;
this._genesisHash = source.genesisHash;
this.registry.setChainProperties(source.registry.getChainProperties());
const methods: string[] = [];
// manually build a list of all available methods in this RPC, we are
// going to filter on it to align the cloned RPC without making a call
Object.keys(source.rpc).forEach((section): void => {
Object.keys((source.rpc as any)[section]).forEach((method): void => {
methods.push(`${section}_${method}`);
});
});
this._filterRpcMethods(methods);
return source.runtimeMetadata;
}
// subscribe to metadata updates, inject the types on changes
private _subscribeUpdates (): void {
if (this.#updateSub || !this.hasSubscriptions) {
return;
}
this.#updateSub = this._rpcCore.state.subscribeRuntimeVersion().pipe(
switchMap((version: RuntimeVersion) =>
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.registry, this._runtimeChain as Text, version.specName, version.specVersion));
this.injectMetadata(metadata, false);
return true;
})
).subscribe();
}
private async _metaFromChain (optMetadata: Record<string, string>): Promise<Metadata> {
const [runtimeVersion, chain, chainProps] = await Promise.all([
this._rpcCore.state.getRuntimeVersion().toPromise(),
this._rpcCore.system.chain().toPromise(),
this._rpcCore.system.properties().toPromise()
]);
// 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(this.registry, chain, runtimeVersion.specName, runtimeVersion.specVersion));
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 = `${this._genesisHash}-${runtimeVersion.specVersion}`;
const metadata = metadataKey in optMetadata
? new Metadata(this.registry, optMetadata[metadataKey])
: await this._rpcCore.state.getMetadata().toPromise();
// get unique types & validate
metadata.getUniqTypes(false);
return metadata;
}
private async _initFromMeta (metadata: Metadata): Promise<boolean> {
// inject types based on metadata, if applicable
this.registerTypes(getMetadataTypes(this.registry, metadata.version));
const metaExtrinsic = metadata.asLatest.extrinsic;
// only inject if we are not a clone (global init)
if (metaExtrinsic.version.gtn(0)) {
this._extrinsicType = metaExtrinsic.version.toNumber();
} else if (!this._options.source) {
// 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 : LATEST_EXTRINSIC_VERSION;
}
this._rx.extrinsicType = this._extrinsicType;
this._rx.genesisHash = this._genesisHash;
this._rx.runtimeVersion = this._runtimeVersion;
this.injectMetadata(metadata, true);
// derive is last, since it uses the decorated rx
this._rx.derive = this._decorateDeriveRx(this._rxDecorateMethod);
this._derive = this._decorateDerive(this._decorateMethod);
return true;
}
#onProviderConnect = async (): Promise<void> => {
this.emit('connected');
this._isConnected.next(true);
try {
const [hasMeta, cryptoReady] = await Promise.all([
this._loadMeta(),
this._options.initWasm === false
? Promise.resolve(true)
: 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);
}
}
#onProviderDisconnect = (): void => {
this.emit('disconnected');
this._isConnected.next(false);
if (this.#healthTimer) {
clearInterval(this.#healthTimer);
this.#healthTimer = null;
}
};
#onProviderError = (error: Error): void => {
this.emit('error', error);
};
}