UNPKG

@polkadot/api

Version:

Promise and RxJS wrappers around the Polkadot JS RPC

398 lines (397 loc) • 20.9 kB
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); } }