UNPKG

@polkadot/api

Version:

Promise and RxJS wrappers around the Polkadot JS RPC

340 lines (339 loc) • 17.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Init = void 0; const rxjs_1 = require("rxjs"); const types_1 = require("@polkadot/types"); const types_known_1 = require("@polkadot/types-known"); const util_1 = require("@polkadot/util"); const util_crypto_1 = require("@polkadot/util-crypto"); const Decorate_js_1 = require("./Decorate.js"); const KEEPALIVE_INTERVAL = 10000; const WITH_VERSION_SHORTCUT = false; const l = (0, util_1.logger)('api/init'); function textToString(t) { return t.toString(); } class Init extends Decorate_js_1.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) => (0, rxjs_1.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(util_1.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((0, types_known_1.getSpecTypes)(registry, chain, version.specName, version.specVersion)); registry.setHasher((0, types_known_1.getSpecHasher)(registry, chain, version.specName)); // for bundled types, pull through the aliases defined if (registry.knownTypes.typesBundle) { registry.knownTypes.typesAlias = (0, types_known_1.getSpecAlias)(registry, chain, version.specName); } registry.setMetadata(metadata, undefined, (0, util_1.objectSpread)({}, (0, types_known_1.getSpecExtensions)(registry, chain, version.specName), this._options.signedExtensions), this._options.noInitWarn); } /** * @description Returns the default versioned registry */ _getDefaultRegistry() { return (0, util_1.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 = (0, util_1.u8aToU8a)(blockHash); const u8aHex = (0, util_1.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 types_1.TypeRegistry(blockHash); const metadata = new types_1.Metadata(registry, await (0, rxjs_1.firstValueFrom)(this._rpcCore.state.getMetadata.raw(header.parentHash))); 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 ((0, util_1.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: util_1.BN_ZERO, parentHash: this._genesisHash } : await (0, rxjs_1.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] = (0, types_known_1.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 (0, rxjs_1.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 && (0, util_1.u8aEq)(lastBlockHash, blockHash)) || // try to find via version this._getBlockRegistryViaVersion(blockHash, knownVersion) || // return new or in-flight result await this._cacheBlockRegistryProgress((0, util_1.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) { 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, (0, types_known_1.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((0, rxjs_1.switchMap)((version) => // only retrieve the metadata when the on-chain version has been changed this._runtimeVersion?.specVersion.eq(version.specVersion) ? (0, rxjs_1.of)(false) : this._rpcCore.state.getMetadata().pipe((0, rxjs_1.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, chainMetadata] = await Promise.all([ (0, rxjs_1.firstValueFrom)(this._rpcCore.chain.getBlockHash(0)), (0, rxjs_1.firstValueFrom)(this._rpcCore.state.getRuntimeVersion()), (0, rxjs_1.firstValueFrom)(this._rpcCore.system.chain()), (0, rxjs_1.firstValueFrom)(this._rpcCore.system.properties()), (0, rxjs_1.firstValueFrom)(this._rpcCore.rpc.methods()), optMetadata ? Promise.resolve(null) : (0, rxjs_1.firstValueFrom)(this._rpcCore.state.getMetadata()) ]); // 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 = chainMetadata || (optMetadata?.[metadataKey] ? new types_1.Metadata(this.registry, optMetadata[metadataKey]) : await (0, rxjs_1.firstValueFrom)(this._rpcCore.state.getMetadata())); // initializes the registry & RPC this._initRegistry(this.registry, chain, runtimeVersion, metadata, chainProps); this._filterRpc(rpcMethods.methods.map(textToString), (0, types_known_1.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; } _subscribeHealth() { this._unsubscribeHealth(); // Only enable the health keepalive on WS, not needed on HTTP this.__internal__healthTimer = this.hasSubscriptions ? setInterval(() => { (0, rxjs_1.firstValueFrom)(this._rpcCore.system.health.raw()).catch(util_1.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 (0, util_crypto_1.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); } } exports.Init = Init;