@polkadot/api
Version:
Promise and RxJS wrappers around the Polkadot JS RPC
447 lines (336 loc) • 18.2 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.Init = void 0;
var _classPrivateFieldLooseBase2 = _interopRequireDefault(require("@babel/runtime/helpers/classPrivateFieldLooseBase"));
var _classPrivateFieldLooseKey2 = _interopRequireDefault(require("@babel/runtime/helpers/classPrivateFieldLooseKey"));
var _rxjs = require("rxjs");
var _types = require("@polkadot/types");
var _typesKnown = require("@polkadot/types-known");
var _util = require("@polkadot/util");
var _utilCrypto = require("@polkadot/util-crypto");
var _Decorate = require("./Decorate");
// Copyright 2017-2022 @polkadot/api authors & contributors
// SPDX-License-Identifier: Apache-2.0
const KEEPALIVE_INTERVAL = 10000;
const l = (0, _util.logger)('api/init');
function textToString(t) {
return t.toString();
}
var _healthTimer = /*#__PURE__*/(0, _classPrivateFieldLooseKey2.default)("healthTimer");
var _registries = /*#__PURE__*/(0, _classPrivateFieldLooseKey2.default)("registries");
var _updateSub = /*#__PURE__*/(0, _classPrivateFieldLooseKey2.default)("updateSub");
var _waitingRegistries = /*#__PURE__*/(0, _classPrivateFieldLooseKey2.default)("waitingRegistries");
var _onProviderConnect = /*#__PURE__*/(0, _classPrivateFieldLooseKey2.default)("onProviderConnect");
var _onProviderDisconnect = /*#__PURE__*/(0, _classPrivateFieldLooseKey2.default)("onProviderDisconnect");
var _onProviderError = /*#__PURE__*/(0, _classPrivateFieldLooseKey2.default)("onProviderError");
class Init extends _Decorate.Decorate {
constructor(options, type, decorateMethod) {
super(options, type, decorateMethod); // all injected types added to the registry for overrides
Object.defineProperty(this, _onProviderError, {
value: _onProviderError2
});
Object.defineProperty(this, _onProviderDisconnect, {
value: _onProviderDisconnect2
});
Object.defineProperty(this, _onProviderConnect, {
value: _onProviderConnect2
});
Object.defineProperty(this, _healthTimer, {
writable: true,
value: null
});
Object.defineProperty(this, _registries, {
writable: true,
value: []
});
Object.defineProperty(this, _updateSub, {
writable: true,
value: null
});
Object.defineProperty(this, _waitingRegistries, {
writable: true,
value: {}
});
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 {
(0, _classPrivateFieldLooseBase2.default)(this, _registries)[_registries] = (0, _classPrivateFieldLooseBase2.default)(options.source, _registries)[_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.firstValueFrom)(this._rpcCore.chain.getBlockHash(blockNumber)));
if (this.hasSubscriptions) {
this._rpcCore.provider.on('disconnected', () => (0, _classPrivateFieldLooseBase2.default)(this, _onProviderDisconnect)[_onProviderDisconnect]());
this._rpcCore.provider.on('error', e => (0, _classPrivateFieldLooseBase2.default)(this, _onProviderError)[_onProviderError](e));
this._rpcCore.provider.on('connected', () => (0, _classPrivateFieldLooseBase2.default)(this, _onProviderConnect)[_onProviderConnect]());
} else {
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) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
(0, _classPrivateFieldLooseBase2.default)(this, _onProviderConnect)[_onProviderConnect]();
}
}
/**
* @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, _typesKnown.getSpecTypes)(registry, chain, version.specName, version.specVersion));
registry.setHasher((0, _typesKnown.getSpecHasher)(registry, chain, version.specName)); // for bundled types, pull through the aliases defined
if (registry.knownTypes.typesBundle) {
registry.knownTypes.typesAlias = (0, _typesKnown.getSpecAlias)(registry, chain, version.specName);
}
registry.setMetadata(metadata, undefined, (0, _util.objectSpread)({}, (0, _typesKnown.getSpecExtensions)(registry, chain, version.specName), this._options.signedExtensions));
}
/**
* @description Returns the default versioned registry
*/
_getDefaultRegistry() {
return (0, _util.assertReturn)((0, _classPrivateFieldLooseBase2.default)(this, _registries)[_registries].find(_ref => {
let {
isDefault
} = _ref;
return 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.u8aToU8a)(blockHash);
const registry = await this.getBlockRegistry(u8aHash, knownVersion); // 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)
return this._createDecorated(registry, true, null, u8aHash).decoratedApi;
}
async _createBlockRegistry(blockHash, header, version) {
const registry = new _types.TypeRegistry(blockHash);
const metadata = new _types.Metadata(registry, await (0, _rxjs.firstValueFrom)(this._rpcCore.state.getMetadata.raw(header.parentHash)));
this._initRegistry(registry, this._runtimeChain, version, metadata); // add our new registry
const result = {
lastBlockHash: blockHash,
metadata,
registry,
specName: version.specName,
specVersion: version.specVersion
};
(0, _classPrivateFieldLooseBase2.default)(this, _registries)[_registries].push(result);
return result;
}
_cacheBlockRegistryProgress(key, creator) {
// look for waiting resolves
let waiting = (0, _classPrivateFieldLooseBase2.default)(this, _waitingRegistries)[_waitingRegistries][key];
if ((0, _util.isUndefined)(waiting)) {
// nothing waiting, construct new
waiting = (0, _classPrivateFieldLooseBase2.default)(this, _waitingRegistries)[_waitingRegistries][key] = new Promise((resolve, reject) => {
creator().then(registry => {
delete (0, _classPrivateFieldLooseBase2.default)(this, _waitingRegistries)[_waitingRegistries][key];
resolve(registry);
}).catch(error => {
delete (0, _classPrivateFieldLooseBase2.default)(this, _waitingRegistries)[_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 = (0, _classPrivateFieldLooseBase2.default)(this, _registries)[_registries].find(_ref2 => {
let {
specName,
specVersion
} = _ref2;
return specName.eq(version.specName) && specVersion.eq(version.specVersion);
});
if (existingViaVersion) {
existingViaVersion.lastBlockHash = blockHash;
return existingViaVersion;
}
}
return null;
}
async _getBlockRegistryViaHash(blockHash) {
// ensure we have everything required
(0, _util.assert)(this._genesisHash && this._runtimeVersion, '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.BN_ZERO,
parentHash: this._genesisHash
} : await (0, _rxjs.firstValueFrom)(this._rpcCore.chain.getHeader.raw(blockHash)));
(0, _util.assert)(!header.parentHash.isEmpty, '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, _typesKnown.getUpgradeVersion)(this._genesisHash, header.number);
const version = this.registry.createType('RuntimeVersionPartial', firstVersion && (lastVersion || firstVersion.specVersion.eq(this._runtimeVersion.specVersion)) ? {
specName: this._runtimeVersion.specName,
specVersion: firstVersion.specVersion
} : await (0, _rxjs.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
(0, _classPrivateFieldLooseBase2.default)(this, _registries)[_registries].find(_ref3 => {
let {
lastBlockHash
} = _ref3;
return lastBlockHash && (0, _util.u8aEq)(lastBlockHash, blockHash);
}) || // try to find via version
this._getBlockRegistryViaVersion(blockHash, knownVersion) || ( // return new or in-flight result
await this._cacheBlockRegistryProgress((0, _util.u8aToHex)(blockHash), () => this._getBlockRegistryViaHash(blockHash)))
);
}
async _loadMeta() {
var _this$_options$source;
// 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 = this._options.source) !== null && _this$_options$source !== void 0 && _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; s < sections.length; s++) {
const section = sections[s];
const methods = Object.keys(source.rpc[section]);
for (let m = 0; m < methods.length; m++) {
rpcs.push(`${section}_${methods[m]}`);
}
}
this._filterRpc(rpcs, (0, _typesKnown.getSpecRpc)(this.registry, source.runtimeChain, source.runtimeVersion.specName));
return [source.genesisHash, source.runtimeMetadata];
} // subscribe to metadata updates, inject the types on changes
_subscribeUpdates() {
if ((0, _classPrivateFieldLooseBase2.default)(this, _updateSub)[_updateSub] || !this.hasSubscriptions) {
return;
}
(0, _classPrivateFieldLooseBase2.default)(this, _updateSub)[_updateSub] = this._rpcCore.state.subscribeRuntimeVersion().pipe((0, _rxjs.switchMap)(version => {
var _this$_runtimeVersion;
return (// only retrieve the metadata when the on-chain version has been changed
(_this$_runtimeVersion = this._runtimeVersion) !== null && _this$_runtimeVersion !== void 0 && _this$_runtimeVersion.specVersion.eq(version.specVersion) ? (0, _rxjs.of)(false) : this._rpcCore.state.getMetadata().pipe((0, _rxjs.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(); // setup the data as per the current versions
thisRegistry.metadata = metadata;
thisRegistry.specVersion = version.specVersion;
this._initRegistry(this.registry, this._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.firstValueFrom)(this._rpcCore.chain.getBlockHash(0)), (0, _rxjs.firstValueFrom)(this._rpcCore.state.getRuntimeVersion()), (0, _rxjs.firstValueFrom)(this._rpcCore.system.chain()), (0, _rxjs.firstValueFrom)(this._rpcCore.system.properties()), (0, _rxjs.firstValueFrom)(this._rpcCore.rpc.methods()), optMetadata ? Promise.resolve(null) : (0, _rxjs.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 && optMetadata[metadataKey] ? new _types.Metadata(this.registry, optMetadata[metadataKey]) : await (0, _rxjs.firstValueFrom)(this._rpcCore.state.getMetadata())); // initializes the registry & RPC
this._initRegistry(this.registry, chain, runtimeVersion, metadata, chainProps);
this._filterRpc(rpcMethods.methods.map(textToString), (0, _typesKnown.getSpecRpc)(this.registry, chain, runtimeVersion.specName));
this._subscribeUpdates(); // setup the initial registry, when we have none
if (!(0, _classPrivateFieldLooseBase2.default)(this, _registries)[_registries].length) {
(0, _classPrivateFieldLooseBase2.default)(this, _registries)[_registries].push({
isDefault: true,
metadata,
registry: this.registry,
specName: runtimeVersion.specName,
specVersion: runtimeVersion.specVersion
});
} // get unique types & validate
metadata.getUniqTypes(this._options.throwOnUnknown || false);
return [genesisHash, metadata];
}
_initFromMeta(metadata) {
this._extrinsicType = metadata.asLatest.extrinsic.version.toNumber();
this._rx.extrinsicType = this._extrinsicType;
this._rx.genesisHash = this._genesisHash;
this._rx.runtimeVersion = this._runtimeVersion; // must be set here
// 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() {
// Only enable the health keepalive on WS, not needed on HTTP
(0, _classPrivateFieldLooseBase2.default)(this, _healthTimer)[_healthTimer] = this.hasSubscriptions ? setInterval(() => {
(0, _rxjs.firstValueFrom)(this._rpcCore.system.health()).catch(() => undefined);
}, KEEPALIVE_INTERVAL) : null;
}
_unsubscribeHealth() {
if ((0, _classPrivateFieldLooseBase2.default)(this, _healthTimer)[_healthTimer]) {
clearInterval((0, _classPrivateFieldLooseBase2.default)(this, _healthTimer)[_healthTimer]);
(0, _classPrivateFieldLooseBase2.default)(this, _healthTimer)[_healthTimer] = null;
}
}
_unsubscribeUpdates() {
if ((0, _classPrivateFieldLooseBase2.default)(this, _updateSub)[_updateSub]) {
(0, _classPrivateFieldLooseBase2.default)(this, _updateSub)[_updateSub].unsubscribe();
(0, _classPrivateFieldLooseBase2.default)(this, _updateSub)[_updateSub] = null;
}
}
_unsubscribe() {
this._unsubscribeHealth();
this._unsubscribeUpdates();
}
}
exports.Init = Init;
async function _onProviderConnect2() {
this._isConnected.next(true);
this.emit('connected');
try {
const cryptoReady = this._options.initWasm === false ? true : await (0, _utilCrypto.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);
}
}
function _onProviderDisconnect2() {
this._isConnected.next(false);
this._unsubscribeHealth();
this.emit('disconnected');
}
function _onProviderError2(error) {
this.emit('error', error);
}