@polkadot/api
Version:
Promise and RxJS wrappers around the Polkadot JS RPC
398 lines (397 loc) • 20.9 kB
JavaScript
import { firstValueFrom, map, of, switchMap } from 'rxjs';
import { Metadata, TypeRegistry } from '@polkadot/types';
import { getSpecAlias, getSpecExtensions, getSpecHasher, getSpecRpc, getSpecTypes, getUpgradeVersion } from '@polkadot/types-known';
import { assertReturn, BN_ZERO, isUndefined, logger, noop, objectSpread, u8aEq, u8aToHex, u8aToU8a } from '@polkadot/util';
import { blake2AsHex, cryptoWaitReady } from '@polkadot/util-crypto';
import { Decorate } from './Decorate.js';
const KEEPALIVE_INTERVAL = 10000;
const WITH_VERSION_SHORTCUT = false;
const SUPPORTED_METADATA_VERSIONS = [15, 14];
const l = logger('api/init');
function textToString(t) {
return t.toString();
}
export class Init extends Decorate {
__internal__atLast = null;
__internal__healthTimer = null;
__internal__registries = [];
__internal__updateSub = null;
__internal__waitingRegistries = {};
constructor(options, type, decorateMethod) {
super(options, type, decorateMethod);
// all injected types added to the registry for overrides
this.registry.setKnownTypes(options);
// 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.__internal__registries = options.source.__internal__registries;
}
this._rpc = this._decorateRpc(this._rpcCore, this._decorateMethod);
this._rx.rpc = this._decorateRpc(this._rpcCore, this._rxDecorateMethod);
if (this.supportMulti) {
this._queryMulti = this._decorateMulti(this._decorateMethod);
this._rx.queryMulti = this._decorateMulti(this._rxDecorateMethod);
}
this._rx.signer = options.signer;
this._rpcCore.setRegistrySwap((blockHash) => this.getBlockRegistry(blockHash));
this._rpcCore.setResolveBlockHash((blockNumber) => firstValueFrom(this._rpcCore.chain.getBlockHash(blockNumber)));
if (this.hasSubscriptions) {
this._rpcCore.provider.on('disconnected', () => this.__internal__onProviderDisconnect());
this._rpcCore.provider.on('error', (e) => this.__internal__onProviderError(e));
this._rpcCore.provider.on('connected', () => this.__internal__onProviderConnect());
}
else if (!this._options.noInitWarn) {
l.warn('Api will be available in a limited mode since the provider does not support subscriptions');
}
// 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.__internal__onProviderConnect().catch(noop);
}
}
/**
* @description Decorates a registry based on the runtime version
*/
_initRegistry(registry, chain, version, metadata, chainProps) {
registry.clearCache();
registry.setChainProperties(chainProps || this.registry.getChainProperties());
registry.setKnownTypes(this._options);
registry.register(getSpecTypes(registry, chain, version.specName, version.specVersion));
registry.setHasher(getSpecHasher(registry, chain, version.specName));
// for bundled types, pull through the aliases defined
if (registry.knownTypes.typesBundle) {
registry.knownTypes.typesAlias = getSpecAlias(registry, chain, version.specName);
}
registry.setMetadata(metadata, undefined, objectSpread({}, getSpecExtensions(registry, chain, version.specName), this._options.signedExtensions), this._options.noInitWarn);
}
/**
* @description Returns the default versioned registry
*/
_getDefaultRegistry() {
return assertReturn(this.__internal__registries.find(({ isDefault }) => isDefault), 'Initialization error, cannot find the default registry');
}
/**
* @description Returns a decorated API instance at a specific point in time
*/
async at(blockHash, knownVersion) {
const u8aHash = u8aToU8a(blockHash);
const u8aHex = u8aToHex(u8aHash);
const registry = await this.getBlockRegistry(u8aHash, knownVersion);
if (!this.__internal__atLast || this.__internal__atLast[0] !== u8aHex) {
// always create a new decoration - since we are pointing to a specific hash, this
// means that all queries needs to use that hash (not a previous one already existing)
this.__internal__atLast = [u8aHex, this._createDecorated(registry, true, null, u8aHash).decoratedApi];
}
return this.__internal__atLast[1];
}
async _createBlockRegistry(blockHash, header, version) {
const registry = new TypeRegistry(blockHash);
const metadata = await this._retrieveMetadata(version.apis, header.parentHash, registry);
const runtimeChain = this._runtimeChain;
if (!runtimeChain) {
throw new Error('Invalid initializion order, runtimeChain is not available');
}
this._initRegistry(registry, runtimeChain, version, metadata);
// add our new registry
const result = { counter: 0, lastBlockHash: blockHash, metadata, registry, runtimeVersion: version };
this.__internal__registries.push(result);
return result;
}
_cacheBlockRegistryProgress(key, creator) {
// look for waiting resolves
let waiting = this.__internal__waitingRegistries[key];
if (isUndefined(waiting)) {
// nothing waiting, construct new
waiting = this.__internal__waitingRegistries[key] = new Promise((resolve, reject) => {
creator()
.then((registry) => {
delete this.__internal__waitingRegistries[key];
resolve(registry);
})
.catch((error) => {
delete this.__internal__waitingRegistries[key];
reject(error);
});
});
}
return waiting;
}
_getBlockRegistryViaVersion(blockHash, version) {
if (version) {
// check for pre-existing registries. We also check specName, e.g. it
// could be changed like in Westmint with upgrade from shell -> westmint
const existingViaVersion = this.__internal__registries.find(({ runtimeVersion: { specName, specVersion } }) => specName.eq(version.specName) &&
specVersion.eq(version.specVersion));
if (existingViaVersion) {
existingViaVersion.counter++;
existingViaVersion.lastBlockHash = blockHash;
return existingViaVersion;
}
}
return null;
}
async _getBlockRegistryViaHash(blockHash) {
// ensure we have everything required
if (!this._genesisHash || !this._runtimeVersion) {
throw new Error('Cannot retrieve data on an uninitialized chain');
}
// We have to assume that on the RPC layer the calls used here does not call back into
// the registry swap, so getHeader & getRuntimeVersion should not be historic
const header = this.registry.createType('HeaderPartial', this._genesisHash.eq(blockHash)
? { number: BN_ZERO, parentHash: this._genesisHash }
: await firstValueFrom(this._rpcCore.chain.getHeader.raw(blockHash)));
if (header.parentHash.isEmpty) {
throw new Error('Unable to retrieve header and parent from supplied hash');
}
// get the runtime version, either on-chain or via an known upgrade history
const [firstVersion, lastVersion] = getUpgradeVersion(this._genesisHash, header.number);
const version = this.registry.createType('RuntimeVersionPartial', WITH_VERSION_SHORTCUT && (firstVersion && (lastVersion ||
firstVersion.specVersion.eq(this._runtimeVersion.specVersion)))
? { apis: firstVersion.apis, specName: this._runtimeVersion.specName, specVersion: firstVersion.specVersion }
: await firstValueFrom(this._rpcCore.state.getRuntimeVersion.raw(header.parentHash)));
return (
// try to find via version
this._getBlockRegistryViaVersion(blockHash, version) ||
// return new or in-flight result
await this._cacheBlockRegistryProgress(version.toHex(), () => this._createBlockRegistry(blockHash, header, version)));
}
/**
* @description Sets up a registry based on the block hash defined
*/
async getBlockRegistry(blockHash, knownVersion) {
return (
// try to find via blockHash
this.__internal__registries.find(({ lastBlockHash }) => lastBlockHash && u8aEq(lastBlockHash, blockHash)) ||
// try to find via version
this._getBlockRegistryViaVersion(blockHash, knownVersion) ||
// return new or in-flight result
await this._cacheBlockRegistryProgress(u8aToHex(blockHash), () => this._getBlockRegistryViaHash(blockHash)));
}
async _loadMeta() {
// on re-connection to the same chain, we don't want to re-do everything from chain again
if (this._isReady) {
// on re-connection only re-subscribe to chain updates if we are not a clone
if (!this._options.source) {
this._subscribeUpdates();
}
return true;
}
this._unsubscribeUpdates();
// only load from on-chain if we are not a clone (default path), alternatively
// just use the values from the source instance provided
[this._genesisHash, this._runtimeMetadata] = this._options.source?._isReady
? await this._metaFromSource(this._options.source)
: await this._metaFromChain(this._options.metadata);
return this._initFromMeta(this._runtimeMetadata);
}
// eslint-disable-next-line @typescript-eslint/require-await
async _metaFromSource(source) {
this._extrinsicType = source.extrinsicVersion;
this._runtimeChain = source.runtimeChain;
this._runtimeVersion = source.runtimeVersion;
// 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
const sections = Object.keys(source.rpc);
const rpcs = [];
for (let s = 0, scount = sections.length; s < scount; s++) {
const section = sections[s];
const methods = Object.keys(source.rpc[section]);
for (let m = 0, mcount = methods.length; m < mcount; m++) {
rpcs.push(`${section}_${methods[m]}`);
}
}
this._filterRpc(rpcs, getSpecRpc(this.registry, source.runtimeChain, source.runtimeVersion.specName));
return [source.genesisHash, source.runtimeMetadata];
}
// subscribe to metadata updates, inject the types on changes
_subscribeUpdates() {
if (this.__internal__updateSub || !this.hasSubscriptions) {
return;
}
this.__internal__updateSub = this._rpcCore.state.subscribeRuntimeVersion().pipe(switchMap((version) =>
// only retrieve the metadata when the on-chain version has been changed
this._runtimeVersion?.specVersion.eq(version.specVersion)
? of(false)
: this._rpcCore.state.getMetadata().pipe(map((metadata) => {
l.log(`Runtime version updated to spec=${version.specVersion.toString()}, tx=${version.transactionVersion.toString()}`);
this._runtimeMetadata = metadata;
this._runtimeVersion = version;
this._rx.runtimeVersion = version;
// update the default registry version
const thisRegistry = this._getDefaultRegistry();
const runtimeChain = this._runtimeChain;
if (!runtimeChain) {
throw new Error('Invalid initializion order, runtimeChain is not available');
}
// setup the data as per the current versions
thisRegistry.metadata = metadata;
thisRegistry.runtimeVersion = version;
this._initRegistry(this.registry, runtimeChain, version, metadata);
this._injectMetadata(thisRegistry, true);
return true;
})))).subscribe();
}
async _metaFromChain(optMetadata) {
const [genesisHash, runtimeVersion, chain, chainProps, rpcMethods] = await Promise.all([
firstValueFrom(this._rpcCore.chain.getBlockHash(0)),
firstValueFrom(this._rpcCore.state.getRuntimeVersion()),
firstValueFrom(this._rpcCore.system.chain()),
firstValueFrom(this._rpcCore.system.properties()),
firstValueFrom(this._rpcCore.rpc.methods())
]);
// set our chain version & genesisHash as returned
this._runtimeChain = chain;
this._runtimeVersion = runtimeVersion;
this._rx.runtimeVersion = runtimeVersion;
// retrieve metadata, either from chain or as pass-in via options
const metadataKey = `${genesisHash.toHex() || '0x'}-${runtimeVersion.specVersion.toString()}`;
const metadata = optMetadata?.[metadataKey]
? new Metadata(this.registry, optMetadata[metadataKey])
: await this._retrieveMetadata(runtimeVersion.apis);
// initializes the registry & RPC
this._initRegistry(this.registry, chain, runtimeVersion, metadata, chainProps);
this._filterRpc(rpcMethods.methods.map(textToString), getSpecRpc(this.registry, chain, runtimeVersion.specName));
this._subscribeUpdates();
// setup the initial registry, when we have none
if (!this.__internal__registries.length) {
this.__internal__registries.push({ counter: 0, isDefault: true, metadata, registry: this.registry, runtimeVersion });
}
// get unique types & validate
metadata.getUniqTypes(this._options.throwOnUnknown || false);
return [genesisHash, metadata];
}
_initFromMeta(metadata) {
const runtimeVersion = this._runtimeVersion;
if (!runtimeVersion) {
throw new Error('Invalid initializion order, runtimeVersion is not available');
}
this._extrinsicType = metadata.asLatest.extrinsic.version.toNumber();
this._rx.extrinsicType = this._extrinsicType;
this._rx.genesisHash = this._genesisHash;
this._rx.runtimeVersion = runtimeVersion;
// inject metadata and adjust the types as detected
this._injectMetadata(this._getDefaultRegistry(), 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;
}
/**
* @internal
*
* Tries to use runtime api calls to retrieve metadata. This ensures the api initializes with the latest metadata.
* If the runtime call is not there it will use the rpc method.
*/
async _retrieveMetadata(apis, at, registry) {
let metadataVersion = null;
const metadataApi = apis.find(([a]) => a.eq(blake2AsHex('Metadata', 64)));
const typeRegistry = registry || this.registry;
// This chain does not have support for the metadataApi, or does not have the required version.
if (!metadataApi || metadataApi[1].toNumber() < 2) {
l.warn('MetadataApi not available, rpc::state::get_metadata will be used.');
return at
? new Metadata(typeRegistry, await firstValueFrom(this._rpcCore.state.getMetadata.raw(at)))
: await firstValueFrom(this._rpcCore.state.getMetadata());
}
try {
const metadataVersionsAsBytes = at
? await firstValueFrom(this._rpcCore.state.call.raw('Metadata_metadata_versions', '0x', at))
: await firstValueFrom(this._rpcCore.state.call('Metadata_metadata_versions', '0x'));
const versions = typeRegistry.createType('Vec<u32>', metadataVersionsAsBytes);
// For unstable versions of the metadata the last value is set to u32 MAX in the runtime. This ensures only supported stable versions are used.
metadataVersion = versions.filter((ver) => SUPPORTED_METADATA_VERSIONS.includes(ver.toNumber())).reduce((largest, current) => current.gt(largest) ? current : largest);
}
catch (e) {
l.debug(e.message);
l.warn('error with state_call::Metadata_metadata_versions, rpc::state::get_metadata will be used');
}
// When the metadata version does not align with the latest supported versions we ensure not to call the metadata runtime call.
// I noticed on some previous runtimes that have support for `Metadata_metadata_at_version` that very irregular versions were being returned.
// This was evident with runtime 1000000 - it return a very large number. This ensures we always stick within what is supported.
if (metadataVersion && !SUPPORTED_METADATA_VERSIONS.includes(metadataVersion.toNumber())) {
metadataVersion = null;
}
if (metadataVersion) {
try {
const metadataBytes = at
? await firstValueFrom(this._rpcCore.state.call.raw('Metadata_metadata_at_version', u8aToHex(metadataVersion.toU8a()), at))
: await firstValueFrom(this._rpcCore.state.call('Metadata_metadata_at_version', u8aToHex(metadataVersion.toU8a())));
// When the metadata is called with `at` it is required to use `.raw`. Therefore since the length prefix is not present the
// need to create a `Raw` type is necessary before creating the `OpaqueMetadata` type or else there will be a magic number
// mismatch
const rawMeta = at
? typeRegistry.createType('Raw', metadataBytes).toU8a()
: metadataBytes;
const opaqueMetadata = typeRegistry.createType('Option<OpaqueMetadata>', rawMeta).unwrapOr(null);
if (opaqueMetadata) {
return new Metadata(typeRegistry, opaqueMetadata.toHex());
}
}
catch (e) {
l.debug(e.message);
l.warn('error with state_call::Metadata_metadata_at_version, rpc::state::get_metadata will be used');
}
}
return at
? new Metadata(typeRegistry, await firstValueFrom(this._rpcCore.state.getMetadata.raw(at)))
: await firstValueFrom(this._rpcCore.state.getMetadata());
}
_subscribeHealth() {
this._unsubscribeHealth();
// Only enable the health keepalive on WS, not needed on HTTP
this.__internal__healthTimer = this.hasSubscriptions
? setInterval(() => {
firstValueFrom(this._rpcCore.system.health.raw()).catch(noop);
}, KEEPALIVE_INTERVAL)
: null;
}
_unsubscribeHealth() {
if (this.__internal__healthTimer) {
clearInterval(this.__internal__healthTimer);
this.__internal__healthTimer = null;
}
}
_unsubscribeUpdates() {
if (this.__internal__updateSub) {
this.__internal__updateSub.unsubscribe();
this.__internal__updateSub = null;
}
}
_unsubscribe() {
this._unsubscribeHealth();
this._unsubscribeUpdates();
}
async __internal__onProviderConnect() {
this._isConnected.next(true);
this.emit('connected');
try {
const cryptoReady = this._options.initWasm === false
? true
: await cryptoWaitReady();
const hasMeta = await this._loadMeta();
this._subscribeHealth();
if (hasMeta && !this._isReady && cryptoReady) {
this._isReady = true;
this.emit('ready', this);
}
}
catch (_error) {
const error = new Error(`FATAL: Unable to initialize the API: ${_error.message}`);
l.error(error);
this.emit('error', error);
}
}
__internal__onProviderDisconnect() {
this._isConnected.next(false);
this._unsubscribe();
this.emit('disconnected');
}
__internal__onProviderError(error) {
this.emit('error', error);
}
}